React Hooks Best Practices: Writing Clean and Efficient Code
React Hooks Best Practices: Writing Clean and Efficient Code
React Hooks have transformed how we write React components. This guide covers best practices to help you write cleaner, more efficient code.
Understanding the Rules of Hooks
Before diving into best practices, remember the two fundamental rules:
- Only call hooks at the top level - Don't call hooks inside loops, conditions, or nested functions
- Only call hooks from React functions - Call them from React function components or custom hooks
useState Best Practices
Lazy Initialization
For expensive initial state calculations, use a function:
// Bad - runs on every render
const [state, setState] = useState(expensiveComputation());
// Good - runs only once
const [state, setState] = useState(() => expensiveComputation());
Functional Updates
When new state depends on previous state:
// Bad - can lead to stale state
const increment = () => setCount(count + 1);
// Good - always uses latest state
const increment = () => setCount(prev => prev + 1);
Managing Complex State
// For related data, use a single object
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
// Update with spread operator
const updateName = (name) => {
setUser(prev => ({ ...prev, name }));
};
// For independent state, use multiple useState calls
const [name, setName] = useState('');
const [email, setEmail] = useState('');
useEffect Best Practices
Dependency Array
Always include all dependencies:
// Bad - missing dependency
useEffect(() => {
fetchData(userId);
}, []); // userId should be in deps
// Good
useEffect(() => {
fetchData(userId);
}, [userId]);
Cleanup Function
Always clean up side effects:
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then setData;
return () => controller.abort();
}, [url]);
Separate Concerns
Use multiple effects for unrelated logic:
// Good - separate effects for different concerns
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
useEffect(() => {
const interval = setInterval(() => {
console.log('Tick');
}, 1000);
return () => clearInterval(interval);
}, []);
useMemo and useCallback
When to Use useMemo
// Use for expensive calculations
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// Don't overuse - simple calculations don't need memoization
const total = items.reduce((sum, item) => sum + item.price, 0);
When to Use useCallback
// Use when passing callbacks to optimized child components
const handleClick = useCallback((id) => {
setSelected(id);
}, []);
// Use when callback is a dependency
const fetchData = useCallback(async () => {
const result = await api.get(id);
setData(result);
}, [id]);
useEffect(() => {
fetchData();
}, [fetchData]);
Custom Hooks
Extract reusable logic into custom hooks:
// hooks/useLocalStorage.js
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = useCallback((value) => {
setStoredValue(prev => {
const valueToStore = value instanceof Function ? value(prev) : value;
localStorage.setItem(key, JSON.stringify(valueToStore));
return valueToStore;
});
}, [key]);
return [storedValue, setValue];
}
// Usage
const [theme, setTheme] = useLocalStorage('theme', 'light');
useReducer for Complex State
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'RESET':
return { ...state, count: 0 };
default:
return state;
}
};
const [state, dispatch] = useReducer(reducer, { count: 0 });
Common Patterns
Fetch Data Pattern
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url, { signal: controller.signal });
const json = await response.json();
setData(json);
setError(null);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
}
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
Toggle Pattern
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
Performance Tips
- Avoid creating objects in render - They create new references
- Use React.memo wisely - Only when re-renders are expensive
- Virtualize long lists - Use react-window or react-virtualized
- Code split - Use React.lazy and Suspense
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}
Conclusion
Following these best practices will help you write React Hooks that are clean, efficient, and maintainable. Remember to always clean up effects, include all dependencies, and extract reusable logic into custom hooks.