What is a closure?
In Javascript, a closure is created anytime a function is created. When a function is created, it captures the references (memory address) to the variables available in its surrounding environment (its lexical scope). If this outer function returns an inner function, and the inner function uses some of those captured variables, those variables are kept alive (they don't get cleared from memory) and are bundled with the returned inner function.
This means the returned inner function can later be executed and still access and modify those preserved variables, even though the original outer function call is long finished.
Closure captures the memory address of a variable, not its value.
// 1. Outer function that defines a variable and returns an inner function
function createCounter() {
// 'count' is the variable in the outer function's scope (lexical scope).
let count = 0;
// The inner function 'increment' is created here.
// It CLOSES OVER (captures the memory address of) the 'count' variable.
return function increment() {
// The inner function accesses and modifies 'count'.
count = count + 1;
console.log(count);
};
}
// 2. The outer function is called.
// It returns the inner function ('increment'), which is saved in 'counter1'.
// The execution of createCounter() is now "finished."
const counter1 = createCounter();
// At this point, the variable 'count' (value 0) should theoretically be gone,
// but the closure keeps it preserved and bundled with 'counter1'.
counter1(); // Output: 1
counter1(); // Output: 2
counter1(); // Output: 3
Where is closure used in React?
Pretty much everywhere. In React, every functional component is essentially a JavaScript function. When you define another function inside your component (like an event handler or a utility function), that inner function automatically creates a closure.
This closure allows the inner function to "remember" and access the variables from its parent component's scope, even when the component has finished rendering.
import React, { useState } from 'react';
// The Functional Component 'PersistentCounter'
function PersistentCounter() {
const [count, setCount] = useState(0);
// 1. Define the handler function in the component body.
// This function is the event handler.
const handleIncrement = () => {
// This logic accesses and modifies the 'count' state.
// This function, 'handleIncrement', forms a CLOSURE over 'count' and 'setCount'.
setCount(count + 1);
};
// 2. Return the JSX, referencing the defined handler.
return (
<div>
<h3>Defined Click Handler</h3>
<p>Current Count: **{count}**</p>
{/* 3. Pass the function reference (handleIncrement) to the onClick prop. */}
<button
onClick={handleIncrement}
>
Increment (Using Defined Handler)
</button>
</div>
);
}
export default PersistentCounter;
Stale closure in React
A closure becomes stale or outdated when the variables (memory address) that the closure uses are changed for some reason. So the closure uses the old data from old memory address and causes bug.
For example, in the code above, if the state count changes for some reason but the component doesn't re-render and therefore handleIncrement is not re-created, then this will cause a stale closure. The setCount call inside handleIncrement will then reference an outdated count value (memory address).
This example is purely hypothetical. In reality, this never occurs: count can only be changed via setCount and when it gets changed, the component will always re-render and handleIncrement function (aka closure) will always be re-created with fresh value of count.
So when does a stale closure occur in React?
It happens when we break React’s default rendering behavior. We know that whenever any state changes, React re-runs the component function, re-creating all variables and functions within it. However, sometimes we prevent this from happening for certain functions or variables by using useCallback and useMemo. Even useEffect can sometimes lead to stale closure.
useEffect stale closure:
function useEffectStaleClosure() {
const [count, setCount] = useState(0);
// ⚠️ BUG: This effect only runs ONCE (when the component mounts).
// The 'tick' function it creates CLOSES over the initial count (which is 0).
// It will forever try to set the count from 0 to 1, then stop.
useEffect(() => {
const interval = setInterval(() => {
// ❌ Stale closure here! The 'count' value used here is always 0.
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []); // Empty dependency array means this only runs once.
return <p>Count: {count}</p>;
}
useMemo stale closure:
function useMemoStaleClosure({ propValue }) {
const [count, setCount] = useState(0);
// ❌ Stale Closure: This calculation function only "remembers"
// the initial 'count' value of 0.
const expensiveResult = useMemo(() => {
// This closure captures 'count' from the first render.
return computeValue(count + propValue);
}, [propValue]); // Only depends on propValue, NOT 'count'
// ... (rest of the component)
}
So, it is clear that stale closures in React most often occur with hooks like (useMemo,useCallback,useEffect) which only re-run when their dependencies change. If we forget to include certain variables in a hook’s dependency array, those variables may become stale inside the closure created by that hook.
How to handle stale closure in React?
Very simple: always include all state variables, props, or functions that are accessed within those hooks in their dependency arrays. So that whenever the states get changed, the hooks re-run and therefore create new closures with fresh values.
function useMemoFixedClosure({ propValue }) {
const [count, setCount] = useState(0);
// ✅ Correct Closure: The dependency array now includes 'count'.
const expensiveResult = useMemo(() => {
// This closure captures the LATEST 'count' value.
return computeValue(count + propValue);
}, [count, propValue]); // 👈 DEPENDENCY ARRAY IS CORRECT
// ... (rest of the component)
}
This works most of the time except when it doesn't. In some scenarios, albeit rare, recreating the function (closure) every time the data changes might not be a viable option. Especially if that function is used as a dependency of some expensive useMemo or as a prop of a React.memo(component). Recreating the function frequently to avoid stale closure may cause unnecessary rendering of child components or unnecessary recalculation of an expensive useMemo.
In this scenario, we can use the useRef hook. The idea is keeping a fresh and updated copy of the state that is used in our function (closure) inside a useRef hook.
Why useRef?
The memory address of the object returned by
useRefnever changes in component lifecycle. Only thecurrentproperty of that object changes. So if a closure uses thisuseRefstate, it will always point to the same memory address and therefore no stale closure.useRefdoes not cause a re-render of the component when itscurrentproperty changes and therefore no unnecessary re-render.
Make a copy of your original state in a useRef hook and then update this useRef every time that state changes using useEffect to make sure that the useRef always contain the fresh copy of the original state.
const [originalState] = useState("Hello");
const stateCopy = useRef(originalState);
useEffect(() => {
stateCopy.current = originalState; // Keep ref updated
// ... other logic
}, [originalState]);
And then use the stateCopy in your closure instead of directly using the originalState.
const [originalState] = useState("Hello");
const stateCopy = useRef(originalState);
useEffect(() => {
stateCopy.current = originalState; // Keep ref updated
// ... other logic
}, [originalState]);
const myClosureFunction = useCallback(() => {
console.log(stateCopy.current) // ✅ Always fresh
}, [])
Now your closure function is never re-created and it has access to the fresh state via useRef. You can safely use this myClosureFunction as a dependency of other hooks or as props of a child component.
Top comments (0)