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

useEffect sees 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 count changes, 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

  1. useEffect sees the world as it was when it was created.

  2. Dependencies allow the effect to “rebirth” when important values change.

  3. useRef allows reading current values without recreating effects.

  4. 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