React Hooks Explained 🎣
React Hooks revolutionized how we write React components by allowing us to use state and other React features in functional components. This guide covers all the essential hooks with practical examples to help you master React development.
Table of content
- useState - Managing Component State
- useEffect - Side Effects and Lifecycle
- useContext - Sharing Data Across Components
- useReducer - Complex State Management
- useMemo - Optimizing Expensive Calculations
- useCallback - Memoizing Functions
- useRef - Accessing DOM and Storing Mutable Values
- Custom Hooks - Reusable Logic
1. useState - Managing Component State
useState is the most fundamental hook that allows functional components to manage local state. It returns an array with two elements: the current state value and a function to update it.
Key Points:
- State updates trigger re-renders
- You can pass an initial value or a function that returns the initial value
- State updates are asynchronous
- For complex state, consider using useReducer
1import { useState } from 'react';
2
3function Counter() {
4 const [count, setCount] = useState(0);
5
6 return (
7 <div>
8 <p>Count: {count}</p>
9 <button onClick={() => setCount(count + 1)}>
10 Increment
11 </button>
12 <button onClick={() => setCount(count - 1)}>
13 Decrement
14 </button>
15 <button onClick={() => setCount(0)}>
16 Reset
17 </button>
18 </div>
19 );
20}2. useEffect - Side Effects and Lifecycle
useEffect lets you perform side effects in functional components. It's the equivalent of componentDidMount, componentDidUpdate, and componentWillUnmount combined.
Key Points:
- Runs after every render by default (unless dependencies are specified)
- Dependency array controls when the effect runs
- Return a cleanup function to prevent memory leaks
- Empty dependency array [] means the effect runs only once on mount
1import { useState, useEffect } from 'react';
2
3function UserProfile({ userId }) {
4 const [user, setUser] = useState(null);
5 const [loading, setLoading] = useState(true);
6
7 useEffect(() => {
8 // Fetch user data when component mounts or userId changes
9 async function fetchUser() {
10 setLoading(true);
11 const response = await fetch(`/api/users/${userId}`);
12 const userData = await response.json();
13 setUser(userData);
14 setLoading(false);
15 }
16
17 fetchUser();
18
19 // Cleanup function (runs when component unmounts or before re-running effect)
20 return () => {
21 // Cancel any pending requests
22 console.log('Cleanup: Component unmounting or userId changed');
23 };
24 }, [userId]); // Dependency array: effect runs when userId changes
25
26 if (loading) return <div>Loading...</div>;
27 return <div>{user?.name}</div>;
28}3. useContext - Sharing Data Across Components
useContext allows you to consume context values without prop drilling. It's perfect for sharing global state like themes, user authentication, or language preferences.
Key Points:
- Requires a Context Provider higher up in the component tree
- All components using the same context will re-render when context value changes
- Great for avoiding prop drilling
- Combine with useState or useReducer for stateful context
1import { createContext, useContext, useState } from 'react';
2
3// 1. Create a context
4const ThemeContext = createContext();
5
6// 2. Create a provider component
7function ThemeProvider({ children }) {
8 const [theme, setTheme] = useState('light');
9
10 const toggleTheme = () => {
11 setTheme(prev => prev === 'light' ? 'dark' : 'light');
12 };
13
14 return (
15 <ThemeContext.Provider value={{ theme, toggleTheme }}>
16 {children}
17 </ThemeContext.Provider>
18 );
19}
20
21// 3. Use the context in child components
22function ThemedButton() {
23 const { theme, toggleTheme } = useContext(ThemeContext);
24
25 return (
26 <button
27 onClick={toggleTheme}
28 style={{
29 background: theme === 'light' ? '#fff' : '#000',
30 color: theme === 'light' ? '#000' : '#fff'
31 }}
32 >
33 Current theme: {theme}
34 </button>
35 );
36}
37
38// 4. Wrap your app with the provider
39function App() {
40 return (
41 <ThemeProvider>
42 <ThemedButton />
43 </ThemeProvider>
44 );
45}4. useReducer - Complex State Management
useReducer is an alternative to useState, ideal for managing complex state logic. It follows the same pattern as Redux reducers, making state updates predictable and testable.
Key Points:
- Best for state with multiple sub-values or complex update logic
- Reducer function must be pure (no side effects)
- Actions are objects with a type property
- More predictable than useState for complex state
1import { useReducer } from 'react';
2
3// 1. Define the initial state
4const initialState = { count: 0 };
5
6// 2. Define the reducer function
7function counterReducer(state, action) {
8 switch (action.type) {
9 case 'increment':
10 return { count: state.count + 1 };
11 case 'decrement':
12 return { count: state.count - 1 };
13 case 'reset':
14 return { count: 0 };
15 case 'set':
16 return { count: action.payload };
17 default:
18 throw new Error(`Unknown action: ${action.type}`);
19 }
20}
21
22function Counter() {
23 // 3. Use the reducer hook
24 const [state, dispatch] = useReducer(counterReducer, initialState);
25
26 return (
27 <div>
28 <p>Count: {state.count}</p>
29 <button onClick={() => dispatch({ type: 'increment' })}>
30 Increment
31 </button>
32 <button onClick={() => dispatch({ type: 'decrement' })}>
33 Decrement
34 </button>
35 <button onClick={() => dispatch({ type: 'reset' })}>
36 Reset
37 </button>
38 <button onClick={() => dispatch({ type: 'set', payload: 10 })}>
39 Set to 10
40 </button>
41 </div>
42 );
43}5. useMemo - Optimizing Expensive Calculations
useMemo memoizes the result of expensive calculations, recalculating only when dependencies change. This helps optimize performance by avoiding unnecessary computations.
Key Points:
- Only use for expensive calculations, not for every computation
- Returns the memoized value
- Dependency array works like useEffect
- Don't use for side effects (use useEffect instead)
1import { useState, useMemo } from 'react';
2
3function ExpensiveCalculation({ numbers }) {
4 // This expensive calculation only runs when 'numbers' changes
5 const sum = useMemo(() => {
6 console.log('Calculating sum...');
7 return numbers.reduce((acc, num) => acc + num, 0);
8 }, [numbers]); // Dependency array: recalculate only when numbers change
9
10 return <div>Sum: {sum}</div>;
11}
12
13function App() {
14 const [numbers] = useState([1, 2, 3, 4, 5]);
15 const [count, setCount] = useState(0);
16
17 return (
18 <div>
19 <button onClick={() => setCount(count + 1)}>
20 Count: {count}
21 </button>
22 {/* Sum won't recalculate when count changes */}
23 <ExpensiveCalculation numbers={numbers} />
24 </div>
25 );
26}6. useCallback - Memoizing Functions
useCallback returns a memoized version of a callback function that only changes if one of its dependencies has changed. It's useful when passing callbacks to optimized child components.
Key Points:
- Prevents unnecessary re-renders of child components
- Works with React.memo() for optimization
- Returns the same function reference if dependencies haven't changed
- Use when passing functions as props to memoized components
1import { useState, useCallback, memo } from 'react';
2
3// Child component that receives a callback
4const ChildComponent = memo(({ onClick, name }) => {
5 console.log(`Rendering ${name}`);
6 return <button onClick={onClick}>{name}</button>;
7});
8
9function ParentComponent() {
10 const [count, setCount] = useState(0);
11 const [name, setName] = useState('Child');
12
13 // Without useCallback: new function created on every render
14 // const handleClick = () => console.log('Clicked');
15
16 // With useCallback: function is memoized and only recreated when dependencies change
17 const handleClick = useCallback(() => {
18 console.log('Button clicked');
19 }, []); // Empty deps: function never changes
20
21 // Another example with dependency
22 const handleNameChange = useCallback((newName) => {
23 setName(newName);
24 }, []); // setName is stable, so empty deps is fine
25
26 return (
27 <div>
28 <p>Count: {count}</p>
29 <button onClick={() => setCount(count + 1)}>Increment</button>
30 <ChildComponent onClick={handleClick} name={name} />
31 </div>
32 );
33}7. useRef - Accessing DOM and Storing Mutable Values
useRef returns a mutable ref object that persists for the lifetime of the component. It has two main use cases: accessing DOM elements and storing mutable values that don't trigger re-renders.
Key Points:
- Refs don't trigger re-renders when their value changes
- Perfect for storing previous values, timers, or DOM references
- Access DOM elements directly via the ref.current property
- Refs persist across re-renders but don't cause them
1import { useRef, useEffect, useState } from 'react';
2
3function RefExample() {
4 // 1. Accessing DOM elements
5 const inputRef = useRef(null);
6 const [count, setCount] = useState(0);
7
8 // 2. Storing mutable values that don't trigger re-renders
9 const renderCount = useRef(0);
10 const previousCount = useRef(0);
11
12 useEffect(() => {
13 renderCount.current += 1;
14 previousCount.current = count;
15 });
16
17 const focusInput = () => {
18 // Direct DOM manipulation
19 inputRef.current?.focus();
20 };
21
22 return (
23 <div>
24 <input ref={inputRef} type="text" placeholder="Click button to focus" />
25 <button onClick={focusInput}>Focus Input</button>
26
27 <p>Current count: {count}</p>
28 <p>Previous count: {previousCount.current}</p>
29 <p>Component rendered {renderCount.current} times</p>
30 <button onClick={() => setCount(count + 1)}>Increment</button>
31 </div>
32 );
33}8. Custom Hooks - Reusable Logic
Custom Hooks are JavaScript functions that start with "use" and can call other hooks. They allow you to extract component logic into reusable functions, promoting code reusability and separation of concerns.
Key Points:
- Must start with "use" (React convention)
- Can call other hooks inside them
- Share stateful logic between components
- Each component using a custom hook has its own independent state
Common Custom Hook Patterns:
- useFetch: Handle API calls with loading and error states
- useLocalStorage: Sync state with browser localStorage
- useDebounce: Debounce values for search inputs
- useWindowSize: Track window dimensions
- useToggle: Toggle boolean state
1import { useState, useEffect, useCallback } from 'react';
2
3// Custom Hook 1: useFetch - Fetch data with loading and error states
4function useFetch(url) {
5 const [data, setData] = useState(null);
6 const [loading, setLoading] = useState(true);
7 const [error, setError] = useState(null);
8
9 useEffect(() => {
10 async function fetchData() {
11 try {
12 setLoading(true);
13 const response = await fetch(url);
14 if (!response.ok) throw new Error('Failed to fetch');
15 const result = await response.json();
16 setData(result);
17 setError(null);
18 } catch (err) {
19 setError(err.message);
20 } finally {
21 setLoading(false);
22 }
23 }
24
25 fetchData();
26 }, [url]);
27
28 return { data, loading, error };
29}
30
31// Custom Hook 2: useLocalStorage - Sync state with localStorage
32function useLocalStorage(key, initialValue) {
33 const [storedValue, setStoredValue] = useState(() => {
34 try {
35 const item = window.localStorage.getItem(key);
36 return item ? JSON.parse(item) : initialValue;
37 } catch (error) {
38 return initialValue;
39 }
40 });
41
42 const setValue = useCallback((value) => {
43 try {
44 setStoredValue(value);
45 window.localStorage.setItem(key, JSON.stringify(value));
46 } catch (error) {
47 console.error(error);
48 }
49 }, [key]);
50
51 return [storedValue, setValue];
52}
53
54// Custom Hook 3: useDebounce - Debounce a value
55function useDebounce(value, delay) {
56 const [debouncedValue, setDebouncedValue] = useState(value);
57
58 useEffect(() => {
59 const handler = setTimeout(() => {
60 setDebouncedValue(value);
61 }, delay);
62
63 return () => {
64 clearTimeout(handler);
65 };
66 }, [value, delay]);
67
68 return debouncedValue;
69}
70
71// Usage Example
72function App() {
73 const { data, loading, error } = useFetch('/api/users');
74 const [name, setName] = useLocalStorage('name', '');
75 const [searchTerm, setSearchTerm] = useState('');
76 const debouncedSearch = useDebounce(searchTerm, 500);
77
78 return (
79 <div>
80 <input
81 value={searchTerm}
82 onChange={(e) => setSearchTerm(e.target.value)}
83 placeholder="Search (debounced)"
84 />
85 <p>Debounced value: {debouncedSearch}</p>
86 </div>
87 );
88}Best Practices
- Only call hooks at the top level: Don't call hooks inside loops, conditions, or nested functions
- Only call hooks from React functions: Call hooks from React function components or custom hooks
- Optimize with care: Don't overuse useMemo and useCallback; they have their own overhead
- Keep effects focused: Each useEffect should have a single responsibility
- Clean up side effects: Always return cleanup functions from useEffect when needed