Closures & Lexical Scope — Why Stale Closures Break React

Mar 08, 2026Venkata Lokesh P
JavaScriptReactClosuresReact Hooks

Introduction

When working with React Hooks, sometimes you may see strange bugs:

  • State does not update correctly
  • A function prints an old value
  • useEffect behaves unexpectedly

Most of the time, this happens because of stale closures.

To understand this problem, we need to understand two basic JavaScript ideas:

  • Lexical Scope
  • Closures

These are core JavaScript fundamentals, but they become very important in React.


1. What is Lexical Scope?

Lexical scope means:

A function can access variables from the place where it was created.

JavaScript decides variable access based on where the code is written.

Example

JAVASCRIPT
function outer() {
  const message = "Hello from outer function";

  function inner() {
    console.log(message);
  }

  inner();
}

outer();

Why this works

  • inner() is created inside outer()
  • Because of lexical scope, it can access message

Even though message is not inside inner(), JavaScript still allows it.


2. What is a Closure?

A closure happens when a function remembers variables from its outer scope, even after the outer function finishes.

Example

JAVASCRIPT
function createCounter() {
  let count = 0;

  return function () {
    count++;
    console.log(count);
  };
}

const counter = createCounter();

counter(); // 1
counter(); // 2
counter(); // 3

Why this works

The returned function remembers the variable count.

Even though createCounter() finished running, the function still has access to it.

Closures are used in many places:

  • React
  • Event handlers
  • Timers
  • Memoization

Closures are powerful, but they can also cause bugs.


3. What is a Stale Closure?

A stale closure happens when a function captures an old value of a variable.

The variable changes later, but the function still uses the old value.


4. How This Breaks React

React components re-render many times.

Each render creates new variables and functions.

If a function captures values from an old render, it may use outdated state.

Example

JAVASCRIPT
import { useState, useEffect } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      console.log(count);
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

Expected output

CODE
0
1
2
3

Actual output

CODE
0
0
0
0

Why this happens

  • useEffect runs only once
  • The closure captures the initial value of count
  • The interval keeps using that old value

This is called a stale closure.


5. Fixing Stale Closures

1. Add dependencies

JAVASCRIPT
useEffect(() => {
  const interval = setInterval(() => {
    console.log(count);
  }, 1000);

  return () => clearInterval(interval);
}, [count]);

Now React recreates the effect when count changes.


2. Use functional updates

JAVASCRIPT
setCount(prev => prev + 1);

This always uses the latest state.


3. Use useRef

JAVASCRIPT
const countRef = useRef(count);

useEffect(() => {
  countRef.current = count;
}, [count]);

Refs help store values that stay updated across renders.


6. Where Stale Closures Commonly Appear

You will usually see stale closures in:

  • setInterval and setTimeout
  • Event listeners
  • useEffect
  • useCallback
  • Async functions

7. Simple Mental Model

A good way to think about React:

Every render is like a snapshot in time.

Functions created during that render remember the state from that snapshot.

If they run later, they may still use that old snapshot.


Conclusion

Closures and lexical scope are basic JavaScript concepts, but they can cause confusing bugs in React.

Understanding them helps you:

  • Avoid stale closure bugs
  • Write better React code
  • Debug state problems faster

Once this concept clicks, many React bugs suddenly become much easier to understand.

React is not broken — it's just JavaScript closures working as designed.


Happy coding! 🚀