A Guide to using the useEffect hook in React
A Guide to using the useEffect hook in React<br>Published: 17 January 2026<br>A practical guide to React useEffect, when to avoid it, common pitfalls, and how cleanup works.
useEffect is one of React’s most useful hooks, and also one of the most misused. The basic rule of thumb is to use effects only to synchronize with something outside React. If the logic can run during render, in an event handler, or via memoization, that is usually the better option.<br>The lifecycle behavior of useEffect<br>An effect runs after React commits a render to the DOM.<br>useEffect(() => {<br>// setup<br>return () => {<br>// cleanup<br>};<br>}, [deps]);
The lifecycle is:<br>Initial mount: run setup after the first render is committed.Dependency change: run cleanup for the previous effect, then run setup again.Unmount: run cleanup one final time.Dependency array behavior:<br>No array: runs after every render.Empty array []: runs once after mount, cleanup on unmount.With deps [a, b]: runs after mount and whenever a or b changes.In React Strict Mode (development), React may intentionally run setup and cleanup an extra time to surface side-effect bugs. This is expected in dev and does not happen the same way in production.<br>How the return value works in useEffect<br>useEffect expects either:<br>nothing (undefined), ora cleanup function.Example:<br>useEffect(() => {<br>const id = setInterval(tick, 1000);
return () => {<br>clearInterval(id);<br>};<br>}, []);
Important detail: a function reference is returned (the cleanup function). React calls that function later:<br>before running the effect again when dependencies change, andwhen the component unmounts.Think of it as “setup now, teardown later.”<br>Why useEffect should be used sparingly<br>Effects are imperative. React is primarily declarative. The more effect-heavy a component becomes, the harder it is to reason about ordering, dependencies, and re-renders.<br>Imperative code tells React step by step what to do and when to do it. In contrast, declarative React code describes what UI should look like for a given state. Effects are necessary for external synchronization, but overusing them pulls component logic away from the simpler state-to-UI model.<br>Common issues in effect-heavy components:<br>Stale values captured by closures. For example, an interval may keep reading the count value from the render that created it, instead of the latest value shown on screen.Missing dependencies that cause subtle bugs. For example, a fetch effect that uses userId but omits it from the dependency array may keep loading data for the previous user after props change.Extra renders due to effect-driven state updates. For example, deriving fullName from firstName and lastName in an effect causes React to render once with stale derived state, then again after the effect updates it.Race conditions when async work completes out of order. For example, a slower request for an older search term can finish after a newer request and replace the current results with stale data.Cleanup bugs that leak listeners, timers, or requests. For example, adding a resize listener or starting an interval without cleanup means that work can continue after the component unmounts.In short, effects are not “run code after render” by default. They are for bridging React with external systems.<br>Problematic patterns and better alternatives<br>The examples below are not exhaustive, but they cover several common situations where useEffect is often used out of habit. In these cases, the same logic can usually be expressed more directly during render, in an event handler, or through a clearer state model.<br>1. Deriving state with an effect<br>This is a common anti-pattern:<br>function Product({ price, taxRate }) {<br>const [total, setTotal] = useState(0);
useEffect(() => {<br>setTotal(price * (1 + taxRate));<br>}, [price, taxRate]);
return p>Total: {total}p>;
This causes an extra render because React first renders with the initial total value, commits that render, then runs the effect. The effect calls setTotal, which schedules a second render with the calculated value.<br>It duplicates state because the calculation does not use the existing total state as an input. The effect does not do setTotal(total + something). It does setTotal(price * (1 + taxRate)), which means total is fully recalculated from price and taxRate every time.<br>So the source values are price and taxRate, and total is only a stored result of those values. If price is 10 and taxRate is 0.2, React already has enough information to calculate 12 during render. Putting 12 into state stores a second copy of information that can be recalculated from the first copy.<br>Prefer direct computation:<br>function Product({ price, taxRate }) {<br>const total = price * (1 + taxRate);<br>return p>Total: {total}p>;
If the calculation is expensive, use useMemo, not useEffect.<br>2. Reacting to user events with an effect<br>useEffect(() => {<br>if (submitted) {<br>saveForm(data);<br>}, [submitted, data]);
This can be brittle and indirect.<br>The user...