๐Ÿณ IntuitiveFE
Login
โ† All concepts

useEffect: The Right Mental Model

โฑ๏ธ ~4-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

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

Sync, Not Lifecycle โ€” common mistake
js
1// โœ— Lifecycle thinking โ€” "run on mount"
2useEffect(() => {
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?'

Gotcha: Empty dependency array `[]` means 'sync with nothing external that changes' โ€” not 'run once'. If your effect references any reactive value (props, state), it should be in the deps array.
Insight: Think of each useEffect as a contract: 'whenever [deps] change, set up this synchronization. When [deps] change again or the component unmounts, tear it down.' That's the full mental model.
Explored:๐Ÿ”„๐Ÿงน๐Ÿ“‹โšก๐ŸŒ
๐ŸŽฏ

Challenge

Toggle between bad and good code for all 5 patterns. Understand what each bug looks like.

Try it
๐ŸŽฏ

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?

js
1// โœ— Infinite loop โ€” data is a new array each render
2useEffect(() => {
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
9const data = useMemo(() => [/* ... */], [deps]);
10ย 
11// โœ“ Fix 2: move the object outside the component
12const 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.

js
1// React 19 โ€” use() reads a promise in render
2function 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
8async 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).

js
1// Without useEffectEvent โ€” must include onVisit in deps
2// โ†’ re-creates the subscription whenever onVisit changes
3useEffect(() => {
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)
11const onVisitEvent = useEffectEvent(onVisit);
12useEffect(() => {
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.
1/4

In React 19, the `use()` hook can be called inside render. What does this change about data fetching?