Mastering React Hooks
React Hooks revolutionized how we write React components, allowing us to use state and other React features in functional components. In this comprehensive guide, we'll explore the most important hooks and learn how to create custom ones.
Understanding useState
The useState
hook is the foundation of state management in functional components:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Working with useEffect
The useEffect
hook handles side effects in your components:
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchUser() {
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Creating Custom Hooks
Custom hooks allow you to extract component logic into reusable functions:
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) {
return initialValue;
}
});
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Error saving to localStorage:', error);
}
};
return [storedValue, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<div>
<p>Current theme: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle theme
</button>
</div>
);
}
Advanced Hook Patterns
useReducer for Complex State
When state logic becomes complex, useReducer
provides a more predictable way to manage state:
import React, { 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();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}
useCallback and useMemo for Performance
Optimize your components with useCallback
and useMemo
:
import React, { useState, useCallback, useMemo } from 'react';
function ExpensiveComponent({ items, onItemClick }) {
const expensiveValue = useMemo(() => {
return items.reduce((acc, item) => acc + item.value, 0);
}, [items]);
const handleClick = useCallback((id) => {
onItemClick(id);
}, [onItemClick]);
return (
<div>
<p>Total value: {expensiveValue}</p>
{items.map(item => (
<button key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</button>
))}
</div>
);
}
Best Practices
- Always use the dependency array in
useEffect
to avoid infinite loops - Extract complex logic into custom hooks for reusability
- Use
useCallback
anduseMemo
sparingly - only when you have performance issues - Keep hooks at the top level - never call hooks inside loops, conditions, or nested functions
- Name custom hooks with the "use" prefix for consistency
React Hooks provide a powerful and flexible way to manage state and side effects in functional components. By mastering these patterns, you'll write cleaner, more maintainable React code.