En esta página

useRef, useMemo y useCallback

12 min lectura TextoCap. 2 — Estado y efectos

useRef — acceso directo al DOM

useRef crea una referencia mutable que persiste entre renders. Su uso más común es obtener acceso directo a elementos del DOM:

import { useRef } from 'react';

function CampoConFoco(): React.JSX.Element {
  const inputRef = useRef<HTMLInputElement>(null);

  const enfocarInput = () => {
    // current es el elemento HTML real
    inputRef.current?.focus();
  };

  return (
    <>
      <input ref={inputRef} type="text" placeholder="Escribe aquí" />
      <button type="button" onClick={enfocarInput}>
        Enfocar
      </button>
    </>
  );
}

El tipo genérico <HTMLInputElement> le dice a TypeScript qué tipo de elemento esperar. El valor de inputRef.current es HTMLInputElement | null porque el ref no apunta a nada hasta que el componente se monta.

Casos de uso de useRef para elementos DOM

function ReproductorVideo(): React.JSX.Element {
  const videoRef = useRef<HTMLVideoElement>(null);

  const reproducir = () => {
    void videoRef.current?.play();
  };

  const pausar = () => {
    videoRef.current?.pause();
  };

  const irAlInicio = () => {
    if (videoRef.current) {
      videoRef.current.currentTime = 0;
    }
  };

  return (
    <div>
      <video ref={videoRef} src="/video/tutorial.mp4" />
      <button type="button" onClick={reproducir}>▶ Reproducir</button>
      <button type="button" onClick={pausar}>⏸ Pausar</button>
      <button type="button" onClick={irAlInicio}>⏮ Inicio</button>
    </div>
  );
}

useRef para valores persistentes sin render

useRef también sirve para almacenar valores que persisten entre renders pero cuyo cambio no debe provocar un re-render:

function AutoguardadoEditor(): React.JSX.Element {
  const [contenido, setContenido] = useState('');
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const ultimoGuardadoRef = useRef<string>('');

  const manejarCambio = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    const nuevoTexto = e.target.value;
    setContenido(nuevoTexto);

    // Limpiar timer anterior (debounce)
    if (timerRef.current) clearTimeout(timerRef.current);

    timerRef.current = setTimeout(() => {
      if (nuevoTexto !== ultimoGuardadoRef.current) {
        ultimoGuardadoRef.current = nuevoTexto;
        console.log('Guardando:', nuevoTexto);
        // llamar a la API de guardado
      }
    }, 1000);
  };

  return (
    <textarea value={contenido} onChange={manejarCambio} />
  );
}

useMemo — memoización de cálculos costosos

useMemo calcula un valor y lo memoiza: solo lo recalcula cuando sus dependencias cambian.

function AnaliticaVentas({ ventas }: { ventas: Venta[] }): React.JSX.Element {
  const [filtroPeriodo, setFiltroPeriodo] = useState<'mes' | 'trimestre' | 'año'>('mes');

  // Este cálculo es costoso: filtrado + suma + estadísticas
  const estadisticas = useMemo(() => {
    const filtradas = ventas.filter((v) => v.periodo === filtroPeriodo);
    const total = filtradas.reduce((sum, v) => sum + v.monto, 0);
    const promedio = filtradas.length > 0 ? total / filtradas.length : 0;
    const maxima = Math.max(...filtradas.map((v) => v.monto));

    return { total, promedio, maxima, cantidad: filtradas.length };
  }, [ventas, filtroPeriodo]); // Solo recalcula si ventas o filtroPeriodo cambian

  return (
    <div>
      <select
        value={filtroPeriodo}
        onChange={(e) => setFiltroPeriodo(e.target.value as 'mes' | 'trimestre' | 'año')}
      >
        <option value="mes">Este mes</option>
        <option value="trimestre">Este trimestre</option>
        <option value="año">Este año</option>
      </select>
      <p>Total: ${estadisticas.total.toLocaleString('es-ES')}</p>
      <p>Promedio: ${estadisticas.promedio.toFixed(2)}</p>
      <p>Máxima: ${estadisticas.maxima}</p>
    </div>
  );
}

useCallback — referencias estables de funciones

useCallback memoiza una función: retorna la misma referencia mientras sus dependencias no cambien. Es útil cuando pasas callbacks a componentes que están envueltos en React.memo:

function Padre(): React.JSX.Element {
  const [valor, setValor] = useState(0);
  const [lista, setLista] = useState<string[]>([]);

  // ❌ Sin useCallback: nueva función en cada render → Hijo siempre re-renderiza
  const agregarItem = (item: string) => {
    setLista((prev) => [...prev, item]);
  };

  // ✅ Con useCallback: misma referencia → Hijo solo re-renderiza si la función cambia
  const agregarItemEstable = useCallback((item: string) => {
    setLista((prev) => [...prev, item]);
  }, []); // No depende de nada gracias al updater funcional

  return (
    <div>
      <button type="button" onClick={() => setValor((v) => v + 1)}>
        Valor: {valor}
      </button>
      <HijoMemoizado onAgregar={agregarItemEstable} />
    </div>
  );
}

React.memo — evitar re-renders del componente

React.memo es un Higher Order Component que memoriza el resultado del render. Solo vuelve a renderizar si las props cambian:

interface FilaProps {
  nombre: string;
  precio: number;
  onEditar: (nombre: string) => void;
}

// Con memo: solo re-renderiza si nombre, precio u onEditar cambian
const FilaProducto = memo(function FilaProducto({ nombre, precio, onEditar }: FilaProps) {
  return (
    <tr>
      <td>{nombre}</td>
      <td>${precio}</td>
      <td>
        <button type="button" onClick={() => onEditar(nombre)}>Editar</button>
      </td>
    </tr>
  );
});

// Comparación personalizada si la comparación por referencia no es suficiente
const FilaProductoCustom = memo(
  function FilaProducto({ nombre, precio, onEditar }: FilaProps) {
    return <tr><td>{nombre}</td><td>${precio}</td></tr>;
  },
  (prevProps, nextProps) => {
    // Retorna true si son "iguales" (no re-renderizar)
    return prevProps.nombre === nextProps.nombre && prevProps.precio === nextProps.precio;
  }
);

El flujo de optimización correcto

El proceso correcto para optimizar renders es:

  1. Mide primero con React DevTools Profiler
  2. Identifica qué componentes se renderizan innecesariamente
  3. Aplica React.memo al componente que tarda
  4. Estabiliza los callbacks con useCallback si son props del componente memoizado
  5. Memoiza cálculos costosos con useMemo si el Profiler los identifica

Con React Compiler en proyectos nuevos, muchos de estos pasos son automáticos. Pero entender los fundamentos te permite depurar cuando algo no funciona como esperas.

No sobreoptimices con useMemo y useCallback
useMemo y useCallback tienen un costo de memoria y CPU. Solo úsalos cuando tengas evidencia de un problema de rendimiento (con React DevTools Profiler) o cuando pases callbacks a componentes React.memo. La optimización prematura complica el código sin beneficio real.
useRef no causa re-renders al cambiar
A diferencia de useState, cambiar ref.current no programa un re-render. Esto lo hace ideal para valores que el componente necesita recordar entre renders pero cuyo cambio no debe actualizar la UI, como IDs de timers, instancias de bibliotecas externas o contadores de renders.
React Compiler reemplazará gran parte de useMemo y useCallback
Con React Compiler (React 19.2+), muchos usos manuales de useMemo y useCallback se vuelven innecesarios porque el compilador los agrega automáticamente. Diseña tu código para que sea correcto primero y optimiza solo cuando sea necesario.