Common Hook Mistakes
In this page:
Mistake: Missing Dependencies
When an effect, memo, or callback uses a value from component scope but doesn't list it in the dependency array, that value gets frozen at whatever it was on the render the Hook was first set up with -- a bug known as a stale closure.
Note: Let the ESLint react-hooks plugin's exhaustive-deps rule guide you -- it flags missing dependencies automatically.
Warning: A missing dependency doesn't cause an immediate error -- it silently produces outdated behavior that can be hard to notice until much later.
Example: Mistake: Missing Dependencies
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function StaleExample() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
const id = setInterval(() => {
console.log("Count is", count); // stale! always logs the initial value
}, 1000);
return () => clearInterval(id);
}, []); // missing 'count' dependency
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<StaleExample />);
</script>
</body>
</html>
Mistake: Conditional Hook Calls
Putting a Hook call inside an if statement means it might be skipped on some renders but not others, breaking React's assumption that Hooks are called in the same order every time. This produces confusing errors that often don't clearly point back to the real cause.
Note: Move the conditional logic inside the Hook (like inside an effect body) rather than around the Hook call itself.
Warning: This mistake often produces the cryptic error 'Rendered more hooks than during the previous render.'
Example: Mistake: Conditional Hook Calls
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function FixedVersion({ showExtra }) {
const [count, setCount] = React.useState(0); // always called
const [extra, setExtra] = React.useState(0); // always called, even if unused
return <button onClick={() => setCount(count + 1)}>{count}{showExtra ? ` (extra: ${extra})` : ""}</button>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<FixedVersion showExtra={true} />);
</script>
</body>
</html>
Mistake: Mutating State Directly
Modifying an array or object in state directly (like calling .push() on a state array) can leave the reference unchanged, so React may not detect that anything changed and skip re-rendering. Always create a new copy when updating object or array state.
Note: Reach for the spread operator (...) or array methods that return new arrays (.map, .filter) whenever updating object/array state.
Warning: This mistake is especially sneaky because the data DID technically change in memory -- it's just that React's change-detection didn't notice.
Example: Mistake: Mutating State Directly
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function TagList() {
const [tags, setTags] = React.useState(["react"]);
function addTag() {
setTags([...tags, "hooks"]); // correct: new array via spread
}
return (
<div>
<ul>{tags.map((t, i) => <li key={i}>{t}</li>)}</ul>
<button onClick={addTag}>Add Tag</button>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<TagList />);
</script>
</body>
</html>
- Forgetting a dependency in useEffect/useMemo/useCallback's dependency array, leading to stale values.
- Calling Hooks conditionally, breaking React's required consistent call order.
- Directly mutating state (arrays/objects) instead of creating new copies before updating.
- Most Hook bugs come from a small, well-known set of common mistakes.
- Stale closures happen when a dependency is missing from an effect's array.
- Conditional Hook calls violate the Rules of Hooks and cause runtime errors.
- The ESLint react-hooks plugin catches many of these mistakes automatically.
These mistakes and their fixes apply to all Hooks since their introduction in React 16.8.
Chapter Quiz — Complete all 12 topics to unlock
0/12 topics done
Complete these topics first: