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
keydel 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)
-
Detectar remounts
useEffect(() => {
console.log("MOUNT");
return () => console.log("UNMOUNT");
}, []);
Si ves UNMOUNT → ya encontraste el problema.
-
Loggear identidad del ref
console.log(ref);
Si cambia → hay nueva instancia.
-
Verificar
key
Muy típico bug:
<Component key={someChangingValue} />
-
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.
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:
-
❌
keydinámico -
❌ render condicional que lo desmonta
-
❌ recreación del árbol
👉 Si necesitás persistencia REAL:
-
subí el
useRefa 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
.currenta 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
.currenten 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.currenten 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.
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:
-
En ese render,
storetoma el valor deref.current -
El handler se crea capturando esa variable
store -
Aunque
ref.currentcambie 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:
-
El handler captura la referencia al objeto
ref -
Cada vez que se ejecuta:
-
lee
ref.currenten tiempo real
-
➡️ siempre obtiene el valor más actualizado
🧠 Analogía rápida
Pensalo así:
-
❌
const store = ref.current
→ sacaste una foto -
✅
ref.currentdentro 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 });
-
ref→ SIEMPRE es el mismo objeto -
ref.current→ puede cambiar
Entonces:
| Qué capturás | ¿Cambia? | Resultado |
|---|---|---|
ref | ❌ no | seguro ✅ |
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:
-
snapshotqueda 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”
-
refes estable,ref.currentno -
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:
-
storees 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
-
storenunca 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/useReducerte 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.
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
-
useRefsolo 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.