Chapter 38: Lifecycle of Reactive Effects
Core Idea
An Effect has its own lifecycle, separate from the component's — think of it as "start synchronizing" / "stop synchronizing," a cycle that repeats every time a value it depends on changes, rather than trying to reason about it in terms of component mount/update/unmount.
Key Concepts
- Think per-render, not per-component. Each render "sees" its own version of props/state and thus its own version of the Effect's dependency values; the Effect from that render synchronizes for that specific set of values, then hands off to the next render's Effect. This differs from class-component lifecycle thinking, where
componentDidMount/componentDidUpdate felt like a single continuous timeline.
- Every reactive value read inside the Effect must be a dependency. A reactive value is anything that can change between renders — props, state, and anything computed from them. The dependency array isn't something you "choose" for timing purposes; it's mechanically determined by what the Effect body actually reads. Lying about it (omitting a value the body uses) is the source of most stale-closure bugs.
- Each render's Effect is independent — the cleanup from render N's Effect runs before render N+1's Effect body, forming a start→stop→start→stop chain across time, one link per change to a dependency.
- Effects "react" to reactive values: this is why the dependency array can't be picked freely — an Effect that reads
roomId and theme but only lists [roomId] will keep using a stale theme from whichever render it last actually ran in, because the closure captured that render's theme value.
- Non-reactive values don't need to be dependencies: a value declared outside the component (a true module-level constant) or something guaranteed never to change across renders doesn't need to appear in the array — but props, state, and anything derived from them generally do.
Code Examples
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // every reactive value the body reads, listed
- What it demonstrates: the dependency array mechanically mirrors the reactive values used inside —
serverUrl and roomId are both listed because the Effect body reads both.
Key Takeaways
- Stop thinking "when does this run relative to mount/update" and start thinking "what values does this Effect need to stay synchronized with" — the dependency array follows directly from that.
- An Effect + its cleanup form one repeating unit (start/stop), not two separate lifecycle hooks bolted together.
- A linter (the ESLint plugin's
exhaustive-deps rule, Ch 47) can mechanically verify the dependency array matches what the Effect body reads — treat its warnings as correctness signals, not suggestions to suppress.
Connects To
- Ch 36 (Synchronizing with Effects): the foundational start/cleanup shape this chapter explains the timing of.
- Ch 39 (Separating Events from Effects): what to do with logic inside an Effect that should not be reactive to every dependency.
- Ch 40 (Removing Effect Dependencies): practical techniques for when a dependency is technically required but causes unwanted re-runs.