Closures & Lexical Scope
Closures are the single most asked JavaScript topic in React interviews. Hooks, event handlers and stale values are all closures in disguise.
Definition
Lexical scope means a function can see the variables from the place where it was written, not where it is called.
A closure is a function together with the variables it remembers from its surrounding scope. Even after the outer function has finished, the inner function still holds on to those variables.
Analogy: a backpack. When a function is created, it packs the outer variables it needs into a backpack. Wherever the function travels later, the backpack goes with it.
How It Works
1function makeCounter() {2 let count = 0;3 return function () {4 count++;5 return count;6 };7}8 9const next = makeCounter();10next(); // 111next(); // 212next(); // 3makeCounter finished running long ago. Normally its count would be gone. But the returned function still uses count, so JavaScript keeps it alive. Nobody outside can touch count directly. Only next can.
Each call to makeCounter creates a new backpack:
1const a = makeCounter();2const b = makeCounter();3a(); a(); // 24b(); // 1 (separate count)What Closures Are Used For
- Private data.
countabove is hidden. This is how modules and factories keep state safe. - Function factories. Create specialized functions from one template.
- Callbacks and event handlers. A handler remembers the variables around it.
- Memoization, debounce, throttle. All of them keep a cache or timer in a closure.
1const multiplyBy = (n) => (x) => x * n;2const double = multiplyBy(2);3double(5); // 10The Classic Loop Interview Question
1for (var i = 0; i < 3; i++) {2 setTimeout(() => console.log(i), 0);3}4// 3 3 3All three callbacks close over the same i. By the time they run, it is 3. Fixes:
1for (let i = 0; i < 3; i++) { ... } // let makes one i per round2for (var i = 0; i < 3; i++) {3 ((j) => setTimeout(() => console.log(j), 0))(i); // old trick: copy into a parameter4}Closures in React: Stale Closures
Every render of a component is a new function call, with its own variables. Event handlers and effects close over the values from the render that created them.
1function Counter() {2 const [count, setCount] = useState(0);3 4 useEffect(() => {5 const id = setInterval(() => {6 console.log(count); // always 0 ✗7 }, 1000);8 return () => clearInterval(id);9 }, []); // empty deps: this callback only sees the first render's count10}Here is what breaks. The effect ran once, during render 1, where count was 0. The interval callback keeps that old backpack forever. This is called a stale closure.
Fixes:
1// 1. Add the dependency so the effect re-runs with fresh values2useEffect(() => { ... }, [count]);3 4// 2. Use the functional updater so you never read the old value5setCount((c) => c + 1);6 7// 3. Keep the latest value in a ref8const countRef = useRef(count);9countRef.current = count;Memory Note
A closure keeps its variables alive as long as the function is reachable. A closure that holds a huge array and is never released can cause a memory leak. Clean up listeners and timers so the closure can be garbage collected.
Summary
1Lexical scope → a function sees variables from where it was written2Closure → function + the outer variables it remembers3Each call of the outer function → a new, separate backpack4Uses → private state, factories, callbacks, debounce, memoization5var in loops → all callbacks share one variable6Stale closure in React → an effect or handler holds values from an old render7Fixes → correct deps, functional updater, refClosures also explain a related question: what does this point to inside a function? That is next.