Runnable lesson · press Try it on the examples to edit and run them here.
Most hook bugs come from a short list of mistakes. Learn to recognise each one, and you can fix problems quickly.
1. Calling hooks conditionally
if (user) {
const [name, setName] = useState(user.name); // Error: order changes between renders
}
Fix: call hooks unconditionally at the top; put the if inside or below. See Rules of Hooks.
2. Stale closures from missing dependencies
The effect or callback keeps using an old value.
useEffect(() => {
const id = setInterval(() => console.log(count), 1000); // always logs the first count
return () => clearInterval(id);
}, []); // count is missing
Fix: add the dependency, or use an updater function, or a ref for the latest value. See Effect Dependencies.
3. Forgetting cleanup
Timers, listeners and subscriptions keep running after the component is gone.
Fix: return a cleanup function that undoes what the effect started. See Cleanup Functions.
4. Infinite loops
useEffect(() => {
setData(fetchedData.map(transform)); // new array every time
}, [data]); // depends on what it sets
Fix: do not depend on the state you set, or calculate the value during render. Also watch for objects and arrays created during render that are used as dependencies.
5. Using an effect for something that is not an effect
Copying props to state, calculating totals in an effect, reacting to clicks in an effect. Fix: Do You Need an Effect?
6. Mutating state
items.push(newItem);
setItems(items); // same reference: no re-render
Fix: always set a new array or object. See Avoiding State Mutation.
7. Reading state right after setting it
setCount(count + 1);
console.log(count); // still the old value
Fix: use the new value you computed, or an updater function. See State as a Snapshot.
8. Ignoring race conditions in fetch effects
An older, slower response overwrites a newer one. Fix: an ignore flag in cleanup or AbortController. See Fetching Data with Effects.
9. Over-memoizing
useMemo and useCallback on everything adds code and bugs and rarely makes anything faster. Fix: measure first, see useMemo.
10. Making an effect async
useEffect(async () => { // wrong: returns a promise, not a cleanup function
const res = await fetch(url);
}, []);
React expects the effect to return nothing or a cleanup function, not a promise. Fix: define an async function inside and call it:
function App() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
let ignore = false;
async function load() {
const res = await fetch("data:application/json,[1,2,3]");
const list = await res.json();
if (!ignore) setCount(list.length);
}
load();
return () => { ignore = true; };
}, []);
return <p>Loaded: {count}</p>;
}
11. Changing the shape of state’s type
Starting with useState() (undefined) and later setting a string makes inputs switch from uncontrolled to controlled. Fix: start with "", 0, [] or null deliberately.
12. Putting hooks in loops or callbacks
items.map((item) => {
const [open, setOpen] = useState(false); // not allowed
return <li>{item}</li>;
});
Fix: extract an Item component; each instance then has its own hook.
function Item({ name }) {
const [open, setOpen] = React.useState(false);
return (
<li>
<button onClick={() => setOpen(!open)}>Toggle {name}</button> {name}{open ? " (open)" : ""}
</li>
);
}
function App() {
return <ul>{["Ada", "Grace"].map((n) => <Item key={n} name={n} />)}</ul>;
}
A debugging routine
- Read the first error message in the console; fix that one first.
- Check the Rules of Hooks: any hook after a condition or
return? - Add
console.logto see which render has the unexpected value. - Check dependency arrays for missing values.
- Use React DevTools to inspect props, state and re-renders. See Debugging React Applications.
Key points
- Most hook bugs are one of: conditional hooks, stale closures, missing cleanup, loops, unnecessary effects, mutation.
- Fix the code instead of silencing the linter.
- Keep it simple: fewer hooks mean fewer bugs.