useEffect: The Right Mental Model
โฑ๏ธ ~4-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.๐ useEffect = the "keep in sync" mechanism. When the external world changes, useEffect keeps your component synchronized. Cleanup = disconnecting when you leave.
Wrong mental model
"Run this code on mount, run this other code on update, run cleanup on unmount." โ This is the class component lifecycle model. It causes bugs in function components.
Correct mental model
"Keep this component synchronized with [deps]. When deps change, re-sync. When unmounting, disconnect." โ Effects are declarative synchronization, not imperative lifecycle hooks.
The Effect vs Event distinction
React is moving toward a formal distinction: Effects (synchronization with external systems โ useEffect) vs Events (user-initiated actions โ event handlers). Effects should be driven by reactive values (props/state). Events are caused by user actions and should not be reactive. The proposed useEffectEvent hook makes this distinction explicit.
Interactive Sandbox
โ Move something, see it react instantly.Pattern
| 1 | // โ Lifecycle thinking โ "run on mount" |
| 2 | useEffect(() => { |
| 3 | analytics.track('page_view'); |
| 4 | fetchUser(userId); |
| 5 | }, []); // empty deps = "componentDidMount" |
| 6 | ย |
| 7 | // Problem: if userId changes, effect never re-runs |
| 8 | // You've created a stale closure bug |
Explanation
useEffect is not componentDidMount. It is a synchronization primitive. The question is not 'when do I run this?' but 'with what external system do I need to stay in sync, and which values drive that synchronization?'
Challenge
Toggle between bad and good code for all 5 patterns. Understand what each bug looks like.
Why Should I Care?
โ The exact interview question + the bug it kills.Interview questions
Q: Why is useEffect not "componentDidMount"?
componentDidMount runs once, period. useEffect with `[]` also runs once after mount โ but the mental model differs. useEffect is declarative: "synchronize with nothing that changes." If you ever add a reactive value to the effect body without adding it to deps, you've broken the contract and created a stale closure. componentDidMount had no such contract โ it always ran once by design.
Q: What causes the React useEffect infinite loop?
| 1 | // โ Infinite loop โ data is a new array each render |
| 2 | useEffect(() => { |
| 3 | processData(data); |
| 4 | setResult(transform(data)); // triggers re-render |
| 5 | }, [data]); // data = [] created in render = new ref every time |
| 6 | // โ effect runs โ setState โ render โ new [] โ effect runs โ ... |
| 7 | ย |
| 8 | // โ Fix 1: stabilize data with useMemo |
| 9 | const data = useMemo(() => [/* ... */], [deps]); |
| 10 | ย |
| 11 | // โ Fix 2: move the object outside the component |
| 12 | const STATIC_DATA = [/* ... */]; |
Q: Why does React 18 run effects twice in development?
React 18 Strict Mode mounts, unmounts, then remounts every component in development. This simulates a future React feature where components can be taken off screen and restored (e.g., React's upcoming offscreen API). If your effects aren't resilient to this pattern โ i.e., they don't clean up properly โ Strict Mode exposes the bug before it reaches production.
The Deep Dive
โ Spec refs, engine internals, the minutiae.Fetching in Suspense โ the React 19 approach
React 19 introduces `use(promise)` which integrates with Suspense. Async Server Components fetch data directly without useEffect. For client-side, TanStack Query's Suspense mode integrates cleanly. The days of `useEffect(() => fetch(...).then(setData), [id])` are numbered for anything beyond toy examples.
| 1 | // React 19 โ use() reads a promise in render |
| 2 | function UserProfile({ userPromise }) { |
| 3 | const user = use(userPromise); // suspends until resolved |
| 4 | return <div>{user.name}</div>; |
| 5 | } |
| 6 | ย |
| 7 | // Async Server Component โ no useEffect at all |
| 8 | async function Page({ id }) { |
| 9 | const user = await fetchUser(id); // server-side, no hook |
| 10 | return <UserProfile user={user} />; |
| 11 | } |
useEffectEvent proposal
The upcoming useEffectEvent hook extracts the "event-like" code from effects โ code that should see the latest props/state but should NOT re-run the effect when those values change. This eliminates the "I need this value in my effect but don't want it in deps" antipattern (using a ref as a workaround).
| 1 | // Without useEffectEvent โ must include onVisit in deps |
| 2 | // โ re-creates the subscription whenever onVisit changes |
| 3 | useEffect(() => { |
| 4 | const id = setInterval(() => { |
| 5 | onVisit(url); // always-fresh function, shouldn't be reactive |
| 6 | }, 1000); |
| 7 | return () => clearInterval(id); |
| 8 | }, [url, onVisit]); // onVisit as dep causes extra re-subscriptions |
| 9 | ย |
| 10 | // With useEffectEvent (proposed) |
| 11 | const onVisitEvent = useEffectEvent(onVisit); |
| 12 | useEffect(() => { |
| 13 | const id = setInterval(() => { |
| 14 | onVisitEvent(url); // always fresh, not reactive |
| 15 | }, 1000); |
| 16 | return () => clearInterval(id); |
| 17 | }, [url]); // clean deps |
Interview Questions
โ Real questions from real interviews โ with answers.Synchronization: 'keep this component in sync with [deps]', not 'run on mount/unmount'.
Object/array literals in deps create new references every render, triggering the effect endlessly.
To surface missing cleanup by simulating mount โ unmount โ remount before production ships.
Use AbortController: abort the in-flight request in the cleanup function so stale responses are ignored.
When you need to read or write the DOM before the browser paints โ to prevent a visual flash.
It extracts 'event-like' code from effects โ code that reads latest values but shouldn't be a reactive dep.
Memory Game
โ Quick quiz โ lock the concept in long-term memory.In React 19, the `use()` hook can be called inside render. What does this change about data fetching?