Complete Guide to React Hooks in 2025
Introduction to React Hooks
React Hooks revolutionized how we write React components by allowing us to use state and other React features without writing class components. Introduced in React 16.8, Hooks have become the standard way to build React applications in 2025.
In this comprehensive guide, we'll cover all essential React Hooks, their use cases, best practices, and common pitfalls to avoid. Whether you're new to React or looking to deepen your understanding, this guide will provide you with practical knowledge and real-world examples.
1. useState - Managing Component State
useState is the most fundamental Hook that lets you add state to functional components. It returns an array with two elements: the current state value and a function to update it.
Basic Usage
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Advanced State Management
You can use useState with objects and arrays:
const [user, setUser] = useState({
name: 'John',
age: 30,
email: 'john@example.com'
});
// Update single property
setUser(prevUser => ({
...prevUser,
age: 31
}));
2. useEffect - Side Effects & Lifecycle
useEffect lets you perform side effects in functional components. It's similar to componentDidMount, componentDidUpdate, and componentWillUnmount combined.
Basic Data Fetching
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchUser() {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
setLoading(false);
}
fetchUser();
}, [userId]); // Re-run when userId changes
if (loading) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
Cleanup Functions
Always cleanup subscriptions and timers to prevent memory leaks:
useEffect(() => {
const timer = setInterval(() => {
console.log('Tick');
}, 1000);
// Cleanup function
return () => clearInterval(timer);
}, []);
exhaustive-deps rule to catch these issues.
3. useContext - Sharing Data Globally
useContext allows you to consume context values without wrapping components in Consumer components. Perfect for theme management, authentication, and global state.
import { createContext, useContext } from 'react';
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <div className={theme}>Current theme: {theme}</div>;
}
4. useReducer - Complex State Logic
useReducer is preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.
import { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return initialState;
default:
throw new Error('Unknown action');
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</>
);
}
5. Custom Hooks - Reusable Logic
Custom Hooks let you extract component logic into reusable functions. They must start with "use" and can call other Hooks.
Example: useFetch Hook
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) throw new Error('Network error');
const json = await response.json();
setData(json);
setError(null);
} catch (err) {
setError(err.message);
setData(null);
} finally {
setLoading(false);
}
}
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage
function UserList() {
const { data, loading, error } = useFetch('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <ul>{data.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}
6. useCallback & useMemo - Performance Optimization
These Hooks help optimize performance by memoizing values and functions to prevent unnecessary re-renders.
useCallback
import { useCallback } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
// Memoized callback
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // Only recreate if dependencies change
return <ChildComponent onClick={handleClick} />;
}
useMemo
import { useMemo } from 'react';
function ExpensiveComponent({ items }) {
const total = useMemo(() => {
return items.reduce((sum, item) => sum + item.price, 0);
}, [items]); // Only recalculate when items change
return <div>Total: ${total}</div>;
}
useCallback and useMemo only when you have measurable performance issues. Premature optimization can make code harder to read.
7. useRef - DOM References & Mutable Values
useRef returns a mutable ref object that persists for the full lifetime of the component. Useful for accessing DOM elements and storing mutable values that don't trigger re-renders.
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
// Focus input on mount
inputRef.current.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
Best Practices & Common Pitfalls
Rules of Hooks
- Only call Hooks at the top level - Don't call Hooks inside loops, conditions, or nested functions
- Only call Hooks from React functions - Call them from functional components or custom Hooks
- Use ESLint plugin - Install
eslint-plugin-react-hooksto enforce these rules
Dependency Array Best Practices
- Always include all values from component scope that change over time and are used by the effect
- Use the ESLint rule
react-hooks/exhaustive-depsto catch missing dependencies - If you need to update state based on previous state, use the functional update form
Performance Considerations
- Don't create new objects/arrays in dependency arrays - they'll cause infinite loops
- Extract expensive calculations into
useMemo - Memoize callbacks passed to child components with
useCallback - Consider using React.memo() for expensive components
Conclusion
React Hooks have fundamentally changed how we write React applications, making code more readable, reusable, and easier to test. By mastering the core Hooks and understanding when to create custom Hooks, you'll be able to build robust and maintainable React applications.
Remember to follow the Rules of Hooks, properly manage dependencies, and optimize only when necessary. With practice, Hooks will become second nature and you'll wonder how you ever lived without them!
- Explore React 18's new
useTransitionanduseDeferredValueHooks for concurrent features - Learn about
useImperativeHandlefor advanced ref forwarding - Study
useLayoutEffectfor synchronous DOM mutations - Build custom Hooks for your common patterns (useLocalStorage, useDebounce, etc.)