Hook useCallback
Cachea una definición de función entre renderizados para mantener una referencia estable.
Busca en todas las páginas de la documentación
Cachea una definición de función entre renderizados para mantener una referencia estable.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
const handleClick = useCallback(() => {
doSomething(a, b);
}, [a, b]);
// Patrón común: estabilizar un callback pasado a un hijo memoizado
const handleDelete = useCallback((id: string) => {
setItems((prev) => prev.filter((item) => item.id !== id));
}, []);Cuándo usarlo: Pasas una función como prop a un componente hijo React.memo, o como dependencia en un useEffect o useMemo, y necesitas que no cambie en cada renderizado.
"use client";
import { memo, useCallback, useState } from "react";
interface TodoItemProps {
id: number;
text: string;
onDelete: (id: number) => void;
}
const TodoItem = memo(function TodoItem({ id, text, onDelete }: TodoItemProps) {
console.log(`Renderizando: ${text}`);
return (
<li className="flex items-center justify-between py-1">
<span>{text}</span>
<button onClick={() => onDelete(id)} className="text-red-500 text-sm">
Eliminar
</button>
</li>
);
});
export function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: "Aprender React" },
{ id: 2, text: "Construir una aplicación" },
{ id: 3, text: "Lanzarla" },
]);
const [input, setInput] = useState("");
const handleDelete = useCallback((id: number) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
}, []);
const handleAdd = useCallback(() => {
if (!input.trim()) return;
setTodos((prev) => [...prev, { id: Date.now(), text: input }]);
setInput("");
}, [input]);
return (
<div className="space-y-2">
<div className="flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
className="border rounded px-2 py-1 flex-1"
placeholder="Nueva tarea"
/>
<button onClick={handleAdd} className="px-3 py-1 border rounded">
Agregar
</button>
</div>
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} id={todo.id} text={todo.text} onDelete={handleDelete} />
))}
</ul>
</div>
);
}Lo que esto demuestra:
handleDelete está envuelto en useCallback con dependencias [], por lo que su referencia nunca cambiaTodoItem está envuelto en React.memo, por lo que solo se re-renderiza cuando sus props cambianuseCallback, escribir en el input re-renderizaría cada TodoItem porque handleDelete sería una nueva función cada vezhandleAdd depende de input, por lo que se actualiza cuando el input cambia - eso es correctouseCallback(fn, deps) es equivalente a useMemo(() => fn, deps)Object.is| Parámetro | Tipo | Descripción |
|---|---|---|
fn | (...args: A) => R | La función a memoizar |
dependencies | unknown[] | Array de valores reactivos usados dentro de la función |
| Retorno | Tipo | Descripción |
|---|---|---|
memoizedFn | (...args: A) => R | Función cacheada con una referencia estable |
Estabilizar un manejador de eventos para useEffect:
const fetchData = useCallback(async () => {
const res = await fetch(`/api/items?page=${page}`);
setData(await res.json());
}, [page]);
useEffect(() => {
fetchData();
}, [fetchData]);Con genéricos:
const handleSelect = useCallback(<T extends { id: string }>(item: T) => {
setSelectedId(item.id);
}, []);Callback estable sin dependencias (común para patrones de actualizadores):
const toggle = useCallback(() => {
setOpen((prev) => !prev);
}, []);Un ejemplo completo y ejecutable que demuestra claramente cómo React.memo funciona junto con useCallback.
"use client";
import React, { useState, useCallback } from "react";
// 1. Componente Hijo Memoizado
const MemoizedChild = React.memo(function MemoizedChild({
onClick,
label,
}: {
onClick: () => void;
label: string;
}) {
console.log(`${label} renderizado`); // ← Mira la consola
return (
<button onClick={onClick} className="border rounded px-3 py-1">
{label}
</button>
);
});
// 2. Componente Hijo Regular (no memoizado) para comparación
function RegularChild({
onClick,
label,
}: {
onClick: () => void;
label: string;
}) {
console.log(`${label} (regular) renderizado`);
return (
<button onClick={onClick} className="border rounded px-3 py-1">
{label} (regular)
</button>
);
}
export default function Parent() {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState(0);
// Callback estable gracias a useCallback
const handleClick = useCallback(() => {
alert("¡Botón presionado!");
}, []); // array de dependencias vacío → nunca cambia
return (
<div className="space-y-4 p-4">
<h2 className="text-lg font-bold">Componente Padre</h2>
<p>Conteo: {count}</p>
<div className="flex gap-2">
<button
onClick={() => setCount((c) => c + 1)}
className="border rounded px-3 py-1"
>
Incrementar Conteo (causa re-renderizado del padre)
</button>
<button
onClick={() => setOtherState((s) => s + 1)}
className="border rounded px-3 py-1"
>
Actualizar Otro Estado ({otherState})
</button>
</div>
<div className="space-y-2">
<h3 className="font-semibold">Hijos Memoizados</h3>
<div className="flex gap-2">
<MemoizedChild onClick={handleClick} label="Botón Memoizado 1" />
<MemoizedChild onClick={handleClick} label="Botón Memoizado 2" />
</div>
<h3 className="font-semibold">Hijos Regulares (no memoizados)</h3>
<RegularChild onClick={handleClick} label="Botón Regular" />
</div>
</div>
);
}Lo que sucede cuando haces clic en "Incrementar Conteo":
count cambió.React.memo hace una comparación superficial de props. onClick es la misma referencia (gracias a useCallback) y label es una cadena primitiva que no ha cambiado. No verás el console.log.onClick sea estable.Sin useCallback (lo que generalmente sale mal):
Si quitas useCallback y escribes const handleClick = () => { alert("¡Botón presionado!"); }, entonces incluso MemoizedChild se re-renderiza en cada re-renderizado del padre porque se crea una nueva función cada vez, causando que la comparación de props falle.
Puntos clave:
useCallback hace la referencia de la función estableReact.memo verifica si los props son iguales (igualdad superficial) y omite el renderizado si lo sonReact.memo solo hace comparación superficial - si pasas objetos o arrays, necesitas useMemo para esos también// TypeScript infiere el tipo de callback del uso
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
}, []);
// handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void
// Al pasar a un hijo, el tipo de prop del hijo limita el tipo de callback
interface Props {
onSelect: (id: string) => void;
}useCallback sin React.memo - Envolver un callback en useCallback no hace nada si el hijo no está memoizado con React.memo. Solución: Solo usa useCallback cuando el consumidor realmente se beneficia de una referencia estable.
Cierre obsoleto - Si omites una dependencia, el callback se cierra sobre un valor antiguo. Solución: Incluye todos los valores reactivos en el array de dependencias, o usa funciones actualizadoras (setState(prev => ...)) para evitar la dependencia.
Sobre-memoización - Envolver cada función en useCallback agrega sobrecarga cognitiva y uso de memoria. Solución: Solo memoiza cuando pasas a componentes hijos con React.memo, o cuando se usa como dependencia de useEffect / useMemo.
Dependencias que cambian en cada renderizado - Si una dependencia es un objeto o array inestable, la referencia del callback cambia en cada renderizado de todas formas. Solución: Memoiza la dependencia con useMemo o reestructura para usar primitivos.
| Alternativa | Usa Cuando | No Uces Cuando |
|---|---|---|
| Función inline | El hijo no está memoizado, o la función solo se usa en el mismo componente | La función se pasa a un hijo con React.memo |
useMemo | Necesitas memoizar un valor que no es una función | Necesitas memoizar una función |
dispatch de useReducer | Múltiples hijos necesitan activar cambios de estado - dispatch siempre es estable | Actualizaciones simples de valor único |
| Callback ref | Necesitas una función estable que siempre lea los valores más recientes | Quieres que la identidad de la función cambie cuando las deps cambian |
Regla de oro: Comienza sin useCallback. Agrégalo cuando perfiles y encuentres re-renderizados innecesarios en componentes hijos memoizados, o cuando una función usada como dependencia de efecto siga re-disparando el efecto.
React.memo, el hijo se re-renderiza en cada re-renderizado del padre independientemente de la estabilidad de props.useCallback solo previene re-renderizados cuando el consumidor verifica la igualdad de props (via React.memo o arrays de dependencias).useCallback y agrégalo cuando el perfilado muestre re-renderizados innecesarios.useCallback(fn, deps) es equivalente a useMemo(() => fn, deps).useCallback cachea la función en sí; useMemo cachea el valor de retorno de una función.useCallback para funciones y useMemo para valores que no son funciones.[], React nunca detecta un cambio de dependencia, por lo que devuelve la misma referencia de función para siempre.setState(prev => ...)) que no dependen de valores externos.const fetchData = useCallback(async () => {
const res = await fetch(`/api/items?page=${page}`);
setData(await res.json());
}, [page]);
useEffect(() => {
fetchData();
}, [fetchData]); // Solo se vuelve a ejecutar cuando la página cambiadispatch de useReducer siempre es estable -- nunca cambia entre renderizados.dispatch a múltiples hijos sin preocupaciones por useCallback o React.memo.const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
},
[]
);
// Tipo: (e: React.ChangeEvent<HTMLInputElement>) => voidObject.is ve una nueva referencia.useMemo o reestructura para usar valores primitivos.React.memo o usas como dependencia de useEffect/useMemo.useCallback no vale la pena.const handleSelect = useCallback(
<T extends { id: string }>(item: T) => {
setSelectedId(item.id);
},
[]
);useCallback mismo.Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥