Mastering React Hooks: Advanced Patterns and Best Practices
Deep dive into React Hooks with advanced patterns, custom hooks, and performance optimization techniques for modern React applications.
Alex Chen
Author
Mastering React Hooks: Advanced Patterns and Best Practices
React Hooks have revolutionized how we write React components, enabling us to use state and other React features in functional components. In this comprehensive guide, we'll explore advanced patterns, custom hooks, and performance optimization techniques.
Understanding React Hooks Fundamentals
The Hook Rules
Before diving into advanced patterns, let's review the fundamental rules:
- Only call Hooks at the top level - Never inside loops, conditions, or nested functions
- Only call Hooks from React functions - Either React function components or custom Hooks
// ❌ Wrong - conditional Hook call
function MyComponent({ condition }) {
if (condition) {
const [state, setState] = useState(0); // This breaks the rules!
}
return <div>Content</div>;
}
// ✅ Correct - Hook at top level
function MyComponent({ condition }) {
const [state, setState] = useState(0);
if (condition) {
// Use the state here
}
return <div>Content</div>;
}
Advanced useState Patterns
Functional Updates
When the new state depends on the previous state, use functional updates:
function Counter() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
// ✅ Functional update ensures we get the latest state
setCount(prevCount => prevCount + 1);
}, []);
const incrementBy = useCallback((amount) => {
setCount(prevCount => prevCount + amount);
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={() => incrementBy(5)}>+5</button>
</div>
);
}
Complex State with useReducer
For complex state logic, useReducer
is often better than useState
:
const initialState = {
loading: false,
data: null,
error: null
};
function dataReducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, data: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
case 'RESET':
return initialState;
default:
throw new Error(`Unknown action type: ${action.type}`);
}
}
function DataComponent() {
const [state, dispatch] = useReducer(dataReducer, initialState);
const fetchData = async () => {
dispatch({ type: 'FETCH_START' });
try {
const response = await fetch('/api/data');
const data = await response.json();
dispatch({ type: 'FETCH_SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'FETCH_ERROR', payload: error.message });
}
};
return (
<div>
{state.loading && <p>Loading...</p>}
{state.error && <p>Error: {state.error}</p>}
{state.data && <pre>{JSON.stringify(state.data, null, 2)}</pre>}
<button onClick={fetchData}>Fetch Data</button>
</div>
);
}
Custom Hooks: Reusable Logic
Data Fetching Hook
function useApi(url) {
const [state, dispatch] = useReducer(dataReducer, initialState);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
dispatch({ type: 'FETCH_START' });
try {
const response = await fetch(url);
const data = await response.json();
if (!cancelled) {
dispatch({ type: 'FETCH_SUCCESS', payload: data });
}
} catch (error) {
if (!cancelled) {
dispatch({ type: 'FETCH_ERROR', payload: error.message });
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, [url]);
const refetch = useCallback(() => {
dispatch({ type: 'RESET' });
}, []);
return { ...state, refetch };
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error, refetch } = useApi(`/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>
<button onClick={refetch}>Refresh</button>
</div>
);
}
Local Storage Hook
function useLocalStorage(key, initialValue) {
// Get from local storage then parse stored json or return 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;
}
});
// Return a wrapped version of useState's setter function that persists the new value to localStorage
const setValue = useCallback((value) => {
try {
// Allow value to be a function so we have the same API as useState
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);
}
}, [key, storedValue]);
return [storedValue, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'en');
return (
<div>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
<option value="en">English</option>
<option value="es">Spanish</option>
</select>
</div>
);
}
Performance Optimization
useMemo for Expensive Calculations
function ExpensiveComponent({ items, filter }) {
// Only recalculate when items or filter changes
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
const expensiveValue = useMemo(() => {
console.log('Calculating expensive value...');
return filteredItems.reduce((sum, item) => sum + item.value, 0);
}, [filteredItems]);
return (
<div>
<p>Total Value: {expensiveValue}</p>
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
useCallback for Function Memoization
function TodoList({ todos, onToggle, onDelete }) {
const [filter, setFilter] = useState('all');
// Memoize the filter function to prevent unnecessary re-renders
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
}, [todos, filter]);
// Memoize event handlers
const handleToggle = useCallback((id) => {
onToggle(id);
}, [onToggle]);
const handleDelete = useCallback((id) => {
onDelete(id);
}, [onDelete]);
return (
<div>
<div>
<button
onClick={() => setFilter('all')}
className={filter === 'all' ? 'active' : ''}
>
All
</button>
<button
onClick={() => setFilter('active')}
className={filter === 'active' ? 'active' : ''}
>
Active
</button>
<button
onClick={() => setFilter('completed')}
className={filter === 'completed' ? 'active' : ''}
>
Completed
</button>
</div>
<ul>
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
</div>
);
}
// Memoized component to prevent unnecessary re-renders
const TodoItem = React.memo(({ todo, onToggle, onDelete }) => {
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span className={todo.completed ? 'completed' : ''}>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
});
Advanced Patterns
Compound Components Pattern
function Tabs({ children, defaultTab = 0 }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<div className="tabs">
{React.Children.map(children, (child, index) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
isActive: index === activeTab,
onActivate: () => setActiveTab(index),
index
});
}
return child;
})}
</div>
);
}
function Tab({ children, isActive, onActivate, label }) {
return (
<div>
<button
onClick={onActivate}
className={isActive ? 'active' : ''}
>
{label}
</button>
{isActive && <div className="tab-content">{children}</div>}
</div>
);
}
// Usage
function App() {
return (
<Tabs defaultTab={0}>
<Tab label="Tab 1">
<p>Content for tab 1</p>
</Tab>
<Tab label="Tab 2">
<p>Content for tab 2</p>
</Tab>
<Tab label="Tab 3">
<p>Content for tab 3</p>
</Tab>
</Tabs>
);
}
Context + Hooks Pattern
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = useCallback(() => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
}, []);
const value = useMemo(() => ({
theme,
toggleTheme,
colors: theme === 'light' ? lightColors : darkColors
}), [theme, toggleTheme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// Usage
function ThemedButton() {
const { theme, toggleTheme, colors } = useTheme();
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: colors.primary,
color: colors.text
}}
>
Current theme: {theme}
</button>
);
}
Testing Hooks
Testing Custom Hooks
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('should initialize with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('should initialize with provided value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('should increment count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('should decrement count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
});
Best Practices Summary
- Follow the Rules of Hooks - Always call Hooks at the top level
- Use the Right Hook -
useState
for simple state,useReducer
for complex state - Optimize Performance - Use
useMemo
anduseCallback
judiciously - Create Custom Hooks - Extract reusable logic into custom Hooks
- Handle Cleanup - Always clean up subscriptions and async operations
- Test Your Hooks - Write tests for custom Hooks and complex logic
- Use TypeScript - Add type safety to your Hooks
Conclusion
React Hooks provide a powerful way to manage state and side effects in functional components. By mastering these advanced patterns and following best practices, you can write more maintainable, performant, and reusable React code.
The key is to understand when and how to use each Hook effectively, create custom Hooks for reusable logic, and always keep performance and testing in mind.