useCallback y useMemo: Optimización de renderizados

Lectura
25 min~5 min lectura

Concepto clave

En React, cuando un componente se vuelve a renderizar, todas las funciones y objetos que contiene se recrean en cada renderizado. Esto puede causar que los componentes hijos se rendericen innecesariamente si reciben estas funciones como props, incluso si la lógica de la función no ha cambiado. useCallback y useMemo son hooks que permiten memorizar (cachear) valores y funciones para evitar cálculos costosos y renderizados innecesarios.

Imagina que tienes una calculadora física: cada vez que presionas un botón, la calculadora vuelve a calcular todo desde cero. Con useMemo, guardas el resultado de una operación para que solo se recalcule cuando cambien los operandos. Con useCallback, guardas la definición de una función (como la operación 'sumar') para que no se cree una nueva función cada vez que presionas un botón, a menos que sus dependencias cambien.

La clave está en entender que estos hooks no hacen que tu aplicación sea mágicamente más rápida; deben usarse solo cuando hay un problema de rendimiento medible. Usarlos en exceso puede empeorar el rendimiento por el costo de la memorización misma.

Cómo funciona en la práctica

Para usar useCallback, envuelves una función y le pasas un array de dependencias. React devolverá la misma instancia de la función mientras las dependencias no cambien. Ejemplo:

import React, { useState, useCallback } from 'react';

function Parent() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount(c => c + 1);
  }, []); // sin dependencias, nunca cambia

  return ;
}

Para useMemo, envuelves un cálculo costoso y un array de dependencias. React devolverá el valor memorizado hasta que las dependencias cambien. Ejemplo:

import React, { useMemo } from 'react';

function ExpensiveComponent({ items, filter }) {
  const filteredItems = useMemo(() => {
    return items.filter(item => item.includes(filter));
  }, [items, filter]);

  return 
    {filteredItems.map(item =>
  • {item}
  • )}
; }

Código en acción

Veamos un ejemplo completo donde un componente padre pasa una función a un hijo. Sin useCallback, el hijo se renderiza cada vez que el padre se renderiza, incluso si la función no cambia. Con useCallback, evitamos ese renderizado innecesario.

Antes (sin optimización):

import React, { useState } from 'react';

const Child = React.memo(({ onReset }) => {
  console.log('Child rendered');
  return Reset;
});

function Parent() {
  const [count, setCount] = useState(0);

  const handleReset = () => {
    setCount(0);
  };

  return (
    

Count: {count}

setCount(c => c + 1)}>Increment
); }

Después (con useCallback):

import React, { useState, useCallback } from 'react';

const Child = React.memo(({ onReset }) => {
  console.log('Child rendered');
  return Reset;
});

function Parent() {
  const [count, setCount] = useState(0);

  const handleReset = useCallback(() => {
    setCount(0);
  }, []);

  return (
    

Count: {count}

setCount(c => c + 1)}>Increment
); }

Nota: Child usa React.memo para que solo se renderice si sus props cambian. Sin useCallback, handleReset se crea nuevo en cada render, por lo que React.memo no puede evitar el re-renderizado. Con useCallback, la referencia a la función es estable, y React.memo funciona correctamente.

Errores comunes

  1. Usar useCallback/useMemo sin necesidad: Memorizar todo empeora el rendimiento. Solo úsalos cuando tengas evidencia de cuellos de botella (profiling con React DevTools).
  2. Olvidar las dependencias: Si omites una dependencia, la función o valor memorizado quedará desactualizado (stale closure). Usa eslint-plugin-react-hooks para detectar dependencias faltantes.
  3. Confundir useCallback con useMemo: useCallback memoriza una función; useMemo memoriza un valor. Son equivalentes: useCallback(fn, deps) es lo mismo que useMemo(() => fn, deps).
  4. No usar React.memo en el hijo: useCallback solo evita que la referencia cambie, pero si el hijo no está envuelto en React.memo, se renderizará igualmente.
  5. Poner objetos en dependencias sin estabilizarlos: Si una dependencia es un objeto que se crea nuevo cada render (ej. {}), useCallback no servirá. Debes memorizar también ese objeto con useMemo.

Checklist de dominio

  • Identifico cuándo un componente hijo se renderiza innecesariamente por props de tipo función u objeto.
  • Explico la diferencia entre useCallback y useMemo con ejemplos.
  • Uso React.memo en componentes hijos para aprovechar la memorización de props.
  • Declaro correctamente el array de dependencias en useCallback y useMemo.
  • Evito el uso excesivo de estos hooks; solo los aplico tras medir un problema de rendimiento.
  • Reconozco un stale closure cuando veo un valor desactualizado dentro de un callback memorizado.
  • Puedo refactorizar un componente sin optimización a uno optimizado, mostrando el antes y después.

Optimización de una lista de tareas con useCallback y useMemo

Refactoriza el siguiente componente de lista de tareas para evitar renderizados innecesarios. El componente debe mostrar una lista de tareas, permitir agregar nuevas tareas y marcarlas como completadas. Usa useCallback para las funciones y useMemo para el listado filtrado (tareas pendientes).

Entregable: Un archivo TaskList.js con el componente optimizado. Incluye comentarios explicando dónde y por qué usaste cada hook.

Paso 1: Crea un componente TaskItem que reciba props: task, onToggle. Envuélvelo en React.memo.

Paso 2: En el componente TaskList, usa useState para manejar el array de tareas y el texto del input.

Paso 3: Usa useCallback para las funciones addTask y toggleTask. Asegúrate de incluir las dependencias correctas.

Paso 4: Usa useMemo para calcular las tareas pendientes (filter por completed === false).

Paso 5: Renderiza las tareas pendientes usando TaskItem.

Rúbrica de evaluación:

  • Uso correcto de React.memo en TaskItem (1 punto)
  • useCallback con dependencias correctas (2 puntos)
  • useMemo para filtrado (1 punto)
  • Renderizado condicional: solo se renderizan tareas pendientes (1 punto)
  • Comentarios claros explicando cada optimización (1 punto)
Pistas
  • Para useCallback en addTask, la dependencia es el array de tareas si usas el spread operator. Considera usar la forma funcional de setState para evitar esa dependencia.
  • React.memo solo funciona si las props no cambian; asegúrate de que las funciones pasadas a TaskItem estén memorizadas.
  • useMemo para el filtrado: el array de tareas es la dependencia; el filtro en sí no cambia.