React Hooks: A Complete Guide for Modern Development
Master React Hooks with this comprehensive guide covering useState, useEffect, custom hooks, and advanced patterns for building powerful React applications.

React Hooks: A Complete Guide for Modern Development
React Hooks revolutionized how we write React components. This comprehensive guide will take you from basic hook usage to advanced patterns that will make your React applications more powerful and maintainable.
What are React Hooks?
Hooks are functions that let you “hook into” React features from functional components. They allow you to use state and other React features without writing class components.
Basic Hooks
useState: Managing Component State
The most fundamental hook for managing state in functional components:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(prev => prev - 1)}>
Decrement
</button>
</div>
);
}
useState with Objects
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
const updateUser = (field, value) => {
setUser(prev => ({
...prev,
[field]: value
}));
};
return (
<form>
<input
value={user.name}
onChange={(e) => updateUser('name', e.target.value)}
placeholder="Name"
/>
<input
value={user.email}
onChange={(e) => updateUser('email', e.target.value)}
placeholder="Email"
/>
</form>
);
}
useEffect: Side Effects
Handle side effects like API calls, subscriptions, and DOM manipulation:
import React, { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUsers();
}, []); // Empty dependency array = run once on mount
const fetchUsers = async () => {
try {
const response = await fetch('/api/users');
const userData = await response.json();
setUsers(userData);
} catch (error) {
console.error('Failed to fetch users:', error);
} finally {
setLoading(false);
}
};
if (loading) return <div>Loading...</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
useEffect Cleanup
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Cleanup function
return () => clearInterval(interval);
}, []);
return <div>Seconds: {seconds}</div>;
}
Conditional Effects
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
if (query) {
searchAPI(query).then(setResults);
}
}, [query]); // Run when query changes
return (
<div>
{results.map(result => (
<div key={result.id}>{result.title}</div>
))}
</div>
);
}
Advanced Hooks
useContext: Global State Management
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
className={`btn btn-${theme}`}
>
Current theme: {theme}
</button>
);
}
useReducer: Complex State Logic
import React, { useReducer } from 'react';
const initialState = {
items: [],
loading: false,
error: null
};
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
items: [...state.items, action.payload]
};
case 'TOGGLE_TODO':
return {
...state,
items: state.items.map(item =>
item.id === action.payload
? { ...item, completed: !item.completed }
: item
)
};
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload };
default:
return state;
}
}
function TodoList() {
const [state, dispatch] = useReducer(todoReducer, initialState);
const addTodo = (text) => {
dispatch({
type: 'ADD_TODO',
payload: {
id: Date.now(),
text,
completed: false
}
});
};
return (
<div>
{state.items.map(todo => (
<div key={todo.id}>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}
>
{todo.text}
</span>
<button
onClick={() => dispatch({
type: 'TOGGLE_TODO',
payload: todo.id
})}
>
Toggle
</button>
</div>
))}
</div>
);
}
useMemo: Performance Optimization
import React, { useState, useMemo } from 'react';
function ExpensiveComponent({ items, filter }) {
const [count, setCount] = useState(0);
// Expensive calculation only runs when items or filter change
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
// This calculation runs on every render
const expensiveValue = useMemo(() => {
console.log('Calculating expensive value...');
return filteredItems.reduce((sum, item) => sum + item.value, 0);
}, [filteredItems]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<p>Expensive Value: {expensiveValue}</p>
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
useCallback: Function Memoization
import React, { useState, useCallback } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// Without useCallback, this function is recreated on every render
const handleClick = useCallback(() => {
console.log('Button clicked!');
}, []); // Empty deps = never recreated
const handleNameChange = useCallback((newName) => {
setName(newName);
}, []); // This could also be empty since setName is stable
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<ChildComponent
onClick={handleClick}
onNameChange={handleNameChange}
name={name}
/>
</div>
);
}
const ChildComponent = React.memo(({ onClick, onNameChange, name }) => {
console.log('ChildComponent rendered');
return (
<div>
<button onClick={onClick}>Click Me</button>
<input
value={name}
onChange={(e) => onNameChange(e.target.value)}
placeholder="Enter name"
/>
</div>
);
});
Custom Hooks
useLocalStorage Hook
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
return [storedValue, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current theme: {theme}
</button>
);
}
useFetch Hook
import { useState, useEffect } from 'react';
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url, JSON.stringify(options)]);
return { data, loading, error };
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
useDebounce Hook
import { useState, useEffect } from 'react';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Usage
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
// Perform search
console.log('Searching for:', debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}
Hook Patterns and Best Practices
Rules of Hooks
- Only call hooks at the top level: Never in loops, conditions, or nested functions
- Only call hooks from React functions: Components or custom hooks
// ❌ Wrong - conditional hook
function BadComponent({ condition }) {
if (condition) {
const [state, setState] = useState(0); // This breaks the rules!
}
return <div>Bad component</div>;
}
// ✅ Correct - conditional logic inside hook
function GoodComponent({ condition }) {
const [state, setState] = useState(condition ? 0 : null);
if (condition) {
return <div>State: {state}</div>;
}
return <div>No state needed</div>;
}
Optimizing Re-renders
import React, { useState, useCallback, useMemo } from 'react';
function OptimizedComponent({ items }) {
const [filter, setFilter] = useState('');
const [sortOrder, setSortOrder] = useState('asc');
// Memoize expensive calculations
const filteredAndSortedItems = useMemo(() => {
const filtered = items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
return filtered.sort((a, b) => {
if (sortOrder === 'asc') {
return a.name.localeCompare(b.name);
}
return b.name.localeCompare(a.name);
});
}, [items, filter, sortOrder]);
// Memoize callback functions
const handleFilterChange = useCallback((e) => {
setFilter(e.target.value);
}, []);
const handleSortToggle = useCallback(() => {
setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
}, []);
return (
<div>
<input
value={filter}
onChange={handleFilterChange}
placeholder="Filter items..."
/>
<button onClick={handleSortToggle}>
Sort {sortOrder === 'asc' ? '↑' : '↓'}
</button>
<ItemList items={filteredAndSortedItems} />
</div>
);
}
const ItemList = React.memo(({ items }) => (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
));
Error Boundaries with Hooks
import React, { useState, useEffect } from 'react';
function useErrorHandler() {
const [error, setError] = useState(null);
const resetError = () => setError(null);
const captureError = (error) => {
setError(error);
console.error('Captured error:', error);
};
return { error, resetError, captureError };
}
function ComponentWithErrorHandling() {
const { error, resetError, captureError } = useErrorHandler();
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(response => response.json())
.then(setData)
.catch(captureError);
}, [captureError]);
if (error) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={resetError}>Try Again</button>
</div>
);
}
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}
Testing Hooks
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('should decrement counter', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(9);
});
Common Mistakes and Solutions
Stale Closures
// ❌ Problem: Stale closure
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // Always adds 1 to initial count!
}, 1000);
return () => clearInterval(interval);
}, []); // Empty deps cause stale closure
return <div>{count}</div>;
}
// ✅ Solution: Use functional update
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prevCount => prevCount + 1); // Always gets current count
}, 1000);
return () => clearInterval(interval);
}, []); // Empty deps are now safe
return <div>{count}</div>;
}
Infinite Re-renders
// ❌ Problem: Missing dependencies
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // Missing userId dependency
return <div>{user?.name}</div>;
}
// ✅ Solution: Include all dependencies
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // Include userId
return <div>{user?.name}</div>;
}
Conclusion
React Hooks have transformed how we write React applications, making functional components more powerful and code more reusable. Key takeaways:
- Start with basic hooks: Master useState and useEffect first
- Create custom hooks: Encapsulate reusable logic
- Optimize when needed: Use useMemo and useCallback judiciously
- Follow the rules: Hooks have specific requirements for proper operation
- Think in hooks: Design your components around hook patterns
Remember, hooks are just functions that let you hook into React features. The more you practice with them, the more natural they’ll become in your React development workflow.
What hook pattern would you like to implement in your next project? Share your experiences in the comments!