jueves, 12 de marzo de 2026

Controlar referencias que entran y salen de existencia

 ¿La asignación a la prop especial ref de React vuelve a ocurrir si el componente se re-renderiza?

Sí, la asignación a la prop especial ref de React vuelve a ocurrir cuando el componente se re-renderiza, pero con matices importantes:

📌 Cómo funciona ref en el ciclo de render

  • Render inicial: React asigna el ref al nodo DOM o al componente hijo.

  • Re-render:

    • Si el mismo elemento con el mismo ref sigue presente, React no lo reasigna porque la referencia no cambió. El objeto ref.current se mantiene igual.

    • Si el elemento cambia (por ejemplo, se reemplaza por otro tipo de nodo o se desmonta/remonta), React actualiza el ref: primero lo limpia (ref.current = null) y luego lo vuelve a asignar al nuevo nodo.

  • Unmount: cuando el componente se desmonta, React pone ref.current = null.

✅ Implicación práctica

  • Si tu componente se re-renderiza pero el nodo DOM asociado al ref no cambia, el ref no se vuelve a asignar; simplemente sigue apuntando al mismo nodo.

  • Si el nodo cambia (por ejemplo, condicionalmente renderizás otro elemento), entonces React sí actualiza el ref.

📌 Ejemplo ilustrativo

tsx
function Ejemplo() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    console.log("Ref apunta a:", inputRef.current);
  });

  return (
    <div>
      <input ref={inputRef} />
    </div>
  );
}
  • En cada render, el ref sigue apuntando al mismo <input>.

  • Si reemplazás el <input> por un <textarea>, React primero limpia el ref y luego lo asigna al nuevo nodo.

👉 En resumen: los ref se actualizan solo si el nodo asociado cambia. En un re-render normal, si el nodo es el mismo, la asignación no se repite.

¿Querés que te arme un diagrama visual del ciclo de vida de un ref mostrando cuándo se asigna, cuándo se mantiene y cuándo se limpia?

Quiero una forma de que un componente padre mantenga un Array de nodos dom de las ref de React de los componentes hijos.

Para que un componente padre mantenga un array de nodos DOM provenientes de las ref de sus hijos, lo más práctico es usar callback refs en lugar de useRef individuales. De esa forma, el padre controla directamente qué se guarda en el array cada vez que React asigna o limpia una referencia.

📌 Estrategia con callback refs

  1. En el padre definís un array (o mejor, un useRef que contiene un array mutable).

  2. Pasás una función como ref a cada hijo.

  3. Esa función recibe el nodo DOM y lo agrega o lo quita del array.

📌 Ejemplo

tsx
import React, { useRef } from "react";

function Hijo({ innerRef }: { innerRef: (el: HTMLDivElement | null) => void }) {
  return <div ref={innerRef}>Soy un hijo</div>;
}

export default function Padre() {
  const nodosRef = useRef<HTMLDivElement[]>([]);

  const registrarNodo = (el: HTMLDivElement | null) => {
    if (el) {
      // Agregar si no está
      if (!nodosRef.current.includes(el)) {
        nodosRef.current.push(el);
      }
    } else {
      // Limpiar si se desmonta
      nodosRef.current = [];
    }
  };

  return (
    <div>
      <Hijo innerRef={registrarNodo} />
      <Hijo innerRef={registrarNodo} />
      <button
        onClick={() => {
          console.log("Nodos DOM actuales:", nodosRef.current);
        }}
      >
        Mostrar nodos
      </button>
    </div>
  );
}

✅ Puntos clave

  • El padre mantiene el array mutable en useRef.

  • Cada hijo recibe un callback ref que se encarga de registrar su nodo.

  • Cuando el hijo se desmonta, React llama al callback con null, lo que te permite limpiar.

  • Así el padre siempre tiene acceso a todos los nodos DOM de sus hijos.

Este patrón es muy útil cuando necesitás manipular o medir varios nodos hijos desde el padre (por ejemplo, en un odontograma para calcular posiciones o aplicar estilos).

¿Querés que te arme un diagrama visual del flujo de refs mostrando cómo el padre recibe y acumula los nodos de los hijos en un array?

Hoy
No quiero que el padre reinicie todo el Array cuando un solo hijo llama al callback con null.

Perfecto, Gabriel. Para evitar que el padre reinicie todo el array cuando un hijo se desmonta y llama al callback con null, lo que tenés que hacer es remover solo ese nodo específico en lugar de vaciar la lista completa.

📌 Estrategia mejorada

  • Usar un useRef en el padre para mantener el array de nodos.

  • En el callback ref:

    • Si recibe un nodo (el distinto de null), lo agrega si no está.

    • Si recibe null, elimina solo ese nodo del array.

📌 Ejemplo

tsx
import React, { useRef } from "react";

function Hijo({ innerRef }: { innerRef: (el: HTMLDivElement | null) => void }) {
  return <div ref={innerRef}>Soy un hijo</div>;
}

export default function Padre() {
  const nodosRef = useRef<HTMLDivElement[]>([]);

  const registrarNodo = (el: HTMLDivElement | null) => {
    if (el) {
      // Agregar si no está
      if (!nodosRef.current.includes(el)) {
        nodosRef.current.push(el);
      }
    } else {
      // Eliminar solo el nodo desmontado
      nodosRef.current = nodosRef.current.filter(n => n !== el);
    }
  };

  return (
    <div>
      <Hijo innerRef={registrarNodo} />
      <Hijo innerRef={registrarNodo} />
      <button
        onClick={() => {
          console.log("Nodos DOM actuales:", nodosRef.current);
        }}
      >
        Mostrar nodos
      </button>
    </div>
  );
}

✅ Resultado

  • El padre mantiene un array con todos los nodos DOM de los hijos.

  • Cuando un hijo se desmonta, se elimina solo ese nodo del array.

  • El resto de los nodos se conserva intacto.

Este patrón es muy útil para listas dinámicas de hijos, porque el padre siempre tiene un array actualizado de referencias sin reiniciar todo.

¿Querés que te arme un diagrama visual del flujo de registro y limpieza mostrando cómo cada hijo agrega o elimina su nodo en el array del padre?

Si el padre no borra la referencia al nodo del hijo cuando este se desmonta. ¿Con qué se queda?

Si el padre no borra la referencia cuando un hijo se desmonta, lo que queda en el array es un puntero a un nodo DOM que ya no existe en el árbol. Técnicamente:

  • Durante el desmontaje, React llama al callback ref con null.

  • Si el padre ignora ese null, el array conserva la referencia previa.

  • Esa referencia apunta a un objeto DOM que fue eliminado del documento, por lo que ref.current ya no está conectado al árbol.

  • El nodo puede quedar como un objeto huérfano en memoria hasta que el recolector de basura lo libere, pero desde el punto de vista de tu código es un elemento inválido: no responde a consultas ni está en el DOM.

📌 Consecuencias de no limpiar

  • Podés terminar con un array lleno de referencias obsoletas.

  • Si intentás manipular esos nodos, obtendrás errores o efectos inesperados (porque ya no están en el DOM).

  • Puede implicar fugas de memoria si mantenés referencias a nodos desmontados que nunca se liberan.

✅ Recomendación

Siempre que React te pase null en el callback ref, eliminá ese nodo del array. Así mantenés el array sincronizado con los hijos que realmente existen en el DOM.

¿Querés que te muestre un mini ejemplo con logs donde se ve cómo queda el array si no eliminás el nodo desmontado, y cómo se mantiene limpio si sí lo eliminás?

Pero si el callback viene con null no sé qué elemento del Array borrar.

Muy buena observación. Cuando React llama al callback ref con null, efectivamente no te dice qué nodo se desmontó, solo que ese ref ya no apunta a nada. Por eso, si querés mantener un array de nodos en el padre y limpiarlo correctamente, necesitás una forma de identificar cada hijo además del nodo en sí.

📌 Estrategias para saber qué borrar

  1. Pasar un identificador desde el hijo Cada hijo puede recibir un id único y el padre usa ese id para saber qué nodo eliminar cuando el callback recibe null.

    tsx
    function Hijo({ id, innerRef }: { id: string, innerRef: (id: string, el: HTMLDivElement | null) => void }) {
      return <div ref={el => innerRef(id, el)}>Soy hijo {id}</div>;
    }
    
    export default function Padre() {
      const nodosRef = useRef<Record<string, HTMLDivElement | null>>({});
    
      const registrarNodo = (id: string, el: HTMLDivElement | null) => {
        nodosRef.current[id] = el; // si es null, se limpia solo ese id
      };
    
      return (
        <div>
          <Hijo id="a" innerRef={registrarNodo} />
          <Hijo id="b" innerRef={registrarNodo} />
          <button onClick={() => console.log(nodosRef.current)}>Mostrar nodos</button>
        </div>
      );
    }
    

    👉 Así el padre mantiene un diccionario {id → nodo} y sabe exactamente cuál borrar.

  1. Usar forwardRef y useImperativeHandle Si los hijos son componentes más complejos, podés exponer su nodo o API interna mediante forwardRef. El padre recibe un ref por cada hijo y puede gestionarlos en un array.

  1. Mantener refs en paralelo a la lista de hijos Si renderizás una lista con .map(), podés generar un array de refs en el padre y asignarlos directamente a cada hijo. Así cada posición del array corresponde a un hijo específico.

    tsx
    const refs = useRef<HTMLDivElement[]>([]);
    items.map((item, i) => <div key={item.id} ref={el => refs.current[i] = el} />);
    

✅ Conclusión

Si solo usás el callback ref con null, no sabés qué borrar porque React no te da esa información. La solución es asociar cada hijo con un identificador o mantener la estructura paralela en el padre, de modo que puedas limpiar solo la referencia correcta.

¿Querés que te arme un mini esquema visual mostrando cómo el padre usa un diccionario {id → nodo} para mantener sincronizadas las refs de los hijos?

No hay comentarios: