Learn the concept
Hooks
A simplified useEffect stores effect callbacks and their dependency arrays in a module-level array. After each render, it compares current dependencies against previous ones using shallow equality and runs the effect callback (plus its cleanup) only when dependencies have changed.
How useEffect Works Conceptually:
Dependency Comparison:
React uses Object.is() to compare each element in the dependency array. This means:
{} is always different from the previous {}undefined deps (no array passed) means the effect runs on every render[] means the effect runs only on mount (no deps to change)How Real React Differs:
requestIdleCallback-like scheduling to avoid blocking the browser's paint cycle.useLayoutEffect runs synchronously before paint — use it when you need to read layout and synchronously re-render (measuring DOM elements, preventing flicker).updateQueue, tagged with flags indicating whether they need to run.The Cleanup Pattern:
Cleanup functions prevent memory leaks and stale subscriptions. Common cleanup patterns:
clearInterval / clearTimeout for timerscontroller.abort() for in-flight fetch requestsunsubscribe() for event listeners and subscriptionscancelled flag for async operationsconst effectStore = []; // Stores { deps, cleanup } for each effect
let effectIndex = 0;
function useEffect(callback, deps) {
const currentIndex = effectIndex;
const prevEffect = effectStore[currentIndex];
// Determine if deps have changed
const hasChanged = !prevEffect || // First render: always run
deps === undefined || // No deps array: run every render
!shallowEqual(deps, prevEffect.deps); // Deps changed
if (hasChanged) {
// Schedule effect to run after render
queueMicrotask(() => {
// Run cleanup from previous effect first
if (prevEffect?.cleanup) {
prevEffect.cleanup();
}
// Run the new effect and store its cleanup
const cleanup = callback();
effectStore[currentIndex] = { deps, cleanup };
});
} else {
// Deps unchanged — preserve existing effect (don't re-run)
effectStore[currentIndex] = prevEffect;
}
effectIndex++;
}
// Shallow comparison for dependency arrays
function shallowEqual(a, b) {
if (a === b) return true;
if (!a || !b || a.length !== b.length) return false;
return a.every((val, i) => Object.is(val, b[i]));
}Understanding useEffect internals helps write correct cleanup patterns for WebSocket connections, event listeners, and real-time data subscriptions.
Knowing how shallow comparison works helps diagnose infinite effect loops caused by objects or arrays in the dependency array.
Build a tool that shows which dependencies changed between renders and why an effect re-ran.
Create a visual comparison showing the timing difference between useEffect (after paint) and useLayoutEffect (before paint).
React's useEffect implementation is one of the most complex parts of the reconciler, with over 2,000 lines handling scheduling, priority, batching, and Suspense integration.