Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { memo, useMemo, useCallback, useState } from "react";
interface Product {
id: string;
name: string;
price: number;
category: string;
}
// 1. useMemo - cachea un cálculo costoso
function ProductFilter({ products, category }: { products: Product[]; category: string }) {
const filtered = useMemo(
() => products.filter((p) => p.category === category).sort((a, b) => a.price - b.price),
[products, category]
);
return <ProductList items={filtered} />;
}
// 2. useCallback - estabiliza la referencia de función para hijos memoizados
function ProductPage() {
const [selected, setSelected] = useState<string | null>(null);
const handleSelect = useCallback((id: string) => {
setSelected(id);
}, []);
return <MemoizedProductGrid onSelect={handleSelect} />;
}
// 3. React.memo - omite re-renderizado cuando los props no cambian
const MemoizedProductGrid = memo(function ProductGrid({
onSelect,
}: {
onSelect: (id: string) => void;
}) {
return <div>{/* renderizado costoso de grid */}</div>;
});Cuándo usarlo: Después de hacer perfiles y descubrir que un cálculo específico o un re-renderizado de componente está causando lentitud medible. Nunca por defecto - React Compiler maneja la mayoría de la memoización automáticamente en React 19+.
// ---- ANTES: Sin memoización - filtrado de 10,000 productos en cada pulsación de tecla ----
function StorePage() {
const [products] = useState<Product[]>(generateProducts(10_000));
const [search, setSearch] = useState("");
const [sortBy, setSortBy] = useState<"price" | "name">("name");
const [cartCount, setCartCount] = useState(0);
// Recompilado en CADA renderizado, incluyendo actualizaciones del carrito (cuesta ~45ms)
const results = products
.filter((p) => p.name.toLowerCase().includes(search.toLowerCase()))
.sort((a, b) =>
sortBy === "price" ? a.price - b.price : a.name.localeCompare(b.name)
);
return (
<div>
<header>Carrito: {cartCount}</header>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
<button onClick={() => setSortBy(sortBy === "price" ? "name" : "price")}>
Ordenar por {sortBy === "price" ? "nombre" : "precio"}
</button>
<button onClick={() => setCartCount((c) => c + 1)}>Agregar al carrito</button>
<ul>
{results.map((p) => (
<ProductRow
key={p.id}
product={p}
onAddToCart={() => setCartCount((c) => c + 1)} // Nueva función en cada renderizado
/>
))}
</ul>
</div>
);
}
function ProductRow({
product,
onAddToCart,
}: {
product: Product;
onAddToCart: () => void;
}) {
return (
<li>
{product.name} - ${product.price}
<button onClick={onAddToCart}>Agregar</button>
</li>
);
}
// ---- DESPUÉS: Memoizado - el filtrado solo se ejecuta cuando search o sortBy cambian ----
function StorePageOptimized() {
const [products] = useState<Product[]>(generateProducts(10_000));
const [search, setSearch] = useState("");
const [sortBy, setSortBy] = useState<"price" | "name">("name");
const [cartCount, setCartCount] = useState(0);
// Solo se recomputa cuando search, sortBy o products cambian (~45ms ahorrados en clics de carrito)
const results = useMemo(
() =>
products
.filter((p) => p.name.toLowerCase().includes(search.toLowerCase()))
.sort((a, b) =>
sortBy === "price" ? a.price - b.price : a.name.localeCompare(b.name)
),
[products, search, sortBy]
);
const handleAddToCart = useCallback(() => {
setCartCount((c) => c + 1);
}, []);
return (
<div>
<header>Carrito: {cartCount}</header>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
<button onClick={() => setSortBy(sortBy === "price" ? "name" : "price")}>
Ordenar por {sortBy === "price" ? "nombre" : "precio"}
</button>
<button onClick={handleAddToCart}>Agregar al carrito</button>
<ul>
{results.map((p) => (
<MemoizedProductRow
key={p.id}
product={p}
onAddToCart={handleAddToCart}
/>
))}
</ul>
</div>
);
}
const MemoizedProductRow = memo(function ProductRow({
product,
onAddToCart,
}: {
product: Product;
onAddToCart: () => void;
}) {
return (
<li>
{product.name} - ${product.price}
<button onClick={onAddToCart}>Agregar</button>
</li>
);
});Lo que esto demuestra:
useMemo cachea el resultado de filtro+ordenamiento: hacer clic en "Agregar al carrito" ya no recomputa 10,000 elementos (ahorra ~45ms por clic)useCallback estabiliza handleAddToCart para que MemoizedProductRow pueda omitir re-renderizadosmemo en ProductRow previene que 10,000 elementos de lista se re-rendericen cuando solo cambia el conteo del carritouseMemo(fn, deps) llama fn en el montaje y cachea el resultado. En renderizados posteriores, compara cada dependencia (usando Object.is) contra los valores anteriores. Si todas coinciden, devuelve el valor cacheado sin llamar fn. Si alguna cambió, llama fn de nuevo y cachea el nuevo resultado.useCallback(fn, deps) es azúcar sintáctico para useMemo(() => fn, deps). Cachea la referencia de la función misma en lugar del valor de retorno de la función. Esto mantiene la referencia estable entre renderizados mientras las dependencias no cambien.React.memo(Component, areEqual?) envuelve un componente para que React realice una comparación superficial de props antes de renderizar. Si todos los props son superficialmente iguales al renderizado anterior, React omite el componente y reutiliza la última salida renderizada. Una función areEqual opcional puede personalizar la comparación.{ a: 1 } y { a: 1 } no son superficialmente iguales porque son referencias diferentes, aunque su contenido coincida.useMemo y useCallback son pistas de rendimiento, no garantías semánticas. React puede descartar valores cacheados bajo presión de memoria o durante futuras características concurrentes.useMemo para igualdad referencial (no solo cálculos costosos):
// Sin useMemo: nueva referencia de array dispara useEffect en hijo
function Parent() {
const config = { theme: "dark", locale: "en" }; // Nuevo objeto en cada renderizado
return <Dashboard config={config} />;
}
// Con useMemo: referencia estable previene ejecuciones de efecto innecesarias
function Parent() {
const config = useMemo(() => ({ theme: "dark", locale: "en" }), []);
return <Dashboard config={config} />;
}Función de comparación personalizada con memo:
const ChartWidget = memo(
function ChartWidget({ data, title }: { data: number[]; title: string }) {
return <canvas>{/* renderizado costoso de gráfico */}</canvas>;
},
(prev, next) => {
// Omite re-renderizado si la longitud del array y el título son los mismos
return prev.data.length === next.data.length && prev.title === next.title;
}
);Cuándo NO memoizar:
// MAL: memoizar un cálculo trivial
const greeting = useMemo(() => `Hello, ${name}!`, [name]);
// BIEN: solo calcúlalo - la concatenación de strings es casi instantánea
const greeting = `Hello, ${name}!`;
// MAL: memoizar una primitiva que nunca cambia de referencia
const TAX_RATE = useMemo(() => 0.08, []);
// BIEN: solo usa una constante
const TAX_RATE = 0.08;useMemo<T> infiere T del tipo de retorno de la función factory. La anotación explícita raramente es necesaria.useCallback infiere la firma de la función. TypeScript refuerza que el tipo de callback coincida con lo que los hijos esperan.memo(GenericList) as typeof GenericList.areEqual en memo recibe (prevProps: Readonly<Props>, nextProps: Readonly<Props>).Memoizar sin estabilizar dependencias - useMemo recalcula si alguna dependencia es una nueva referencia. Pasar un objeto inline como dependencia anula el caché. Solución: Asegúrate de que todas las dependencias sean primitivas o referencias estables.
useCallback con dependencias faltantes - Omitir una dependencia de useCallback crea un cierre obsoleto que captura valores de estado antiguos. Solución: Siempre incluye todos los valores referenciados. Habilita la regla ESLint react-hooks/exhaustive-deps.
memo en componentes que reciben children - children es un nuevo árbol de elementos React en cada renderizado, por lo que memo es inefectivo para componentes que reciben children. Solución: Ya sea memoiza los children en el nivel padre o reestructura para evitar pasar children a componentes memoizados.
Sobre-memoizar - Envolver cada componente en memo y cada valor en useMemo agrega sobrecarga de comparación que ralentiza componentes simples. Solución: Haz perfiles primero. Solo memoiza componentes que el Profiler muestra que son costosos (más de 5ms de tiempo de renderizado) y se están re-renderizando innecesariamente.
React Compiler hace obsoleta la mayoría de la memoización manual - En React 19+ con el compiler habilitado, useMemo, useCallback y memo se aplican automáticamente en tiempo de compilación. Solución: Si usas React Compiler, elimina la memoización manual y solo agrega de vuelta si los perfiles muestran que el compiler perdió un caso.
useMemo no garantiza que el caché persista - React documenta que useMemo es una pista, no una garantía. Las futuras versiones de React pueden descartar cachés más agresivamente. Solución: Nunca confíes en useMemo para la corrección, solo para el rendimiento. Tu código debe funcionar correctamente incluso si cada useMemo se recomputa en cada renderizado.
| Enfoque | Compensación |
|---|---|
| React Compiler | Memoización automática; sin hooks manuales necesarios; requiere React 19+ |
| Colocación de estado | Mueve el estado más cerca del uso; evita la necesidad de memoizar en primer lugar |
useDeferredValue | Difiere actualizaciones en lugar de cachear; muestra datos obsoletos brevemente |
| Virtualización | Renderiza solo elementos visibles; más efectivo que memo para listas grandes |
| Web Workers | Descarga computación pesada fuera del hilo principal; transferencia de datos compleja |
| Selectores de Zustand | Suscripciones de estado de grano fino sin memo o context |
De una aplicación SaaS de producción Next.js 15 / React 19 (SystemsArchitect.io).
// Ejemplo de producción: SavePointCheckbox con comparación memo personalizada
// Archivo: src/components/study/save-point-checkbox.tsx
'use client';
import React, { memo } from 'react';
interface SavePointCheckboxProps {
pointId: string;
isSaved: boolean;
onSavedChange: (pointId: string, isSaved: boolean) => void;
}
const SavePointCheckbox = memo(
function SavePointCheckbox({ pointId, isSaved, onSavedChange }: SavePointCheckboxProps) {
return (
<input
type="checkbox"
checked={isSaved}
onChange={(e) => onSavedChange(pointId, e.target.checked)}
aria-label="Guardar este punto"
/>
);
},
(prev, next) => {
// Comparación personalizada: ignora la identidad del callback, compara solo props de datos
return prev.pointId === next.pointId && prev.isSaved === next.isSaved;
}
);// Ejemplo de producción: Hook de Quiz con valor de retorno memoizado
// Archivo: src/hooks/use-quiz.ts
import { useState, useMemo, useCallback } from 'react';
export function useQuiz(questions: Question[]) {
const [currentIndex, setCurrentIndex] = useState(0);
const [answers, setAnswers] = useState<Map<string, string>>(new Map());
const currentQuestion = useMemo(
() => questions[currentIndex],
[questions, currentIndex]
);
const submitAnswer = useCallback(
(questionId: string, answer: string) => {
setAnswers((prev) => new Map(prev).set(questionId, answer));
},
[]
);
const goToNext = useCallback(() => {
setCurrentIndex((prev) => Math.min(prev + 1, questions.length - 1));
}, [questions.length]);
return useMemo(() => ({
currentQuestion,
submitAnswer,
goToNext,
progress: currentIndex / questions.length,
}), [currentQuestion, submitAnswer, goToNext, currentIndex, questions.length]);
}Lo que esto demuestra en producción:
SavePointCheckbox fue necesaria porque el padre recrea el callback onSavedChange en cada renderizado. Sin el comparador personalizado, cada checkbox en una lista de 50+ elementos se re-rendería siempre que cualquier checkbox cambiara.onSavedChange. Esto es seguro porque el callback siempre tiene el mismo comportamiento (llama a la misma acción de store) aunque su referencia cambie.useQuiz, el useMemo en el objeto de retorno es crítico. Sin él, cada componente que consume este hook recibiría una nueva referencia de objeto en cada renderizado, anulando cualquier envoltorio memo downstream.submitAnswer usa useCallback con un array de dependencias vacío porque solo llama setAnswers con un updater funcional. La función updater siempre recibe el estado más reciente, por lo que no se necesitan dependencias externas.new Map(prev).set(questionId, answer) crea una nueva referencia de Map para que React detecte el cambio de estado. Mutar el Map existente con prev.set(...) no dispararía un re-renderizado.useMemo(fn, deps) cachea el valor de retorno de fn.useCallback(fn, deps) cachea la referencia de función misma.useCallback(fn, deps) es equivalente a useMemo(() => fn, deps).const).children es un nuevo árbol de elementos React en cada renderizado, creando una nueva referencia cada vez.memo ve una referencia diferente y se re-renderiza de todas formas.const MyComponent = memo(
function MyComponent({ data, title }: Props) {
return <div>{title}: {data.length} elementos</div>;
},
(prev, next) => {
// Devuelve true para OMITIR re-renderizado, false para re-renderizar
return prev.data.length === next.data.length
&& prev.title === next.title;
}
);useMemo ve una dependencia diferente cada vez y recalcula, anulando el caché.useMemo/useCallback).useMemo se recomputa en cada renderizado.useMemo para la corrección -- solo para optimización de rendimiento.useMemo, useCallback y memo se aplican automáticamente en tiempo de compilación.useCallback captura valores de estado antiguos en el cierre.react-hooks/exhaustive-deps.// React.memo pierde genéricos, así que usa una aserción de tipo
function GenericList<T>({ items }: { items: T[] }) {
return <ul>{items.map((item, i) => <li key={i}>{String(item)}</li>)}</ul>;
}
const MemoizedList = memo(GenericList) as typeof GenericList;
// Uso: <MemoizedList<number> items={[1, 2, 3]} />(prevProps: Readonly<Props>, nextProps: Readonly<Props>) => booleantrue para omitir re-renderizado, false para permitirlo.Readonly -- no puedes mutar props dentro del comparador.useMemo en el valor de retorno, cada consumidor recibe una nueva referencia de objeto en cada renderizado.memo downstream.return useMemo(() => ({
currentQuestion,
submitAnswer,
progress,
}), [currentQuestion, submitAnswer, progress]);Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥