sábado, 14 de marzo de 2026

Detectar cambios en nodos del DOM de componentes React

 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

  1. Comparar referencias en un useEffect Guardás el nodo en un useRef y luego verificás si cambió entre renders:

    tsx
    function 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>;
    }
    
  2. Usar useLayoutEffect Si necesitás detectar cambios justo después del commit del DOM, useLayoutEffect te asegura que la comparación se hace antes de que el navegador pinte.

  3. 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:

    tsx
    function 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?

Sí.

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

tsx
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

tsx
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 changed que 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?

Si a un componente entre renders le cambio el valor de la prop especial key, ¿Qué consecuencias tiene?

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 key distinta, 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 key fuerza un reset completo del ciclo de vida del componente.

📌 Ejemplo

tsx
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 toggle cambia de true a false, la key pasa de "A" a "B".

  • React desmonta el componente con key "A" y monta uno nuevo con key "B".

  • El estado count se pierde y vuelve a empezar en 0.

✅ Conclusión

  • Cambiar la key entre 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á key para 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í.
Diagrama ciclo de vida React con key

Aquí tienes el diagrama que muestra cómo React maneja el ciclo de vida de un componente cuando cambia la prop especial key entre renders:

https://copilot.microsoft.com/th/id/BCO.a20a22f9-6a86-4455-969c-2fb26be4fe25.png

📌 Lectura del diagrama

  • Componente con key "A": está montado y tiene su estado interno (ej. count = 5).

  • Nuevo render con key "B": React detecta que la key cambió.

  • Acción:

    • Desmonta el componente "A" (se pierde su estado, efectos, refs).

    • Monta un nuevo componente "B" desde cero (estado reiniciado).

✅ Conclusión

Cambiar la key entre renders equivale a decirle a React: “este es un componente completamente nuevo”.

  • Útil para forzar un reset (ej. reiniciar formularios, animaciones).

  • Peligroso si lo hacés sin querer, porque perdés estado y refs.

¿Querés que te muestre también un ejemplo práctico con un formulario donde cambiar la key lo reinicia automáticamente, para ver cómo se aplica en un caso real?

¿Entonces voy a recibir un llamado al ref callback por el componente que se desmonta y otro llamado por el componente que se monta?

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:

  1. Primero con null → cuando el componente anterior se desmonta.

    • Esto indica que el nodo DOM asociado fue eliminado.

    • Ejemplo: refCallback(null)

  2. 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?

Sí.

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

tsx
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

tsx
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: true si el nodo fue montado en este render.

  • unmounted: true si el nodo fue desmontado (callback con null).

  • replaced: true si el nodo anterior fue reemplazado por otro (ej. cambio de key).

  • refCallback: el callback que asignás al ref.

  • 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: