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.
| Hook | Runs relative to paint | Best for |
|---|---|---|
useEffect | After paint | Fetch, subscriptions, analytics, focus |
useLayoutEffect | Before paint | Measure-then-mutate, tooltip positioning |
useInsertionEffect | Before layout effects | CSS-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 → useEffectCleanup 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:
useEffectis 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 → removeEventListenerEffects 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 againStrict 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.iscompare. 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 paintPractical 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