Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// BEFORE: Every keystroke re-renders the entire product list (47 renders)
function ProductPage() {
const [search, setSearch] = useState("");
const [products] = useState<Product[]>(initialProducts);
return (
<div>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
<ProductList products={products} /> {/* Re-renders on every keystroke */}
<Footer /> {/* Also re-renders unnecessarily */}
</div>
);
}
// AFTER: Only SearchBar re-renders on keystroke (3 renders)
function ProductPage() {
const [products] = useState<Product[]>(initialProducts);
return (
<div>
<SearchBar /> {/* State colocated here */}
<ProductList products={products} /> {/* No longer re-renders */}
<Footer /> {/* No longer re-renders */}
</div>
);
}
function SearchBar() {
const [search, setSearch] = useState("");
return <input value={search} onChange={(e) => setSearch(e.target.value)} />;
}Cuándo usarlo: Cuando el Profiler de React DevTools muestra componentes parpadeando en cada interacción, o cuando la interfaz se siente lenta durante escritura, desplazamiento o cambio de pestaña. Siempre perfila primero para confirmar el problema antes de aplicar soluciones.
// ---- BEFORE: Slow list with unnecessary re-renders ----
interface Task {
id: string;
title: string;
completed: boolean;
}
// Problem: TaskItem re-renders for ALL items when any state changes
function TaskApp() {
const [tasks, setTasks] = useState<Task[]>(generateTasks(500));
const [filter, setFilter] = useState("all");
const [newTitle, setNewTitle] = useState("");
const toggleTask = (id: string) => {
setTasks((prev) =>
prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
);
};
const filtered = tasks.filter((t) => {
if (filter === "done") return t.completed;
if (filter === "todo") return !t.completed;
return true;
});
return (
<div>
{/* Typing here re-renders all 500 TaskItems */}
<input
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="New task..."
/>
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="done">Done</option>
<option value="todo">Todo</option>
</select>
<ul>
{filtered.map((task) => (
<TaskItem
key={task.id}
task={task}
onToggle={() => toggleTask(task.id)} // New function every render!
/>
))}
</ul>
</div>
);
}
function TaskItem({ task, onToggle }: { task: Task; onToggle: () => void }) {
// Simulated expensive render
const start = performance.now();
while (performance.now() - start < 1) {} // 1ms per item = 500ms total
return (
<li onClick={onToggle} style={{ textDecoration: task.completed ? "line-through" : "none" }}>
{task.title}
</li>
);
}
// ---- AFTER: Optimized - reduces re-renders from 500 to ~1-3 per interaction ----
function TaskAppOptimized() {
const [tasks, setTasks] = useState<Task[]>(generateTasks(500));
const [filter, setFilter] = useState("all");
const toggleTask = useCallback((id: string) => {
setTasks((prev) =>
prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
);
}, []);
const filtered = useMemo(
() =>
tasks.filter((t) => {
if (filter === "done") return t.completed;
if (filter === "todo") return !t.completed;
return true;
}),
[tasks, filter]
);
return (
<div>
{/* State colocated - typing no longer re-renders the list */}
<NewTaskInput
onAdd={(title) =>
setTasks((prev) => [...prev, { id: crypto.randomUUID(), title, completed: false }])
}
/>
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="done">Done</option>
<option value="todo">Todo</option>
</select>
<ul>
{filtered.map((task) => (
<MemoizedTaskItem key={task.id} task={task} onToggle={toggleTask} />
))}
</ul>
</div>
);
}
// Colocated input state - isolated from the list
function NewTaskInput({ onAdd }: { onAdd: (title: string) => void }) {
const [title, setTitle] = useState("");
return (
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && title.trim()) {
onAdd(title.trim());
setTitle("");
}
}}
placeholder="New task..."
/>
);
}
// Memoized item - only re-renders when its own task changes
const MemoizedTaskItem = memo(function TaskItem({
task,
onToggle,
}: {
task: Task;
onToggle: (id: string) => void;
}) {
const start = performance.now();
while (performance.now() - start < 1) {}
return (
<li
onClick={() => onToggle(task.id)}
style={{ textDecoration: task.completed ? "line-through" : "none" }}
>
{task.title}
</li>
);
});Lo que esto demuestra:
NewTaskInput evita que 500 elementos se re-renderizen en cada pulsación de teclaReact.memo en MemoizedTaskItem omite re-renderizados cuando los props task y onToggle no cambianuseCallback en toggleTask estabiliza la referencia de función para que memo funcioneuseMemo en filtered evita recomputar el filtro en cambios de estado no relacionadossetState, React re-renderiza ese componente y todos sus descendientes. Este es el disparador de re-renderizado más común.useContext(SomeContext) se re-renderiza siempre que cambie el valor del contexto, independientemente de si cambió el campo específico que utiliza.onClick={() => handleClick(id)} crea una nueva función en cada renderizado. Si el hijo está envuelto en memo, esta nueva referencia anula la optimización.style={{ color: "red" }} o data={{ items }} crea una nueva referencia de objeto en cada renderizado, rompiendo la comparación superficial en memo.Patrón de hijos para prevenir re-renderizados:
// BEFORE: SlowChild re-renders when count changes
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<SlowChild />
</div>
);
}
// AFTER: SlowChild passed as children, won't re-render
function CounterWrapper({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
{children}
</div>
);
}
function Parent() {
return (
<CounterWrapper>
<SlowChild /> {/* Created in Parent scope, not CounterWrapper */}
</CounterWrapper>
);
}Dividiendo componentes para aislar estado:
// BEFORE: Hover state re-renders the entire card
function ProductCard({ product }: { product: Product }) {
const [isHovered, setIsHovered] = useState(false);
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<ExpensiveChart data={product.data} />
<HoverOverlay visible={isHovered} />
</div>
);
}
// AFTER: Hover state isolated in its own component
function ProductCard({ product }: { product: Product }) {
return (
<div>
<ExpensiveChart data={product.data} />
<HoverArea />
</div>
);
}
function HoverArea() {
const [isHovered, setIsHovered] = useState(false);
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<HoverOverlay visible={isHovered} />
</div>
);
}React.memo preserva tipos de props. Para componentes genéricos, lanza después de envolver: memo(MyComponent) as typeof MyComponent.useCallback es verificado por TypeScript y la regla ESLint react-hooks/exhaustive-deps.React.ReactNode para máxima flexibilidad.Agregar memo en todas partes sin perfilar - memo tiene sobrecarga (comparación superficial en cada renderizado). Para componentes simples que se renderizen rápidamente, el costo de comparación excede el costo de renderizado. Solución: Perfila primero. Solo envuelve componentes que el Profiler muestre que son costosos y se re-renderizen innecesariamente.
Claves inestables causando remontajes - Usar el índice del array como key o generar claves con Math.random() fuerza a React a desmontar y remontar componentes, lo cual es más costoso que un re-renderizado. Solución: Usa IDs únicos y estables de tus datos.
Olvidar useCallback al pasar manejadores a hijos memoizados - memo en el hijo es inútil si el padre pasa una nueva referencia de función en cada renderizado. Solución: Envuelve funciones de manejador en useCallback o reestructura para que el hijo posea el manejador.
El contexto causando re-renderizados globales - Un único contexto con estado y dispatch causa que todos los consumidores se re-renderizen en cualquier cambio de estado. Solución: Divide en StateContext y DispatchContext separados, o usa Zustand con selectores.
Expandir props anulando memo - <Child {...props} /> donde props se reconstruye en cada renderizado crea nuevas referencias de objeto. Solución: Pasa props individuales o memoiza el objeto props.
| Enfoque | Compensación |
|---|---|
| Colocation de estado | Solución más simple; puede requerir reestructurar el árbol de componentes |
React.memo + useCallback | Control preciso; agrega boilerplate |
| Patrón de hijos | Cero sobrecarga; solo funciona cuando el componente propietario de estado envuelve el contenido |
| React Compiler | Automático; experimental, no aún estable |
| Selectores de Zustand | Reemplaza contexto; añade dependencia |
| Virtualización | Solo renderiza elementos visibles; cambia comportamiento de desplazamiento |
useDeferredValue | Mantiene la entrada responsiva; muestra contenido obsoleto brevemente |
Un componente padre que se re-renderiza causa que todos sus hijos se re-renderizen, incluso si sus props no han cambiado. Este es el comportamiento predeterminado de React y la fuente principal de renderizados desperdiciados.
Al mover el estado al componente que realmente lo usa, los componentes padre ya no se re-renderizen cuando ese estado cambia. Por ejemplo, mover el estado de entrada de búsqueda a un componente SearchBar evita que el hermano ProductList se re-renderice en cada pulsación de tecla.
Las funciones en línea como onClick={() => handleClick(id)} crean una nueva referencia de función en cada renderizado. memo usa comparación superficial, así que ve un nuevo prop y re-renderiza el hijo de todas formas.
Solución: Envuelve el manejador en useCallback o reestructura para que el hijo posea el manejador.
function CounterWrapper({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
{children}
</div>
);
}Los hijos se crean en el ámbito padre, no dentro de CounterWrapper, por lo que no se re-renderizen cuando count cambia. Úsalo cuando un componente propietario de estado envuelve contenido que no necesita controlar.
Sí. Usar el índice del array como key o generar claves con Math.random() fuerza a React a desmontar y remontar componentes en lugar de actualizarlos. El remontaje es más costoso que un re-renderizado. Siempre usa IDs únicos y estables de tus datos.
Cualquier componente que llame a useContext(SomeContext) se re-renderiza siempre que cambie el valor del contexto, independientemente de qué campo cambió. Si el estado y dispatch comparten un contexto, cada cambio de estado re-renderiza todos los consumidores, incluyendo aquellos que solo hacen dispatch.
Solución: Divide en StateContext y DispatchContext, o usa Zustand con selectores.
Sí. <Child {...props} /> donde props se reconstruye en cada renderizado crea nuevas referencias de objeto cada vez, haciendo que memo siempre vea props "cambiados". Pasa props individuales o memoiza el objeto props en su lugar.
function Wrapper({ children }: { children: React.ReactNode }) {
return <div>{children}</div>;
}Usa React.ReactNode para máxima flexibilidad. Acepta elementos, strings, números, fragmentos, portales y null.
React.memo preserva tipos de props, pero para componentes genéricos, necesitas una aserción de tipo después de envolver:
const MemoizedList = memo(MyList) as typeof MyList;Sin la aserción, se pierde el parámetro de tipo genérico.
Siempre perfila primero para confirmar que el componente es tanto costoso como se re-renderiza innecesariamente.
useDeferredValue aún re-renderiza el componente lento pero con menor prioridad, manteniendo la entrada responsiva mientras muestra contenido obsoleto brevementeLa colocation de estado es más simple cuando es posible; useDeferredValue es útil cuando el componente lento genuinamente depende del estado cambiante.
Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥