Cuándo Usar useMemo y useCallback
Cómo y cuándo usar los hooks useMemo y useCallback de React para optimización de performance, y cuándo añaden overhead innecesario.
Overview
useMemo cachea un valor computado para que React lo reutilice entre renders a menos que sus dependencias cambien. useCallback cachea una referencia de función para el mismo propósito. Ambos hooks previenen re-renders innecesarios y cálculos redundantes. Pero tienen su propio costo — almacenar el valor cacheado y comparar dependencias en cada render. Usarlos en valores baratos de computar o funciones que no se pasan a hijos memoizados hace las cosas más lentas.
When to Use
- Cómputos costosos: filtrar una lista de 10,000 items, parsear JSON grande, math complejo
- Referencias estables para hijos memoizados: pasar callbacks a componentes `React.
- Dependencias estables para otros hooks: un valor usado en un
useEffectdependency array que no debería disparar el effect en cada render - Estabilización de context value: prevenir que todos los context consumers re-rendericen cuando el state del provider cambia
When NOT to Use
- Cómputos baratos: aritmética simple, concatenación de strings, operaciones de arrays pequeños — el overhead del hook excede los ahorros
- Funciones pasadas solo a hijos no memoizados: si el child no usa `React.
- Valores primitivos:
useMemo(() => 42, [])— los primitivos se comparan por valor, no por referencia - Cada variable y función: envolver todo en hooks es cargo-cult programming, no optimización
Solution
useMemo básico para cómputo costoso
import { useMemo, useState } from "react";
function ProductList({ products }) {
const [query, setQuery] = useState("");
const [sort, setSort] = useState("name");
const filteredSorted = useMemo(() => {
const filtered = products.filter((p) =>
p.name.toLowerCase().includes(query.toLowerCase())
);
return filtered.sort((a, b) => {
if (sort === "name") return a.name.localeCompare(b.name);
if (sort === "price") return a.price - b.price;
return 0;
});
}, [products, query, sort]);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<select value={sort} onChange={(e) => setSort(e.target.value)}>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
<ul>
{filteredSorted.map((p) => (
<li key={p.id}>{p.name} — ${p.price}</li>
))}
</ul>
</div>
);
}
Sin useMemo, cada keystroke en el search input re-filtra y re-ordena la lista completa de productos incluso si products no ha cambiado.
useCallback para referencias de función estables
import { useCallback, useState, memo } from "react";
const ExpensiveChild = memo(function ExpensiveChild({ onClick, id }) {
console.log("ExpensiveChild rendered");
return <button onClick={() => onClick(id)}>Click me</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
const handleClick = useCallback((id) => {
console.log("Clicked item", id);
}, []);
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ExpensiveChild onClick={handleClick} id={1} />
</div>
);
}
Sin useCallback, cada render de Parent (disparado por escribir en el input o incrementar count) crea una nueva referencia de handleClick, lo que rompe memo en ExpensiveChild y causa que re-renderice.
Cuando useCallback es innecesario
function Parent() {
const [text, setText] = useState("");
// Este child NO está memoizado — re-renderiza en cada render del parent de todas formas
const handleClick = (id) => {
console.log("Clicked", id);
};
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={() => handleClick(1)}>Click</button>
</div>
);
}
Envolver handleClick en useCallback aquí añade overhead con cero beneficio — el botón no es un componente memoizado.
useMemo para estabilización de context value
import { createContext, useContext, useMemo, useState } from "react";
const UserContext = createContext(null);
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("light");
const value = useMemo(
() => ({ user, setUser, theme, setTheme }),
[user, theme]
);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
function UserProfile() {
const { user } = useContext(UserContext);
return <div>{user?.name}</div>;
}
Sin useMemo, cada render del provider crea un nuevo objeto value, causando que todos los consumers re-rendericen — incluso si solo cambió theme y UserProfile solo usa user.
Medir con React Profiler
import { Profiler } from "react";
function onRender(id, phase, actualDuration) {
console.log(`${id} ${phase} took ${actualDuration}ms`);
}
function App() {
return (
<Profiler id="ProductList" onRender={onRender}>
<ProductList products={largeProductList} />
</Profiler>
);
}
Usa el Profiler para medir tiempos de render reales antes y después de agregar useMemo. Si la duración no mejora, remueve el hook.
useMemo con creación de objeto costoso
function Chart({ data }) {
const chartConfig = useMemo(() => {
return {
scales: {
x: { type: "time" },
y: { min: Math.min(...data.map((d) => d.value)) },
},
plugins: {
tooltip: {
callbacks: {
label: (ctx) => `${ctx.dataset.label}: ${ctx.parsed.y}`,
},
},
},
};
}, [data]);
return <canvas ref={(c) => drawChart(c, chartConfig)} />;
}
useCallback con dependencia de useEffect
function SearchResults({ query, onSearch }) {
const [results, setResults] = useState([]);
const fetchResults = useCallback(async () => {
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
setResults(data);
}, [query]);
useEffect(() => {
fetchResults();
}, [fetchResults]);
return <ResultList results={results} onSearch={onSearch} />;
}
Sin useCallback, fetchResults es una nueva función en cada render, causando que useEffect se re-ejecute en cada render — un infinite loop.
Variants
Usar useMemo para valores debounced
function SearchInput() {
const [input, setInput] = useState("");
const debounced = useMemo(() => {
let timeout;
return (value) => {
clearTimeout(timeout);
timeout = setTimeout(() => setInput(value), 300);
};
}, []);
return <input onChange={(e) => debounced(e.target.value)} />;
}
Custom hook combinando useMemo y useCallback
function useFilteredData(data, filterFn) {
const stableFilter = useCallback(filterFn, [filterFn]);
const filtered = useMemo(() => data.filter(stableFilter), [data, stableFilter]);
const sorted = useMemo(() => [...filtered].sort((a, b) => a.id - b.id), [filtered]);
return sorted;
}
Best Practices
-
For a deeper guide, see Virtualize Long Lists with react-window.
-
Mide antes de optimizar — usa el React Profiler para identificar bottlenecks reales
-
Aplica
useMemoa cómputos que toman más de 1-2ms — los más baratos no valen el overhead del hook -
Aplica
useCallbacksolo cuando la función se pasa a un childReact.memoo se usa como dependencia deuseEffect -
Mantén los dependency arrays completos — omitir una dependencia causa stale closures y bugs
-
No memoices valores primitivos (strings, numbers, booleans) — se comparan por valor
-
Usa
React.memoen children antes de usaruseCallback— memoizar un callback para un child no memoizado es desperdicio
Common Mistakes
- Envolver todo en hooks:
useMemo(() => a + b, [a, b])para suma simple es más lento queconst sum = a + b - Arrays de dependencias vacíos con valores que cambian:
useMemo(() => compute(x), [])—xse captura una vez y nunca se actualiza - Usar useMemo para side effects:
useMemoes para cómputos puros. - No memoizar context values: un nuevo object literal en un context provider causa que todos los consumers re-rendericen
- Memoizar la cosa equivocada: memoizar el resultado de una operación barata mientras se ignora la costosa
Troubleshooting
- Component does not re-render: verify state reference, props, and memoization. A mutated object can bypass change detection.
- Style does not apply in production: check that CSS is loaded, class names are not mangled, and specificity wins. Purge unused styles carefully.
- Build fails after dependency update: read the changelog, pin versions, and clean the lock file.
- Accessibility audit fails: add labels, landmarks, focus management, and color contrast.
- Hydration mismatch: ensure server and client render the same initial HTML. random, or window during SSR.
Puntos Clave
- Aplica cuándo usar usememo y usecallback cuando necesites una solución práctica para tu caso de uso.
- Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
- Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
- Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.
Errores Comunes en Producción
- Copiar el ejemplo sin adaptarlo a volúmenes y modos de fallo reales.
- Saltar tests de carga e inyección de errores antes del primer despliegue productivo.
- Codificar valores fijos que deberían ser configurables por entorno.
- Olvidar agregar logging y monitoreo en cada paso.
- Desplegar sin plan de rollback ni estrategia de backup probada.
- Asumir que el ejemplo mínimo escalará sin agregar caché o procesamiento por lotes.
- No documentar la versión y configuración usadas en producción.
- Dejar la receta sin cambios cuando evolucionan las dependencias o la escala.
Preguntas frecuentes
¿useMemo garantiza que el valor cacheado se reutiliza?
No. React puede descartar valores cacheados para liberar memoria. useMemo es un hint, no una garantía. No dependas de él para correctness — solo para performance.
¿Debería siempre usar React.memo en componentes?
No. React.memo añade una shallow comparison en cada render. Si los props del componente cambian en cada render de todas formas (nuevos objetos, nuevos arrays), memo nunca previene un re-render y la comparación es overhead desperdiciado.
¿Cuál es el overhead de useMemo?
En cada render, React compara cada dependencia con Object.is. Para un hook con 3 dependencias, son 3 comparaciones más el costo de almacenar el valor cacheado. Para cómputos baratos (menos de 1ms), este overhead excede los ahorros.
¿Puedo usar useMemo para operaciones async?
No. useMemo es sincrónico. Para async, usa useEffect con state, o una librería como @tanstack/react-query o SWR.
¿Cómo sé si un cómputo es lo suficientemente costoso para memoizar?
Usa console.time y console.timeEnd alrededor del cómputo, o usa el React Profiler. Si un render toma más de 16ms (un frame a 60fps), busca cómputos costosos para memoizar.
Recursos Relacionados
Virtualizar Listas Largas con react-window
Cómo renderizar listas grandes eficientemente en React usando react-window para virtualización del DOM, incluyendo filas de altura fija y variable y layouts de grid.
RecipeValidación de Formularios con react-hook-form y Zod
Construí formularios type-safe en React usando react-hook-form con validación de esquemas Zod, incluyendo campos anidados, reglas async, campos dinámicos e integración con librerías de UI.
RecipeContainer Queries para Responsiveness de Componentes
Cómo usar CSS container queries para layouts responsivos a nivel de componente que se adaptan al tamaño de su contenedor en lugar del viewport.
RecipeDark Mode con prefers-color-scheme y CSS Variables
Cómo implementar dark mode usando el media query CSS prefers-color-scheme, CSS custom properties y toggle manual con persistencia en localStorage.
RecipeState Management Reactivo con Svelte Stores
Cómo manejar estado reactivo en Svelte usando writable, readable, derived stores y custom stores con actualizaciones basadas en contrato.
RecipeType Checking Exhaustivo con Discriminated Unions
Cómo usar discriminated unions de TypeScript para type checking exhaustivo, asegurando que todos los casos se manejen en compile time con aserciones de tipo never.