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, useTransition } from "react";
// Memoiza componente caro
const ExpensiveList = memo(function ExpensiveList({
items,
onSelect,
}: {
items: Item[];
onSelect: (id: string) => void;
}) {
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
// Padre: estabiliza props pasados a hijos memoizados
function Parent() {
const [items] = useState<Item[]>(initialItems);
const [selected, setSelected] = useState<string | null>(null);
// Referencia de callback estable
const handleSelect = useCallback((id: string) => {
setSelected(id);
}, []);
return <ExpensiveList items={items} onSelect={handleSelect} />;
}Cuándo usarlo: Solo después de que el perfilado revele un problema de rendimiento. La optimización prematura añade complejidad sin ganancia. Usa el Profiler de React DevTools para medir primero, luego aplica memo, useMemo o useCallback donde realmente ayudan.
import {
memo,
useMemo,
useCallback,
useState,
useTransition,
useDeferredValue,
} from "react";
// --- Lista virtualizada con filas memoizadas ---
interface Contact {
id: string;
name: string;
email: string;
avatar: string;
}
const ContactRow = memo(function ContactRow({
contact,
isSelected,
onSelect,
}: {
contact: Contact;
isSelected: boolean;
onSelect: (id: string) => void;
}) {
return (
<div
onClick={() => onSelect(contact.id)}
className={`flex items-center gap-3 p-3 cursor-pointer ${
isSelected ? "bg-blue-50 border-l-4 border-blue-500" : "hover:bg-gray-50"
}`}
>
<img
src={contact.avatar}
alt=""
className="w-10 h-10 rounded-full"
loading="lazy"
/>
<div>
<p className="font-medium">{contact.name}</p>
<p className="text-sm text-gray-500">{contact.email}</p>
</div>
</div>
);
});
// --- Búsqueda con valor diferido ---
function ContactList({ contacts }: { contacts: Contact[] }) {
const [query, setQuery] = useState("");
const [selectedId, setSelectedId] = useState<string | null>(null);
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
// El filtrado caro solo se vuelve a ejecutar cuando deferredQuery o contacts cambian
const filtered = useMemo(
() =>
contacts.filter(
(c) =>
c.name.toLowerCase().includes(deferredQuery.toLowerCase()) ||
c.email.toLowerCase().includes(deferredQuery.toLowerCase())
),
[contacts, deferredQuery]
);
const handleSelect = useCallback((id: string) => {
setSelectedId(id);
}, []);
return (
<div className="max-w-md border rounded-lg overflow-hidden">
<div className="p-3 border-b">
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Buscar contactos..."
className="w-full px-3 py-2 border rounded"
/>
</div>
<div
className={`max-h-96 overflow-y-auto ${isStale ? "opacity-60" : ""}`}
>
{filtered.length === 0 ? (
<p className="p-4 text-gray-500 text-center">No se encontraron contactos</p>
) : (
filtered.map((contact) => (
<ContactRow
key={contact.id}
contact={contact}
isSelected={contact.id === selectedId}
onSelect={handleSelect}
/>
))
)}
</div>
<div className="p-2 border-t text-sm text-gray-400 text-center">
{filtered.length} de {contacts.length} contactos
</div>
</div>
);
}
// --- Transición para cambio de pestaña ---
function TabPanel({ tabs }: { tabs: { id: string; label: string; content: () => JSX.Element }[] }) {
const [activeTab, setActiveTab] = useState(tabs[0].id);
const [isPending, startTransition] = useTransition();
const handleTabChange = (tabId: string) => {
startTransition(() => {
setActiveTab(tabId);
});
};
const ActiveContent = tabs.find((t) => t.id === activeTab)?.content;
return (
<div>
<div className="flex border-b">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => handleTabChange(tab.id)}
className={`px-4 py-2 -mb-px ${
activeTab === tab.id
? "border-b-2 border-blue-500 font-medium"
: "text-gray-500"
}`}
>
{tab.label}
</button>
))}
</div>
<div className={`p-4 ${isPending ? "opacity-50 transition-opacity" : ""}`}>
{ActiveContent && <ActiveContent />}
</div>
</div>
);
}Lo que esto demuestra:
memo en ContactRow evita re-renderizados cuando los props no han cambiadouseCallback estabiliza handleSelect para que no rompa memouseDeferredValue mantiene el input de búsqueda receptivo mientras el filtrado se difiereuseMemo cachea la lista filtrada para evitar recomputar en cada pulsación de teclauseTransition marca el cambio de pestaña como no urgente, manteniendo las interacciones fluidasReact.memo(Component) envuelve un componente para que solo se re-renderice cuando sus props cambian (comparación superficial por defecto).useMemo(fn, deps) cachea un valor computado y solo recomputa cuando las dependencias cambian.useCallback(fn, deps) cachea una referencia de función - equivalente a useMemo(() => fn, deps).useDeferredValue(value) devuelve una copia diferida que se atrasa durante actualizaciones urgentes, permitiendo que el input permanezca receptivo.useTransition envuelve actualizaciones de estado como no urgentes, permitiendo que React interrumpa el renderizado si llega una actualización más urgente.memo/useMemo/useCallback manual.| API | Input | Output | Comportamiento de Re-renderizado |
|---|---|---|---|
memo(Component, areEqual?) | Componente, comparador opcional | Componente memoizado | Evita renderizado si props son superficialmente iguales |
useMemo(fn, deps) | Función factory, array de dependencias | Valor cacheado de tipo T | Recomputa solo cuando las deps cambian |
useCallback(fn, deps) | Función, array de dependencias | Referencia de función estable | Nueva referencia solo cuando las deps cambian |
useDeferredValue(value) | Cualquier valor | Copia diferida del valor | Se actualiza con menor prioridad |
useTransition() | Ninguno | [isPending, startTransition] | Marca actualizaciones como interrumpibles |
Comparación personalizada para memo:
const Chart = memo(
function Chart({ data, config }: ChartProps) {
// Renderizado caro
return <canvas />;
},
(prev, next) => {
// Solo re-renderiza si la longitud de data o config cambian
return (
prev.data.length === next.data.length &&
prev.config.type === next.config.type
);
}
);Perfilado con React DevTools:
import { Profiler, type ProfilerOnRenderCallback } from "react";
const onRender: ProfilerOnRenderCallback = (
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) => {
console.table({ id, phase, actualDuration, baseDuration });
};
function App() {
return (
<Profiler id="ContactList" onRender={onRender}>
<ContactList contacts={contacts} />
</Profiler>
);
}Evitar re-renderizados con el patrón children:
// Esto hace que SearchResults se re-renderice cuando count cambia:
function BadParent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<SearchResults /> {/* Se re-renderiza innecesariamente */}
</div>
);
}
// Solución: eleva el estado a un componente más pequeño
function GoodParent() {
return (
<div>
<Counter /> {/* count state delimitado aquí */}
<SearchResults /> {/* Ya no se re-renderiza */}
</div>
);
}
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}memo preserva los tipos de prop del componente. Los componentes genéricos requieren un wrapper: memo(Component) as typeof Component.useMemo<T> y useCallback infieren tipos de la función factory. Las anotaciones de tipo explícito raramente son necesarias.areEqual en memo recibe (prevProps, nextProps) y debe devolver true para evitar re-renderizado.Optimización prematura - Añadir memo, useMemo y useCallback por todas partes añade complejidad y puede ralentizar componentes rápidos debido a sobrecarga de comparación. Solución: Perfila primero con React DevTools. Solo optimiza componentes que muestren costo de re-renderizado medible.
Referencias de prop inestables que rompen memo - Pasar onClick={() => {}} o style={{ color: "red" }} inline crea nuevas referencias cada renderizado, anulando memo. Solución: Usa useCallback para funciones y useMemo para objetos/arrays pasados a hijos memoizados.
Falta de dependencia en useMemo/useCallback - Omitir una dependencia lleva a cierres obsoletos. Solución: Incluye todos los valores referenciados en el array de dependencias. Usa la regla ESLint react-hooks/exhaustive-deps.
Memoizar computaciones baratas - useMemo para concatenación de cadenas simple cuesta más que simplemente recomputarla. Solución: Solo memoiza computaciones que sean realmente caras (filtrado de arrays grandes, cálculos complejos, crear objetos para contexto).
Prop key reseteando memo - Cambiar la key de un componente hace que React lo desmonte y remonte, sin importar memo. Solución: Usa claves estables (IDs, no índices de array) a menos que quieras intencionalmente un reseteo.
Árboles de componentes grandes sin virtualización - Memoizar 10,000 elementos de lista es más lento que virtualizar para renderizar solo elementos visibles. Solución: Usa @tanstack/react-virtual, react-window, o CSS content-visibility: auto para listas grandes.
| Enfoque | Compensación |
|---|---|
React.memo + useCallback | Manual pero preciso; boilerplate |
| React Compiler (experimental) | Memoización automática; experimental, no listo para producción |
| Virtualización | Renderiza solo elementos visibles; comportamiento de scrolling más complejo |
| State colocation | Mueve estado más cerca de donde se usa; solución más simple |
useDeferredValue | Mantiene la UI receptiva; muestra datos anticuados brevemente |
CSS content-visibility | Renderizado lazy nativo del navegador; no se necesita API de React |
| Web Workers | Descarga computación; transferencia de datos compleja |
useState o useReducer.memo).const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const filtered = useMemo(
() => contacts.filter((c) => c.name.includes(deferredQuery)),
[contacts, deferredQuery]
);query, pero deferredQuery se atrasa.isStale puede usarse para mostrar un indicador visual (p. ej., opacidad reducida) mientras el filtrado se pone al día.useTransition envuelve una actualización de estado para marcarla como no urgente, manteniendo la UI actual visible.useDeferredValue crea una copia diferida de un valor que se actualiza con menor prioridad.useTransition cuando controlas la actualización de estado. Usa useDeferredValue cuando recibes un valor de props.onClick={() => {}} o style={{ color: "red" }} crea una nueva referencia cada renderizado.React.memo compara props por referencia, por lo que ve un "cambio" y se re-renderiza.useCallback para funciones y useMemo para objetos/arrays pasados a hijos memoizados.useMemo tiene sobrecarga: almacena el valor anterior, compara dependencias y gestiona memoria.// Malo: SearchResults se re-renderiza cuando count cambia
function BadParent() {
const [count, setCount] = useState(0);
return <div><button onClick={() => setCount(c => c + 1)}>{count}</button><SearchResults /></div>;
}
// Bueno: mueve estado a un componente más pequeño
function GoodParent() {
return <div><Counter /><SearchResults /></div>;
}SearchResults ya no se re-renderiza porque su padre (GoodParent) no posee el estado que está cambiando.memo(Component) puede perder parámetros de tipo genérico.memo(Component) as typeof Component.const Chart = memo(
function Chart({ data, config }: ChartProps) { return <canvas />; },
(prev, next) => prev.data.length === next.data.length && prev.config.type === next.config.type
);(prevProps, nextProps) y devuelve true para evitar re-renderizado, false para re-renderizar.@tanstack/react-virtual y react-window renderizan solo elementos en el viewport.content-visibility: auto proporciona renderizado lazy nativo del navegador sin biblioteca de React.memo, useMemo y useCallback manual sean innecesarios.useTransition y useDeferredValue para renderizado asincrónicoRevisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥