jueves, 19 de marzo de 2026

Problemas con useRef en React

 Tengo un objeto que guardo en un Array que está a su vez guardado en un useRef. Este objeto se usa tanto en la función del componente como así también siendo capturado como contexto de manejadores de evento definidos dentro de la misma función del componente. De pronto el objeto pierde su valor y es reiniciado a su valor inicial definido el useRef. ¿Hay algún reporte de algo así atribuido a un bug en React o en el motor de Javascript? (Contexto: React)

Lo que describís no coincide con un bug conocido ni de React ni del motor de JavaScript. Sí coincide perfectamente con varios patrones clásicos (y bastante traicioneros) de React. Te resumo los más probables —porque tu síntoma (“el objeto vuelve al valor inicial del useRef”) tiene causas bastante específicas:


🧠 1. Remount del componente (LA causa más común)

useRef NO se resetea en re-renders, pero sí se reinicializa en un remount.

Esto pasa cuando:

  • cambia el key del componente

  • hay render condicional ({cond && <Comp />})

  • cambia la estructura del árbol

  • navegación / routing

👉 En ese caso React destruye la instancia y crea otra nueva → todos los hooks arrancan de cero.

💥 Resultado:
Tu ref.current vuelve al valor inicial → exactamente lo que describís.


🧠 2. Closures + event handlers (MUY probable por tu descripción)

Decís que:

el objeto es capturado por handlers definidos en el componente

Ahí entra el clásico problema de stale closures.

Un handler:

const handler = () => {
console.log(ref.current);
};
  • captura el ref object (eso está bien)

  • pero si hay lógica alrededor o copias, podés terminar leyendo algo viejo o inconsistente

Además, si mezclás refs + async/eventos:

  • timers

  • listeners (addEventListener)

  • callbacks externos

podés tener ejecuciones fuera del ciclo actual del componente.

👉 Esto no es un bug: es cómo funcionan las closures en JS + el modelo de React.


🧠 3. Tenés múltiples instancias del componente

Esto pasa más de lo que parece:

  • el componente se renderiza dos veces (ej: StrictMode en dev)

  • o hay dos ramas del árbol con el mismo componente

  • o un parent recrea todo sin que lo notes

👉 Cada instancia tiene su propio useRef

Entonces parece que “se resetea”, pero en realidad:

estás viendo otra instancia distinta


🧠 4. Mutación + asincronía (race conditions)

Si estás haciendo algo tipo:

ref.current.array.push(obj);
setTimeout(() => {
console.log(ref.current.array);
});

y en el medio:

  • reasignás el array

  • o lo vaciás

  • o lo reemplazás

👉 el callback ve un estado inesperado

Esto es común con refs + buffers + async.


🧠 5. Reasignación accidental del ref

Ojo con cosas como:

ref.current = initialValue;

en algún efecto o rama lógica.
Esto pasa más seguido de lo que uno cree.


🚫 Entonces… ¿bug de React?

👉 Hay bugs reportados en React relacionados con closures y eventos (ej: useEffectEvent con valores stale)
pero:

  • no describen reset de useRef

  • son casos específicos experimentales

👉 En producción, lo que describís casi siempre es lifecycle + closures, no un bug del framework.


🔍 Cómo lo debuggearía yo (rápido y efectivo)

  1. Detectar remounts

useEffect(() => {
console.log("MOUNT");
return () => console.log("UNMOUNT");
}, []);

Si ves UNMOUNT → ya encontraste el problema.


  1. Loggear identidad del ref

console.log(ref);

Si cambia → hay nueva instancia.


  1. Verificar key
    Muy típico bug:

<Component key={someChangingValue} />

  1. Ver quién muta ref.current
    Buscar en todo el código:

ref.current =

🧩 Conclusión clara

Lo que describís:

“objeto en useRef se reinicia inesperadamente”

👉 NO es un bug conocido de React ni de JS
👉 99% de las veces es uno de estos:

  • remount del componente (top sospechoso)

  • múltiples instancias

  • closure desfasada

  • async/race condition

  • mutación accidental


Si querés, pegá un snippet del componente (aunque sea simplificado) y te digo exactamente cuál de estos cinco es en tu caso.

Mi lógica depende de que todos los closures de event handlers y la función del componente vean exactamente la misma referencia siempre. ¿Qué puedo hacer?

Perfecto, estás apuntando a algo clave: consistencia de identidad + consistencia de lectura entre closures.
Te lo digo directo: eso no está garantizado automáticamente en React, pero sí se puede diseñar para lograrlo de forma robusta.


🧠 El problema real (en una línea)

Querés que:

todos los handlers + render lean exactamente el mismo objeto vivo

Pero:

  • React re-renderiza

  • closures capturan momentos distintos

  • el componente puede remountarse

👉 entonces esa garantía hay que construirla explícitamente


✅ Estrategias sólidas (de verdad)

1. Usar useRef como “single source of truth” (bien usado)

Esto es lo correcto, pero con una regla:

👉 NUNCA dependas de valores capturados, siempre leé .current en tiempo de ejecución

const storeRef = useRef({ items: [] });

const handler = () => {
// SIEMPRE esto:
const store = storeRef.current;
console.log(store.items);
};

✔ esto garantiza:

  • misma referencia

  • mismo contenido actualizado

❌ esto rompe todo:

const store = storeRef.current; // ❌ capturado
const handler = () => {
console.log(store.items);
};

2. Evitar remounts (esto es CRÍTICO)

Si el componente se remonta, perdiste todo.

Checklist:

  • key dinámico

  • ❌ render condicional que lo desmonta

  • ❌ recreación del árbol

👉 Si necesitás persistencia REAL:

  • subí el useRef a un componente padre estable

  • o sacalo fuera del árbol (módulo / singleton)


3. Patrón “store mutable estable” (muy robusto)

Esto es lo que estás buscando realmente:

function useStableStore() {
const ref = useRef(null);

if (!ref.current) {
ref.current = {
items: [],
add(item) {
this.items.push(item);
}
};
}

return ref.current;
}

Uso:

const store = useStableStore();

const handler = () => {
store.add("x"); // siempre misma referencia
};

👉 Esto elimina completamente el problema de closures.


4. Event handlers siempre actualizados (anti-closure trap)

Si tenés handlers que viven mucho (listeners, timers, etc.), usá este patrón:

function useEvent(callback) {
const ref = useRef(callback);

ref.current = callback;

return useCallback((...args) => {
return ref.current(...args);
}, []);
}

👉 Esto asegura que:

  • el handler nunca cambia de identidad

  • pero siempre ejecuta lógica actual


5. Si necesitás consistencia fuerte → usar un store externo

Cuando la consistencia es crítica:

👉 no dependas del ciclo de React

Opciones:

  • Zustand

  • Redux

  • o un simple singleton:

const store = {
items: []
};

export default store;

✔ todos los closures ven lo mismo
✔ no hay remount que lo afecte


⚠️ Anti-patrones que causan EXACTAMENTE tu bug

  • copiar .current a una variable

  • mutar y luego reasignar ref.current

  • depender de valores capturados en handlers

  • asumir que React mantiene instancia única

  • lógica async sin leer .current en el momento


🧩 TL;DR (lo más importante)

Si querés que todo vea lo mismo:

👉 regla de oro

nunca uses el valor capturado, siempre leé ref.current en el momento de uso

👉 y además

  • evitá remounts

  • o mové el estado fuera del componente


Si querés, describime cómo estás usando ese array (por ejemplo: eventos DOM, websockets, timers, etc.) y te propongo una arquitectura exacta para tu caso —porque ahí suele estar el verdadero problema.

¿Por qué capturar el current afuera del closure no funciona pero capturarlo dentro sí?

La diferencia no es React: es cómo funcionan las closures en JavaScript + el momento en que se evalúan las variables.


🧠 Idea clave (muy concreta)

Una closure no “vuelve a leer” variables externas
recuerda el valor que tenían cuando se creó


🔍 Caso 1: capturás current afuera ❌

const store = ref.current; // ⛔ snapshot en ESTE render

const handler = () => {
console.log(store);
};

👉 Qué pasa:

  1. En ese render, store toma el valor de ref.current

  2. El handler se crea capturando esa variable store

  3. Aunque ref.current cambie después…

➡️ el handler sigue apuntando a ese valor viejo

💥 Es un snapshot congelado en el tiempo


🔍 Caso 2: accedés dentro del handler ✅

const handler = () => {
console.log(ref.current);
};

👉 Qué pasa:

  1. El handler captura la referencia al objeto ref

  2. Cada vez que se ejecuta:

    • lee ref.current en tiempo real

➡️ siempre obtiene el valor más actualizado


🧠 Analogía rápida

Pensalo así:

  • const store = ref.current
    → sacaste una foto

  • ref.current dentro del handler
    → estás mirando una cámara en vivo


⚠️ Detalle importante (muy sutil)

Esto es lo que mucha gente no ve:

const ref = useRef({ value: 1 });
  • refSIEMPRE es el mismo objeto

  • ref.currentpuede cambiar

Entonces:

Qué capturás¿Cambia?Resultado
ref❌ noseguro ✅
ref.current✅ sístale ❌

🔬 Por qué pasa (más técnico)

Las closures en JS capturan:

  • bindings (variables)

  • no expresiones dinámicas

Entonces:

const store = ref.current;

👉 crea un binding nuevo (store) con un valor fijo

Mientras que:

ref.current

👉 es una lectura dinámica de propiedad cada vez


💥 Conclusión fuerte

No es que “capturar afuera no funciona”
es que estás capturando el valor equivocado

✔ Capturá:

  • ref (estable)

❌ No captures:

  • ref.current (mutable)


🧩 Regla mental (muy útil)

Si algo puede cambiar → no lo captures, léelo


Si querés, puedo mostrarte un ejemplo donde parece que debería funcionar pero falla igual (casos con async / eventos DOM), que son los que suelen romper producción sin que se entienda por qué.

Buenísimo—te muestro casos reales donde parece que está bien… pero se rompe igual. Estos son los que más dolores de cabeza causan.


💥 Caso 1: setTimeout (el clásico engañoso)

const ref = useRef({ count: 0 });

function handleClick() {
const snapshot = ref.current; // ❌ parece inocente

setTimeout(() => {
console.log(snapshot.count);
}, 1000);
}

👉 Problema:

  • hacés click cuando count = 0

  • antes de 1s, alguien hace ref.current.count = 5

➡️ el setTimeout imprime 0, no 5

💥 porque capturaste un snapshot


✅ Solución correcta

function handleClick() {
setTimeout(() => {
console.log(ref.current.count); // siempre actualizado
}, 1000);
}

💥 Caso 2: event listeners del DOM

useEffect(() => {
const snapshot = ref.current; // ❌

const handler = () => {
console.log(snapshot.value);
};

window.addEventListener("click", handler);

return () => window.removeEventListener("click", handler);
}, []);

👉 Esto parece correcto porque:

  • el effect corre una sola vez

👉 Pero:

  • snapshot queda congelado para siempre


✅ Solución

useEffect(() => {
const handler = () => {
console.log(ref.current.value);
};

window.addEventListener("click", handler);

return () => window.removeEventListener("click", handler);
}, []);

💥 Caso 3: closures + lógica intermedia (EL MÁS TRAICIONERO)

const handler = () => {
const store = ref.current; // ⚠️ sutil

doSomethingAsync(() => {
console.log(store.value); // ❌ puede ser viejo
});
};

👉 Aunque “parece dentro del handler”, igual es snapshot


✅ Solución

const handler = () => {
doSomethingAsync(() => {
console.log(ref.current.value); // ✅
});
};

💥 Caso 4: destructuring (ultra común)

const { current } = ref; // ❌

const handler = () => {
console.log(current.value);
};

👉 Esto es EXACTAMENTE lo mismo que romper todo.


✅ Solución

const handler = () => {
console.log(ref.current.value);
};

💥 Caso 5: “pero si es el mismo objeto…” (error conceptual)

const store = ref.current;

store.value = 10;

👉 Esto sí funciona porque no cambiaste la referencia

Pero:

ref.current = { value: 20 };

👉 ahora:

  • store → viejo objeto

  • ref.current → nuevo objeto

💥 inconsistencia total


🧠 Insight clave (nivel senior)

Hay dos tipos de mutación:

1. Mutar el mismo objeto ✅

ref.current.value = 2;

2. Reemplazar el objeto ❌ (rompe snapshots)

ref.current = { value: 2 };

🔥 Regla de oro definitiva

Nunca captures algo que pueda ser reemplazado en el futuro


🧩 Patrón mental correcto

✔ Siempre:

ref.current.algo

❌ Nunca:

const algo = ref.current.algo

(si ese valor puede cambiar)


🚨 Bonus: por qué esto explota más en React

Porque React:

  • re-renderiza

  • mezcla sync + async

  • recrea funciones

  • puede remountar

👉 entonces los timings hacen que estos bugs aparezcan “de la nada”


🧠 TL;DR

  • Closure captura valores → no “reactividad”

  • ref es estable, ref.current no

  • snapshots = bugs en async/eventos


Si querés, en el próximo paso puedo:
👉 armarte un patrón tipo “mini-store reactivo” con useRef que evita TODOS estos problemas (nivel production-ready)

Perfecto—te armo un patrón production-ready que resuelve todo lo que venimos hablando:

✔ misma referencia siempre
✔ sin stale closures
✔ seguro con async, eventos, timers
✔ sin depender del ciclo de render de React


🧠 Idea base

Construimos un store mutable estable dentro de un useRef, pero con:

  • API clara (get, set, subscribe)

  • lectura siempre “en vivo”

  • handlers sin closures stale


🧱 Implementación

import { useRef, useCallback } from "react";

export function useStableStore(initialState) {
const storeRef = useRef(null);

if (!storeRef.current) {
const listeners = new Set();

let state = initialState;

storeRef.current = {
get() {
return state;
},

set(updater) {
const next =
typeof updater === "function" ? updater(state) : updater;

state = next;

listeners.forEach((l) => l(state));
},

mutate(fn) {
fn(state); // mutación controlada
listeners.forEach((l) => l(state));
},

subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
};
}

return storeRef.current;
}

⚡ Uso básico

const store = useStableStore({ count: 0 });

🎯 Event handlers (sin stale closures)

const handleClick = () => {
store.set((s) => ({ ...s, count: s.count + 1 }));
};

👉 No hay problema de closures porque:

  • store es estable

  • get() lee estado actual


⏱ Async seguro

const handleAsync = () => {
setTimeout(() => {
console.log(store.get().count); // SIEMPRE actualizado
}, 1000);
};

🌐 Eventos DOM seguros

useEffect(() => {
const handler = () => {
console.log(store.get().count);
};

window.addEventListener("click", handler);
return () => window.removeEventListener("click", handler);
}, [store]);

🔄 Re-render opcional (si querés UI reactiva)

Esto es clave: el store es independiente de React, pero podés “conectarlo”.

import { useState, useEffect } from "react";

function useStoreValue(store) {
const [state, setState] = useState(store.get());

useEffect(() => {
return store.subscribe(setState);
}, [store]);

return state;
}

Uso:

const store = useStableStore({ count: 0 });
const state = useStoreValue(store);

return <div>{state.count}</div>;

🔥 Ventajas reales

✅ 1. Cero stale closures

  • nunca capturás estado

  • siempre store.get()


✅ 2. Identidad estable

  • store nunca cambia

  • closures seguros


✅ 3. No depende de renders

  • funciona en:

    • timers

    • sockets

    • listeners

    • promesas


✅ 4. Control total

  • decidís cuándo re-renderizar

  • no hay magia oculta


⚠️ Reglas para no romperlo

❌ No hagas esto:

const state = store.get(); // snapshot peligroso fuera de uso inmediato

(si lo vas a usar más tarde)


❌ No reemplaces el store

store = otroStore; // ❌

⚠️ Con mutate:

store.mutate((s) => {
s.count++; // OK, pero controlado
});

🧠 Insight importante

Esto es básicamente:

  • un mini-Redux

  • pero sin reducers obligatorios

  • y sin re-renders automáticos


🚀 Cuándo usar esto

Este patrón es ideal si:

  • manejás eventos externos (DOM, sockets)

  • tenés lógica async compleja

  • necesitás consistencia fuerte entre closures

  • useState / useReducer te generan inconsistencias


🧩 TL;DR

Si querés evitar TODOS los problemas de closures en React:

👉 no guardes estado en closures
👉 guardalo en un store estable
👉 leelo en tiempo de ejecución


Si querés, en el siguiente paso puedo:
👉 adaptarte este patrón exactamente a tu caso (array dentro de useRef + handlers) y mostrarte cómo quedaría tu código real simplificado.

Sí.

Perfecto—vamos a llevar tu caso exacto (array en useRef + handlers) a una versión robusta y sin sorpresas.


🔴 Cómo suele estar hoy (y por qué falla)

Algo así:

const itemsRef = useRef([]);

function addItem(item) {
itemsRef.current.push(item);
}

function handleClick() {
const items = itemsRef.current; // ❌ snapshot

setTimeout(() => {
console.log(items.length); // 💥 puede estar desactualizado
}, 1000);
}

👉 Problemas:

  • snapshot (items)

  • si en el medio reemplazás el array → inconsistencia

  • closures async leen cosas viejas


✅ Versión sólida (mínimo cambio)

const itemsRef = useRef([]);

function addItem(item) {
itemsRef.current.push(item);
}

function handleClick() {
setTimeout(() => {
console.log(itemsRef.current.length); // ✅ siempre actualizado
}, 1000);
}

👉 Regla aplicada: no capturar .current


🚀 Versión PRO (la que realmente te conviene)

Convertimos ese array en un store estable:

function useItemsStore() {
const ref = useRef(null);

if (!ref.current) {
const listeners = new Set();
const items = [];

ref.current = {
get() {
return items;
},

add(item) {
items.push(item);
listeners.forEach((l) => l(items));
},

remove(index) {
items.splice(index, 1);
listeners.forEach((l) => l(items));
},

clear() {
items.length = 0;
listeners.forEach((l) => l(items));
},

subscribe(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
}
};
}

return ref.current;
}

🎯 Uso en tu componente

const store = useItemsStore();

👉 Handlers (seguros siempre)

function handleAdd() {
store.add("nuevo");
}

function handleAsync() {
setTimeout(() => {
console.log(store.get().length); // ✅ SIEMPRE correcto
}, 1000);
}

👉 Event listeners (cero stale closures)

useEffect(() => {
const handler = () => {
console.log(store.get().length);
};

window.addEventListener("click", handler);
return () => window.removeEventListener("click", handler);
}, [store]);

👉 Si necesitás renderizar

function useItems(store) {
const [items, setItems] = useState(store.get());

useEffect(() => {
return store.subscribe(setItems);
}, [store]);

return items;
}

Uso:

const items = useItems(store);

return <div>{items.length}</div>;

💥 Por qué esto elimina tu bug

✔ No hay snapshots peligrosos

Nunca hacés:

const items = ...

para usarlos después


✔ No importa cuándo corre el handler

Siempre lee:

store.get()

✔ No importa async / eventos / timers

El acceso es en tiempo real


✔ No importa cuántos re-renders haya

El store:

  • vive en useRef

  • tiene identidad estable


⚠️ Dos reglas IMPORTANTES

1. Nunca hagas esto:

const items = store.get(); // ❌ si lo vas a usar después

2. Nunca reemplaces el array:

// ❌ MAL
ref.current = []

👉 siempre mutá:

items.length = 0
items.push(...)

🧠 Insight final (clave para tu caso)

Vos querías:

“que todos los closures vean la misma referencia”

👉 Con este patrón lográs algo mejor:

todos leen el mismo estado actual, en el momento correcto


🧩 TL;DR

  • useRef solo no alcanza si usás snapshots

  • el problema no es React, es cuándo leés los datos

  • solución: store estable + lectura en runtime


Si querés, en el próximo paso puedo:
👉 revisar tu código real (aunque sea recortado) y marcarte exactamente dónde se rompe y cómo migrarlo sin reescribir todo.