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

  1. useState - Managing Component State
  2. useEffect - Side Effects and Lifecycle
  3. useContext - Sharing Data Across Components
  4. useReducer - Complex State Management
  5. useMemo - Optimizing Expensive Calculations
  6. useCallback - Memoizing Functions
  7. useRef - Accessing DOM and Storing Mutable Values
  8. 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:

useState.jsx
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:

useEffect.jsx
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:

useContext.jsx
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:

useReducer.jsx
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:

useMemo.jsx
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:

useCallback.jsx
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:

useRef.jsx
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:

Common Custom Hook Patterns:

customHooks.jsx
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