Necesito detectar si en un render hubo un nodo que no cambió respecto al render anterior y por lo tanto el ref callback nunca será llamado.
En React, el callback de un ref solo se dispara cuando el nodo cambia (se monta, se desmonta o se reemplaza). Si entre un render y el siguiente el mismo nodo se mantiene sin cambios, el callback no se vuelve a invocar.
Por eso, si querés detectar que un nodo no cambió respecto al render anterior, no podés confiar únicamente en el callback del ref. Tenés algunas estrategias:
📌 Estrategias posibles
Comparar referencias en un
useEffectGuardás el nodo en unuseRefy luego verificás si cambió entre renders:tsxfunction MiComponente() { const divRef = React.useRef<HTMLDivElement | null>(null); const prevNode = React.useRef<HTMLDivElement | null>(null); React.useEffect(() => { if (prevNode.current === divRef.current) { console.log("El nodo no cambió"); } else { console.log("El nodo cambió"); prevNode.current = divRef.current; } }); return <div ref={divRef}>Hola</div>; }Usar
useLayoutEffectSi necesitás detectar cambios justo después del commit del DOM,useLayoutEffectte asegura que la comparación se hace antes de que el navegador pinte.Ref callback + estado propio Podés combinar el ref callback con un estado que marque cuándo fue llamado. Si en un render no se llama, sabés que el nodo se mantuvo igual:
tsxfunction MiComponente() { const [called, setCalled] = React.useState(false); const refCallback = React.useCallback((node: HTMLDivElement | null) => { if (node) { setCalled(true); } }, []); React.useEffect(() => { if (!called) { console.log("El nodo no cambió en este render"); } setCalled(false); // reset para el próximo ciclo }); return <div ref={refCallback}>Hola</div>; }
✅ Conclusión
El ref callback no se dispara si el nodo sigue siendo el mismo.
Para detectar esa situación, tenés que comparar manualmente la referencia entre renders o usar un estado auxiliar.
La elección depende de si querés detectar cambios en el DOM inmediatamente (
useLayoutEffect) o después del render (useEffect).
¿Querés que te arme un patrón reutilizable (un custom hook tipo useStableRefChange) que encapsule esta lógica y te diga directamente si el nodo cambió o se mantuvo igual?
Perfecto, Gabriel. Te armo un custom hook reutilizable que encapsula la lógica de detectar si un nodo cambió o se mantuvo igual entre renders. Así no tenés que repetir comparaciones manuales en cada componente.
📌 Hook useStableRefChange
import { useEffect, useRef, useState, useCallback } from "react";
export function useStableRefChange<T extends HTMLElement>() {
const ref = useRef<T | null>(null);
const prevNode = useRef<T | null>(null);
const [changed, setChanged] = useState<boolean | null>(null);
const callback = useCallback((node: T | null) => {
ref.current = node;
}, []);
useEffect(() => {
if (prevNode.current === ref.current) {
setChanged(false); // el nodo se mantuvo igual
} else {
setChanged(true); // el nodo cambió
prevNode.current = ref.current;
}
});
return { refCallback: callback, node: ref.current, changed };
}
📌 Ejemplo de uso
function MiComponente() {
const { refCallback, node, changed } = useStableRefChange<HTMLDivElement>();
useEffect(() => {
if (changed === false) {
console.log("El nodo se mantuvo igual en este render");
} else if (changed === true) {
console.log("El nodo cambió respecto al render anterior");
}
}, [changed]);
return <div ref={refCallback}>Hola</div>;
}
✅ Qué hace
Te devuelve un ref callback para asignar al nodo.
Guarda el nodo actual y el anterior.
Expone un flag
changedque indica si el nodo cambió (true), se mantuvo igual (false), o todavía no hay comparación (null).Cada instancia del hook es independiente, así que podés usarlo en varios componentes sin interferencias.
¿Querés que te prepare también una versión más avanzada que detecte no solo si el nodo cambió, sino también si sus atributos o props relevantes se mantuvieron iguales entre renders?
En React, la prop especial key tiene un rol muy específico: le dice al reconciliador cómo identificar un nodo en la lista de hijos. Cambiarla entre renders tiene consecuencias importantes:
📌 Qué pasa al cambiar key
Cuando un componente recibe una
keydistinta, React considera que es un nodo completamente nuevo.Eso significa que:
El componente anterior se desmonta (se destruye su estado interno, refs, efectos, etc.).
Se monta un nuevo componente desde cero con la nueva
key.
En otras palabras: cambiar la
keyfuerza un reset completo del ciclo de vida del componente.
📌 Ejemplo
function Item({ value }: { value: number }) {
const [count, setCount] = React.useState(0);
return <div onClick={() => setCount(c => c + 1)}>{value} - {count}</div>;
}
function App({ toggle }: { toggle: boolean }) {
return (
<Item key={toggle ? "A" : "B"} value={toggle ? 1 : 2} />
);
}
Si
togglecambia detrueafalse, lakeypasa de"A"a"B".React desmonta el componente con key
"A"y monta uno nuevo con key"B".El estado
countse pierde y vuelve a empezar en0.
✅ Conclusión
Cambiar la
keyentre renders reinicia el componente.Es útil cuando querés forzar un reset (ej. limpiar formularios, reiniciar animaciones).
Pero si lo hacés accidentalmente, podés perder estado y refs sin darte cuenta.
👉 Regla práctica:
Usá
keypara identificar elementos en listas.No la cambies arbitrariamente en componentes individuales, salvo que realmente quieras forzar un desmontaje y montaje.
¿Querés que te muestre un diagrama visual del ciclo de vida mostrando cómo React desmonta y monta el componente cuando cambia la key?

Sí, exactamente. Cuando cambiás la key de un componente entre renders, React realiza un desmontaje completo del componente anterior y un montaje nuevo del componente con la nueva key. Eso implica que:
✅ El callback del ref se llama dos veces:
Primero con
null→ cuando el componente anterior se desmonta.Esto indica que el nodo DOM asociado fue eliminado.
Ejemplo:
refCallback(null)
Luego con el nuevo nodo → cuando el nuevo componente se monta.
React invoca el callback con el nuevo elemento DOM.
Ejemplo:
refCallback(<div>)
📌 ¿Por qué es útil saber esto?
Podés detectar explícitamente el ciclo de desmontaje/montaje.
Ideal para limpiar recursos, cancelar animaciones, o reiniciar lógica asociada al nodo.
También te permite diferenciar entre un cambio de props y un reemplazo completo.
¿Querés que te prepare un custom hook tipo useRefLifecycle que te diga cuándo el ref fue desmontado, montado o reemplazado, y te dé acceso a esos eventos de forma declarativa?
Aquí tenés un custom hook reutilizable que detecta el ciclo de vida del nodo referenciado: si fue montado, desmontado o reemplazado. Ideal para observar el comportamiento del ref callback en casos como cambio de key.
📌 Hook: useRefLifecycle
import { useCallback, useEffect, useRef, useState } from "react";
type RefLifecycle<T> = {
refCallback: (node: T | null) => void;
node: T | null;
mounted: boolean;
unmounted: boolean;
replaced: boolean;
};
export function useRefLifecycle<T extends HTMLElement>(): RefLifecycle<T> {
const nodeRef = useRef<T | null>(null);
const prevNodeRef = useRef<T | null>(null);
const [mounted, setMounted] = useState(false);
const [unmounted, setUnmounted] = useState(false);
const [replaced, setReplaced] = useState(false);
const refCallback = useCallback((node: T | null) => {
if (node === null) {
setUnmounted(true);
setMounted(false);
setReplaced(false);
} else {
if (prevNodeRef.current === null) {
setMounted(true);
setReplaced(false);
} else if (prevNodeRef.current !== node) {
setMounted(true);
setReplaced(true);
} else {
setMounted(false);
setReplaced(false);
}
}
nodeRef.current = node;
prevNodeRef.current = node;
}, []);
useEffect(() => {
// Reset flags after each render
return () => {
setMounted(false);
setUnmounted(false);
setReplaced(false);
};
});
return {
refCallback,
node: nodeRef.current,
mounted,
unmounted,
replaced,
};
}
📌 Ejemplo de uso
function MiComponente() {
const { refCallback, mounted, unmounted, replaced } = useRefLifecycle<HTMLDivElement>();
useEffect(() => {
if (mounted) console.log("Nodo montado");
if (unmounted) console.log("Nodo desmontado");
if (replaced) console.log("Nodo reemplazado");
}, [mounted, unmounted, replaced]);
return <div ref={refCallback}>Hola</div>;
}
✅ Qué te da
mounted:truesi el nodo fue montado en este render.unmounted:truesi el nodo fue desmontado (callback connull).replaced:truesi el nodo anterior fue reemplazado por otro (ej. cambio dekey).refCallback: el callback que asignás alref.node: el nodo actual (si está montado).
¿Querés que te prepare una versión que también detecte si el nodo fue actualizado sin cambiar (por ejemplo, si sus props cambiaron pero el DOM se mantuvo)?
No hay comentarios:
Publicar un comentario