How to Run Code When a Component Mounts in React
Advertisement
🧩 Scenario
Architecture Walkthrough
The Empty Dependency Array
useEffect accepts two arguments: a callback and a dependency array. The dependency array controls when the effect re-runs. When the array is empty ([]), React compares the deps on each render and finds nothing has changed, so the effect runs only after the first render and never again. This is the functional component equivalent of componentDidMount.
Without the dependency array, the effect runs after every render. With a non-empty dependency array, it runs after the first render and after any render where one of the listed values changed. The empty array is the intentional way to say "run once and only once."
Cleanup on Unmount
The callback passed to useEffect can return a cleanup function. When the dependency array is empty, the cleanup function runs once: when the component unmounts. This is the equivalent of componentWillUnmount. Use the cleanup function to cancel timers, remove event listeners, unsubscribe from observables, and abort in-flight requests. Failing to clean up creates memory leaks and stale callbacks that fire after the component is gone.
Async in useEffect
useEffect callbacks must either return a cleanup function or return nothing (undefined). An async function implicitly returns a Promise, which is neither. Passing an async function directly to useEffect causes React to ignore the returned Promise (including any cleanup it contains) and produces a warning. The correct pattern is to define an async function inside the callback and call it immediately.
Key Code Explained
function Dashboard({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Define async logic inside — cannot make the callback itself async
let cancelled = false;
async function loadUser() {
try {
setIsLoading(true);
const data = await fetchUser(userId);
if (!cancelled) {
setUser(data);
}
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
}
loadUser();
// Cleanup: runs when component unmounts
// Prevents setState call on an unmounted component
return () => {
cancelled = true;
};
}, []); // [] = run only on mount
if (isLoading) return <Skeleton />;
return <UserProfile user={user} />;
}
// Event listener example: add on mount, remove on unmount
function KeyboardShortcuts({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 's' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
onSave();
}
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleKeyDown);
// Cleanup: remove the listener on unmount
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, []); // onSave and onClose are intentionally omitted here for a mount-only setup
// In production, you'd add them as deps or use useEffectEvent
return null;
}
// Third-party library initialization
function ChartContainer({ data }: { data: number[] }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
if (!canvasRef.current) return;
// Initialize chart library once on mount
const chart = new Chart(canvasRef.current, {
type: 'line',
data: { datasets: [{ data }] },
});
// Destroy chart on unmount to free memory
return () => {
chart.destroy();
};
}, []); // Mount only — data updates handled separately
return <canvas ref={canvasRef} />;
}
The cancelled flag in the data fetching example is a lightweight alternative to AbortController. When the component unmounts before the fetch resolves, cancelled is set to true by the cleanup function. The if (!cancelled) guards prevent setState from being called on the unmounted component. A more robust approach is to use AbortController: pass signal to fetch, and call controller.abort() in the cleanup function to actually cancel the in-flight HTTP request.
Tradeoffs
| useEffect deps | When it runs | Cleanup fires when |
|---|---|---|
| No array | After every render | Before next effect run |
[] (empty) | After first render only (mount) | On unmount |
[a, b] (non-empty) | After mount + when a or b changes | Before next run + unmount |
What Interviewers Actually Check
- Whether you know the empty dependency array is what makes
useEffectequivalent tocomponentDidMount - Whether you write a cleanup function for anything added on mount
- Whether you know why
useEffectcannot be async directly and how to work around it - Whether you handle stale fetch results when the component unmounts before the request completes
- Whether you know about the React Strict Mode double-invocation in development
Follow-Up Questions
- If
useEffectwith[]only runs once, how do you update the chart from the library initialization example when thedataprop changes? - How does
AbortControllerimprove on thecancelledflag for in-flight fetch cleanup? - In React 19,
use(promise)suspends a component during data loading. How does this change the mount-fetch pattern? - If the cleanup function from a mount effect needs to close over data that was set during the effect, how do you structure that with a
useRef? - What does the React Strict Mode double-mount tell you about your cleanup function if state is different after the second mount than after the first?
Common Candidate Mistakes
- Writing
useEffect(() => { fetchData(); })without the empty array and running the fetch on every render - Subscribing to a WebSocket or adding an event listener on mount without a cleanup function that removes it on unmount
- Marking the
useEffectcallback itself asasyncinstead of defining an async function inside it - Not handling the case where the component unmounts before an async operation completes, leading to state updates on unmounted components
- Removing React Strict Mode to avoid the double-mount in development instead of writing a correct cleanup function
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you write a
useEffectwith an empty dependency array that runs only on mount? - Can you write a cleanup function that correctly tears down resources added on mount?
- Can you explain why the empty dependency array controls when the effect runs?
- Can you handle stale async results using a cancelled flag or
AbortController? - Can you explain what React Strict Mode's double-invocation is testing for?
Summary
useEffect with an empty dependency array runs once after the component's first render and never again. This is the functional component equivalent of componentDidMount. The empty array tells React there are no values to watch for changes, so the effect never needs to re-run.
The useEffect callback can return a cleanup function. When the dependency array is empty, this cleanup function runs once: when the component unmounts. This is equivalent to componentWillUnmount. Failing to provide a cleanup for subscriptions, event listeners, timers, or third-party library instances creates memory leaks and callbacks that fire on unmounted components.
useEffect callbacks cannot be async functions because async functions return Promises, not cleanup functions. Define an async function inside the callback and call it immediately. For data fetching, guard the setState calls with an AbortController signal or a cancelled flag that the cleanup function sets to true, preventing state updates after the component unmounts.
Why does React Strict Mode run mount effects twice in development?
React Strict Mode intentionally mounts, unmounts, and remounts components to help you find effects that do not clean up correctly. The double invocation only happens in development, not in production.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement