Introduction
In this post, we’ll explain a common problem in React: stale closures inside useEffect.
You’ll learn how to:
-
Identify when your effect is using outdated values
-
Fix it using dependencies in the array
-
Use useRef to access up-to-date values without recreating the effect
By the end, you’ll have three practical examples you can copy and try yourself.
What is a stale closure?
A stale closure happens when:
-
An effect (
useEffect) captures a value from the initial render -
That value changes later, but the effect still uses the old value
Conceptual example:
/*
Render → useEffect se crea → closure captura count = 0
Usuario incrementa count → componente re-renderiza
useEffect sigue viendo count = 0
*/
💡 In simple terms
useEffectsees the world as it was when it was created.
Example 1: Classic bug (no dependencies)
import { useState, useEffect, useRef } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current);
}, 1000);
return () => clearInterval(id);
}, []);
return (
Count: {count}
);
}
What happens:
-
The counter on the screen increases
-
The console always logs
0 -
ESLint warning: “React Hook useEffect has a missing dependency: ‘count'”
Example 2: Fix using dependencies
useEffect(() => {
const id = setInterval(() => {
console.log('Valor actualizado con dependencias:', count);
}, 1000);
return () => clearInterval(id);
}, [count]); // ✅ ahora count está en dependencias
What happens:
-
Each time
countchanges, React:-
cleans the previous interval
-
creates a new interval with the updated value
-
-
The console always logs the correct value
-
⚠️ Note: if the effect is heavy, recreating it each time may be costly
Example 3: Advanced fix using useRef
'use client';
import { useState, useEffect, useRef } from 'react';
export default function StaleClosureRef() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
countRef.current = count; // update the ref on each render
useEffect(() => {
const id = setInterval(() => {
console.log('Value updated with ref:', countRef.current);
}, 1000);
return () => clearInterval(id);
}, []); // ✅ effect created only once
return (
Count: {count}
);
}
What happens:
-
The effect runs only once
-
The console always logs the current value
-
More efficient than recreating the effect on every change
Conclusion and golden rule
-
useEffectsees the world as it was when it was created. -
Dependencies allow the effect to “rebirth” when important values change.
-
useRefallows reading current values without recreating effects. -
Always clean up resources like intervals, event listeners, or pending fetches.
Key phrase:
“Dependencies → rebirth of the effect. useRef → read the present without rebirth.”
Pro tips for production
- For intervals, animations, polling, and WebSockets, useRef is highly recommended
- For fetches depending on filters or props, use proper dependencies
- Never ignore ESLint warnings without understanding the reason

