Effects: Synchronizing with the Outside World
useEffect exists to synchronize a component with something outside React — not as a general-purpose "run this after render" hook. The dependency array is the exact algorithm behind when it re-runs.
What you'll learn
- Explain what kind of work belongs in an effect versus in an event handler
- Implement the shallow-equality check that decides whether an effect re-runs
- Explain when and why an effect needs a cleanup function
Prerequisites
Explanation
useEffect is not a generic "run some code after render" escape hatch — it exists specifically to synchronize a component with a system outside React's own rendering: a browser API (setting document.title, subscribing to window.resize), a network request, a WebSocket connection, a third-party widget. If the code you're writing doesn't reach outside React — it's just deriving one value from another, or responding to a user action — it almost certainly belongs directly in the render body (for derived values) or an event handler (for user-triggered work), not an effect. This distinction avoids a huge share of effect-related bugs, because most of them come from using an effect where an event handler or a plain derived value would have been simpler and correct.
The dependency array is a literal algorithm, not a hint. After every render, React compares each value in the new dependency array to the corresponding value from the previous render, using the same kind of shallow Object.is-style comparison you could implement yourself. If every dependency is unchanged, the effect is skipped entirely; if even one changed, the effect re-runs. An empty array ([]) means "no dependencies, so nothing can ever differ — run once, after the first render, and never again." Omitting the array entirely means "run after every single render," rarely what's actually intended.
Cleanup exists because "starting" something and "stopping" it are a pair, not a one-time action. A subscription needs to unsubscribe; an interval needs to be cleared; an in-flight request's result needs to be ignored if the component using it is no longer around to receive it. The function an effect optionally returns runs before the effect re-runs (cleaning up the previous run's setup) and once more when the component unmounts entirely — the same cleanup function handles both cases, because they're the same underlying situation: "this effect's setup is no longer valid, undo it before doing anything else."
Example
A real implementation of the shallow comparison React's dependency-array check is conceptually built on.
function dependenciesChanged(prevDeps, nextDeps) {
if (prevDeps === null) return true; // first render: always "changed"
if (prevDeps.length !== nextDeps.length) return true;
return prevDeps.some((dep, i) => !Object.is(dep, nextDeps[i]));
}
console.log(dependenciesChanged(null, [1, "a"])); // true -- first render
console.log(dependenciesChanged([1, "a"], [1, "a"])); // false -- nothing changed, skip the effect
console.log(dependenciesChanged([1, "a"], [2, "a"])); // true -- first dependency changedTry it yourself
Try comparing two arrays containing objects with the same shape but different references -- what happens, and why?
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Guided exercise
Guided exercise
Using dependenciesChanged already defined, determine whether an effect with dependency array [userId, sortOrder] would re-run given prevDeps = [42, 'asc'] and nextDeps = [42, 'desc']. Store the result in willRerun.
Checks: correctly determines the effect re-runs
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Independent exercise
Independent exercise
Model an effect's setup/cleanup pair as an object. Write createSubscriptionEffect() returning { isActive: boolean, start(), stop() } where start() sets isActive to true, and stop() (the cleanup) sets isActive to false. Then write runEffectCycle(effect) that calls start(), then immediately calls stop() (simulating a re-run or unmount), and returns the final isActive value.
Checks: starts inactive · cleanup correctly deactivates after a start/stop cycle
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Common mistakes
- Using an effect to compute a value that could be calculated directly during render instead, adding an unnecessary extra render cycle.
- Omitting the dependency array entirely, causing the effect to run after every single render instead of only when something relevant changed.
- Starting a subscription, timer, or request in an effect without returning a cleanup function, leaking it every time the effect re-runs or the component unmounts.
Knowledge check
Takeaway
useEffect synchronizes with the outside world, not a general after-render hook — the dependency array is a real shallow-comparison algorithm, and cleanup exists because starting and stopping something are always a pair.
Summary
This lesson implemented the shallow-comparison algorithm behind the dependency array and explained why effects need cleanup functions, distinguishing effect-appropriate work from what belongs in render or an event handler.
References
Your notes
Notes save automatically.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.