React
Chapter 07: useEffect & Lifecycle β Side effects, dependency array rules, cleanup functions, memory leaks, and React 18 StrictMode double-mounting.
1. What is a Side Effect?
In React, component render functions should ideally be pure: they take props and state, calculate JSX, and return it without modifying anything outside the component. Any operation that interacts with the outside world is called a Side Effect.
π‘ Real-World Analogy: Chef Cooking vs Delivering Food
Think of your component lifecycle:
- Pure Render Phase (Chef in Kitchen): Preparing the dish on a plate based solely on the recipe ticket (props & state). Zero outside interaction.
- Side Effect (Delivery Driver): Leaving the kitchen to deliver food to the customer's house, making phone calls, or updating inventory logs (fetching data from APIs, setting timers, attaching window resize listeners, modifying
document.title).
2. The 3 Dependency Array Modes
The behavior of useEffect(callback, dependencies) is entirely dictated by its second argumentβthe dependency array:
No Dependency Array
Runs after EVERY render
Executes on initial mount and after every single state/prop change. Rare; often causes infinite loops if you update state inside!
useEffect(() => {
console.log("Rendered!");
});
Empty Array []
Runs ONCE on Mount
Executes only once when the component is first inserted into the DOM. Perfect for initial API calls and global event setup.
useEffect(() => {
fetchInitialData();
}, []);
With Dependencies [a, b]
Runs on Mount + Changes
Executes on mount, and re-executes only if any value in the dependency array changes (checked via Object.is()).
useEffect(() => {
fetchUser(userId);
}, [userId]);
3. The Cleanup Function (Preventing Memory Leaks)
If your effect creates an ongoing subscription, starts an interval/timer, or attaches an event listener to window, you must return a cleanup function. React calls this cleanup function right before the component unmounts, and before re-running the effect on dependency change.
function LiveClock() {
const [seconds, setSeconds] = React.useState(0);
React.useEffect(() => {
// 1. Setup Phase: Start the interval timer
const timerId = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// 2. Cleanup Phase: Executed when component unmounts
return () => {
clearInterval(timerId); // π Prevents orphan timers in memory!
console.log("Timer cleaned up safely!");
};
}, []); // Empty array = mount & unmount lifecycle
return <div>Elapsed: {seconds}s</div>;
}
4. Top Interview Questions & Common Pitfalls
β οΈ Pitfall 1: "Why can't I write useEffect(async () => { ... }) directly?"
The Explanation: An async function automatically returns a Promise. But React expects the return value of useEffect to either be undefined or a cleanup function! If you return a Promise, React cannot clean up the effect.
// β WRONG: Async effect callback
// useEffect(async () => { const res = await fetch(...); }, []);
// β
CORRECT: Declare an inner async function and invoke it immediately:
useEffect(() => {
async function loadData() {
const res = await fetch("/api/users");
const json = await res.json();
setUsers(json);
}
loadData();
}, []);
β οΈ Pitfall 2: "Why does my useEffect run twice on mount in React 18?"
Senior Answer: In React 18 development mode inside <React.StrictMode>, React intentionally mounts, unmounts, and re-mounts your components immediately (Mount $\rightarrow$ Unmount $\rightarrow$ Remount). This stress-tests your code to verify that your cleanup functions are properly implemented and don't leak subscriptions!
5. Hands-On Practice Challenges
Master component lifecycles, dependency diffing, and cleanup handlers with these interactive challenges. Click "Solve Challenge" to open the code editor!
Challenge 1: Replicate useEffect Dependency Diffing
Write a utility simulateEffectRunner(effectFn, deps, prevDeps) that compares current dependencies with previous dependencies using Object.is() and returns whether the effect should run.
- If
prevDeps === null, return{ shouldRun: true, reason: 'INITIAL_MOUNT' }. - Compare elements using
Object.is(deps[i], prevDeps[i]). - If any item differs, return
shouldRun: true. If all match, returnshouldRun: false.
{ shouldRun: true, reason: "INITIAL_MOUNT" } | { shouldRun: false, reason: "DEPS_IDENTICAL" }
simulateEffectRunner to implement dependency equality checks.
Challenge 2: Effect Cleanup & Subscription Lifecycle
Implement a createSubscriptionManager() closure that simulates subscribing to window events and returning a cleanup un-subscriber function to prevent listener accumulation.
- Track
activeListenerscount in closure. subscribe(eventName)increments counter and returns a cleanup callback.- Executing the returned cleanup callback decrements the counter.
Active before cleanup: 2 | Active after unsubA: 1
subscribe.
Challenge 3: useEffect Linter & Memory Leak Detector
Write a linter function checkEffectBestPractices(effectCodeString) that scans effect code and flags direct async callbacks and missing cleanup returns on timers.
- Flag direct async callbacks (e.g.
useEffect(async () => ...)). - Flag missing return cleanup when
setIntervaloraddEventListenerare used. - Return 'β Effect adheres to best practices!' if clean.
Audit Buggy: ["CRITICAL: useEffect callback cannot be async directly.", ...] | Audit Good: "β
Effect adheres to best practices!"
checkEffectBestPractices and test against sample code strings.