On this page

Closures & Lexical Scope

3 min read

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

js
1function makeCounter() {
2 let count = 0;
3 return function () {
4 count++;
5 return count;
6 };
7}
8
9const next = makeCounter();
10next(); // 1
11next(); // 2
12next(); // 3

makeCounter 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:

js
1const a = makeCounter();
2const b = makeCounter();
3a(); a(); // 2
4b(); // 1 (separate count)

What Closures Are Used For

  • Private data. count above 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.
js
1const multiplyBy = (n) => (x) => x * n;
2const double = multiplyBy(2);
3double(5); // 10

The Classic Loop Interview Question

js
1for (var i = 0; i < 3; i++) {
2 setTimeout(() => console.log(i), 0);
3}
4// 3 3 3

All three callbacks close over the same i. By the time they run, it is 3. Fixes:

js
1for (let i = 0; i < 3; i++) { ... } // let makes one i per round
2for (var i = 0; i < 3; i++) {
3 ((j) => setTimeout(() => console.log(j), 0))(i); // old trick: copy into a parameter
4}

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.

jsx
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 count
10}

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:

jsx
1// 1. Add the dependency so the effect re-runs with fresh values
2useEffect(() => { ... }, [count]);
3
4// 2. Use the functional updater so you never read the old value
5setCount((c) => c + 1);
6
7// 3. Keep the latest value in a ref
8const 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 written
2Closure → function + the outer variables it remembers
3Each call of the outer function → a new, separate backpack
4Uses → private state, factories, callbacks, debounce, memoization
5var in loops → all callbacks share one variable
6Stale closure in React → an effect or handler holds values from an old render
7Fixes → correct deps, functional updater, ref

Closures also explain a related question: what does this point to inside a function? That is next.

Next Topicthis, call, apply & bind