Hint
useEffect cleanup runs BEFORE the next effect — not when the component unmounts (unless deps=[]). Each dep change: cleanup previous → run new effect.
const log = [];
function simulateEffect(deps, effectFn) {
let prevDeps = null;
let cleanup = null;
return function run(newDeps) {
const changed = !prevDeps || newDeps.some((d, i) => d !== prevDeps[i]);
if (changed) {
if (cleanup) cleanup(); // run previous cleanup
cleanup = effectFn() || null; // run new effect, store cleanup
prevDeps = newDeps;
}
};
}
const run = simulateEffect([], () => {
log.push('effect');
return () => log.push('cleanup');
});
run([1]);
run([1]); // no change — nothing runs
run([2]); // cleanup old, run new
run([3]); // cleanup old, run new
console.log(log.join(','));effect,cleanup,effect,cleanup,effect
Explanation: Run [1]: effect. Run [1] again: no change. Run [2]: cleanup from [1] fires, new effect fires. Run [3]: cleanup from [2] fires, new effect fires.
Key Insight: useEffect cleanup runs BEFORE the next effect — not when the component unmounts (unless deps=[]). Each dep change: cleanup previous → run new effect.