Topics
v5.0.0
On this page · sections

Effects & Side Effects

Effect hooks bridge declarative React UI to external systems — but they run at different points in the commit lifecycle and carry dependency, cleanup, and concurrency pitfalls.

HookRuns relative to paintBest for
useEffectAfter paintFetch, subscriptions, analytics, focus
useLayoutEffectBefore paintMeasure-then-mutate, tooltip positioning
useInsertionEffectBefore layout effectsCSS-in-JS style injection (library authors)

useEffect

Definition

useEffect schedules passive side effects after the browser paints. The user sees committed UI before subscriptions or fetches run.

'use client'; import { useEffect, useState } from 'react'; function UserBadge({ userId }: { userId: string }) { const [name, setName] = useState<string | null>(null); useEffect(() => { const controller = new AbortController(); fetch(`/api/users/${userId}`, { signal: controller.signal }) .then((r) => r.json()) .then((data) => setName(data.name)) .catch(() => {}); return () => controller.abort(); }, [userId]); return <span>{name ?? '…'}</span>; }

Timeline

Render → commit DOM → paint → useEffect

Cleanup from the previous effect runs before the next effect body and on unmount. Effects do not run during SSR — only in the browser after hydration commit.

Interview: useEffect is for synchronizing with external systems, not for deriving data from props/state.

useLayoutEffect

Definition

useLayoutEffect runs synchronously after DOM mutations and before paint. Use it when visual adjustment must happen before the user sees a frame.

'use client'; import { useLayoutEffect, useRef, useState } from 'react'; function Tooltip({ anchorRef, children }: { anchorRef: React.RefObject<HTMLElement | null>; children: React.ReactNode; }) { const tipRef = useRef<HTMLDivElement>(null); const [style, setStyle] = useState<React.CSSProperties>({}); useLayoutEffect(() => { const anchor = anchorRef.current; const tip = tipRef.current; if (!anchor || !tip) return; const rect = anchor.getBoundingClientRect(); setStyle({ position: 'fixed', top: rect.bottom + 8, left: rect.left, }); }, [anchorRef, children]); return ( <div ref={tipRef} role="tooltip" style={style}> {children} </div> ); }

With useEffect for positioning, the user may briefly see the tooltip at (0, 0). Layout effects block paint — reserve them for flicker fixes, not general DOM work.

SSR warns on useLayoutEffect — server HTML will not match client if layout effect mutates DOM before hydration paint.

Interview: useLayoutEffect = before paint. useEffect = after paint. Default to passive effects.

Dependency Arrays

Definition

React compares deps with Object.is each commit. A change triggers cleanup of the old effect, then runs the new one.

Omit array → effect every commit — almost never intended Empty [] → mount and unmount only (in production) Include every reactive value the effect reads
// BUG — roomId frozen at mount value useEffect(() => { subscribe(roomId, handler); }, []);

Stable setters from useState need not be listed. ref.current does not belong in deps — changing .current does not re-fire the effect.

In React 19, useEffectEvent gives a stable function reading latest props/state without listing them in effect deps.

Interview: Stale closures are the most common effect bug. Fix the dependency model — do not disable ESLint without a comment.

Stale Closures

Definition

An effect closure captures values from the render that scheduled it. If deps are wrong, the effect reads stale props or state.

'use client'; import { useEffect, useState } from 'react'; function Chat({ roomId }: { roomId: string }) { const [messages, setMessages] = useState<string[]>([]); useEffect(() => { const socket = connect(roomId); socket.on('message', (msg) => { setMessages((prev) => [...prev, msg]); // functional update — safe }); return () => socket.disconnect(); }, [roomId]); // roomId must be listed return <ul>{messages.map((m) => <li key={m}>{m}</li>)}</ul>; }

Functional setState avoids stale state reads. Event handlers and useEffectEvent avoid stale prop reads without bloating dependency arrays.

Race Conditions

Definition

Without cleanup, a slow response from an old request can overwrite current state.

useEffect(() => { const controller = new AbortController(); fetch(url, { signal: controller.signal }) .then((r) => r.json()) .then(setData) .catch(() => {}); return () => controller.abort(); }, [url]);
Fetch → AbortController Subscription → unsubscribe Timer → clearInterval / clearTimeout Event listener → removeEventListener

Effects may run for renders that never commit under concurrent rendering — cleanup must be correct.

Interview: Strict Mode double-invokes effects in dev to surface missing cleanup. Do not use a ref guard to skip the second run.

Strict Mode Double Invocation

Definition

In development, React Strict Mode runs: mount → cleanup → remount for every effect. Production runs a single mount cycle.

Dev Strict Mode ↓ Mount → run effect ↓ Simulated unmount → cleanup ↓ Remount → run effect again

Strict Mode also double-invokes render in development to catch impure render logic — mutation of props, module scope writes, side effects during render. State is preserved across Strict Mode remount in React 18+.

Interview: Do not disable Strict Mode. Write idempotent cleanup instead.

What Belongs Where

Definition

Render / useMemo → Derived data computable from props and state Event handler → User gestures: validate, POST, toggle useEffect → Synchronize with external systems only → network, subscriptions, DOM outside React, widgets key reset → Entity switch — cleaner than useEffect(() => setState(prop), [prop])

Prop-to-state anti-pattern

// Anti-pattern function Editor({ initialText }: { initialText: string }) { const [text, setText] = useState(initialText); useEffect(() => { setText(initialText); }, [initialText]); } // Prefer <Editor key={docId} initialText={initialText} />

Data fetching in modern apps belongs in React Server Components, React Query, or SWR — not raw useEffect fetch when a better layer exists.

Interview: “You Might Not Need an Effect” — compute in render, react in handlers, sync in effects.

Cleanup Contract

Definition

Every effect that opens a resource should close it in cleanup:

useEffect(() => { function onResize() { /* ... */ } window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []);

Cleanup runs before the next effect invocation with updated deps and on unmount. Missing cleanup causes memory leaks, duplicate subscriptions, and race-condition bugs.

Latest callback without resetting the effect

const callbackRef = useRef(onTick); callbackRef.current = onTick; useEffect(() => { const id = setInterval(() => callbackRef.current(), 1000); return () => clearInterval(id); }, []);

Keeps the interval stable while always calling the latest callback.

Senior Interview Summary

useEffect

After paint. Passive side effects: fetch, subscriptions, analytics.

useLayoutEffect

Before paint. Measure-then-mutate. Blocks the frame.

Cleanup

Before next run and on unmount. Abort fetches, clear timers, unsubscribe.

Dependency array

Object.is compare. Include every reactive read. Refs do not belong.

Stale closures

Wrong deps freeze values. Fix deps, use functional setState, or useEffectEvent.

Race conditions

AbortController on fetch. Cleanup before next run prevents old responses overwriting state.

Strict Mode

Dev mount/unmount/remount tests cleanup. Production single cycle.

Best mental model

render → compute what to show event handler → react to user action useLayoutEffect → fix DOM before user sees it useEffect → sync with the outside world after paint

Practical rule

Filter from props → render or useMemo Submit form → onSubmit handler Fetch on mount → effect with AbortController (or RSC / React Query) Measure tooltip position → useLayoutEffect Sync prop into local state → key={entityId}, not effect

Last updated on