react1/25/20248 min read
React Hooks Best Practices
Essential patterns and best practices for using React Hooks effectively
reacthooksbest-practicesperformancepatterns
By Zen Frontend
React Hooks Best Practices
React Hooks revolutionized how we write React components. Understanding best practices is crucial for writing maintainable, performant, and bug-free React applications.
Rules of Hooks
1. Only Call Hooks at the Top Level
// ❌ Bad - Conditional hook call
function BadComponent({ shouldUseEffect }) {
if (shouldUseEffect) {
useEffect(() => {
console.log('Effect runs');
}, []);
}
return <div>Bad example</div>;
}
// ✅ Good - Always call hooks at the top level
function GoodComponent({ shouldUseEffect }) {
useEffect(() => {
if (shouldUseEffect) {
console.log('Effect runs');
}
}, [shouldUseEffect]);
return <div>Good example</div>;
}
2. Only Call Hooks from React Functions
// ❌ Bad - Calling hook in regular function
function regularFunction() {
const [state, setState] = useState(0); // Error!
}
// ✅ Good - Only in React components or custom hooks
function MyComponent() {
const [state, setState] = useState(0);
return <div>{state}</div>;
}
useState Best Practices
1. Use Functional Updates for State Based on Previous State
// ❌ Bad - Direct state access
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1); // May not work correctly with rapid clicks
};
return <button onClick={increment}>{count}</button>;
}
// ✅ Good - Functional update
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(prevCount => prevCount + 1); // Always uses latest state
};
return <button onClick={increment}>{count}</button>;
}
2. Initialize State with Functions for Expensive Calculations
// ❌ Bad - Expensive calculation on every render
function ExpensiveComponent({ items }) {
const [filteredItems, setFilteredItems] = useState(
items.filter(item => item.active) // Runs on every render
);
return <div>{filteredItems.length} items</div>;
}
// ✅ Good - Lazy initialization
function ExpensiveComponent({ items }) {
const [filteredItems, setFilteredItems] = useState(() =>
items.filter(item => item.active) // Only runs once
);
return <div>{filteredItems.length} items</div>;
}
3. Group Related State
// ❌ Bad - Multiple related state variables
function UserForm() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
// ... many setters
}
// ✅ Good - Group related state
function UserForm() {
const [user, setUser] = useState({
firstName: '',
lastName: '',
email: '',
age: 0
});
const updateUser = (field, value) => {
setUser(prev => ({ ...prev, [field]: value }));
};
return (
<form>
<input
value={user.firstName}
onChange={(e) => updateUser('firstName', e.target.value)}
/>
{/* ... other inputs */}
</form>
);
}
useEffect Best Practices
1. Always Include Dependencies
// ❌ Bad - Missing dependencies
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // Missing userId dependency
return <div>{user?.name}</div>;
}
// ✅ Good - Complete dependency array
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // Includes all dependencies
return <div>{user?.name}</div>;
}
2. Clean Up Side Effects
// ❌ Bad - No cleanup
function Timer() {
const [time, setTime] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setTime(prev => prev + 1);
}, 1000);
// No cleanup - memory leak!
}, []);
return <div>{time}</div>;
}
// ✅ Good - Proper cleanup
function Timer() {
const [time, setTime] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setTime(prev => prev + 1);
}, 1000);
return () => clearInterval(interval); // Cleanup
}, []);
return <div>{time}</div>;
}
3. Separate Concerns
// ❌ Bad - Multiple concerns in one effect
function UserDashboard({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [notifications, setNotifications] = useState([]);
useEffect(() => {
// Fetching user
fetchUser(userId).then(setUser);
// Fetching posts
fetchPosts(userId).then(setPosts);
// Setting up notifications
const subscription = subscribeToNotifications(userId, setNotifications);
return () => subscription.unsubscribe();
}, [userId]);
return <div>Dashboard</div>;
}
// ✅ Good - Separate effects for separate concerns
function UserDashboard({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [notifications, setNotifications] = useState([]);
// User data effect
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// Posts effect
useEffect(() => {
fetchPosts(userId).then(setPosts);
}, [userId]);
// Notifications effect
useEffect(() => {
const subscription = subscribeToNotifications(userId, setNotifications);
return () => subscription.unsubscribe();
}, [userId]);
return <div>Dashboard</div>;
}
useMemo and useCallback Best Practices
1. Don't Overuse useMemo and useCallback
// ❌ Bad - Unnecessary memoization
function ExpensiveComponent({ items }) {
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]); // Sorting is not that expensive
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // Simple function doesn't need memoization
return <div>{sortedItems.map(item => <div key={item.id}>{item.name}</div>)}</div>;
}
// ✅ Good - Only memoize when necessary
function ExpensiveComponent({ items }) {
const sortedItems = items.sort((a, b) => a.name.localeCompare(b.name));
const handleClick = () => {
console.log('clicked');
};
return <div>{sortedItems.map(item => <div key={item.id}>{item.name}</div>)}</div>;
}
2. Use useMemo for Expensive Calculations
// ✅ Good - Expensive calculation
function DataVisualization({ data }) {
const processedData = useMemo(() => {
return data.map(item => ({
...item,
normalized: item.value / Math.max(...data.map(d => d.value)),
category: categorize(item.value)
}));
}, [data]);
return <Chart data={processedData} />;
}
3. Use useCallback for Stable References
// ✅ Good - Stable callback reference
function ParentComponent() {
const [count, setCount] = useState(0);
const handleIncrement = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return <ChildComponent onIncrement={handleIncrement} />;
}
const ChildComponent = React.memo(({ onIncrement }) => {
return <button onClick={onIncrement}>Increment</button>;
});
Custom Hooks Best Practices
1. Start with "use" Prefix
// ✅ Good - Custom hook naming
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);
}
};
return [storedValue, setValue];
}
2. Return Consistent API
// ✅ Good - Consistent return pattern
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
3. Handle Edge Cases
// ✅ Good - Robust custom hook
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
if (delay <= 0) {
setDebouncedValue(value);
return;
}
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
Performance Best Practices
1. Avoid Creating Objects in Render
// ❌ Bad - New object on every render
function BadComponent({ items }) {
return (
<ExpensiveChild
config={{ theme: 'dark', size: 'large' }} // New object every render
items={items}
/>
);
}
// ✅ Good - Stable object reference
function GoodComponent({ items }) {
const config = useMemo(() => ({
theme: 'dark',
size: 'large'
}), []);
return <ExpensiveChild config={config} items={items} />;
}
2. Use React.memo Wisely
// ✅ Good - Memoize expensive components
const ExpensiveChild = React.memo(({ data, onUpdate }) => {
const processedData = useMemo(() => {
return data.map(item => expensiveCalculation(item));
}, [data]);
return <div>{processedData.map(item => <div key={item.id}>{item.result}</div>)}</div>;
});
// ❌ Bad - Memoizing simple components
const SimpleButton = React.memo(({ onClick, children }) => {
return <button onClick={onClick}>{children}</button>;
}); // Unnecessary memoization
Common Pitfalls to Avoid
1. Stale Closures
// ❌ Bad - Stale closure
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // Always uses initial count value
}, 1000);
return () => clearInterval(interval);
}, []); // Missing count dependency
return <div>{count}</div>;
}
// ✅ Good - Fresh closure
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prev => prev + 1); // Always uses latest count
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>{count}</div>;
}
2. Infinite Loops
// ❌ Bad - Infinite loop
function BadComponent() {
const [data, setData] = useState([]);
useEffect(() => {
fetchData().then(setData);
}, [data]); // data changes, effect runs, data changes again...
return <div>{data.length} items</div>;
}
// ✅ Good - Proper dependencies
function GoodComponent() {
const [data, setData] = useState([]);
useEffect(() => {
fetchData().then(setData);
}, []); // Only run once
return <div>{data.length} items</div>;
}
Key Concepts
- Rules of Hooks: Always follow the two rules
- Dependency Arrays: Include all dependencies in useEffect
- Cleanup: Always clean up side effects
- Performance: Use memoization judiciously
- Custom Hooks: Extract reusable logic
- Stable References: Avoid creating new objects/functions in render
Common Interview Questions
- What are the rules of hooks?
- When should you use useMemo vs useCallback?
- How do you avoid stale closures in useEffect?
- What's the difference between useMemo and useCallback?
- How do you handle cleanup in useEffect?
Related Topics
- Custom Hooks
- useEffect Patterns
- Performance Optimization
- React.memo
- useRef and useImperativeHandle