Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
Effects run after the browser paints — React lets the DOM update be visible to the user before running side effects. This ordering is intentional: subscriptions, fetches, and logging shouldn't block the frame. The cleanup function runs before the next effect fires and on unmount, which is how you avoid double-subscription bugs. The dependency array doesn't watch values reactively — it's a hint to React about when to re-run, compared shallowly with Object.is. An empty array means 'run once on mount', but in StrictMode React deliberately mounts twice in development to catch cleanup bugs.
useEffect runs after paint. The returned cleanup runs before the next invocation and on unmount. Each run sees a fresh closure with current props and state.
import { useState, useEffect } from 'react';
interface TimerProps { id: number }
export function Timer({ id }: TimerProps) {
const [ticks, setTicks] = useState(0);
useEffect(() => {
console.log(`effect fired for id=${id}`);
const interval = setInterval(() => {
setTicks(t => t + 1);
}, 1000);
// cleanup: runs before next effect AND on unmount
return () => {
console.log(`cleanup for id=${id}`);
clearInterval(interval);
};
}, [id]); // re-runs when id changes
return <div>Timer {id}: {ticks} ticks</div>;
}
// Effect with no cleanup — safe because there is nothing to clean up
export function DocumentTitle({ title }: { title: string }) {
useEffect(() => {
document.title = title;
// no cleanup needed: overwriting the title on the next run is fine
}, [title]);
return null;
}clearInterval from the cleanup and rapidly toggle the id. Open the browser performance tab. You should see multiple intervals firing simultaneously — that's the leak. Re-add the cleanup and confirm only one interval ticks at a time.[id] to []. Confirm in the console that 'effect fired' only logs once regardless of how many times id changes. Then change the array to [id, ticks] — now the effect (and the interval) re-creates on every tick. Measure the CPU cost with the profiler.Use these three in order. Each builds on the one before.
In one paragraph, explain when useEffect's cleanup function runs. Give two concrete examples: one where missing cleanup causes a bug, and one where no cleanup is needed.
Walk me through the lifecycle of a useEffect call: when exactly does it fire relative to render, paint, and the browser layout? How does React determine whether to re-run an effect based on the dependency array?
React StrictMode invokes effects twice in development. This is intentional — what class of bugs does it catch? Give a concrete example of code that passes in production but fails under double-invocation, and explain the fix.