intermediate20 min

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 changed

Try 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.

Loading editor…

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.

Loading editor…

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.

Loading editor…

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

Knowledge check

1. What kind of work does useEffect exist for?
2. What does an empty dependency array (`[]`) mean for an effect?
3. Why does an effect's cleanup function run before the effect re-runs, not just on unmount?

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.