Hook useCallback
Cachea una definición de función entre renderizados para mantener una referencia estable.
Receta
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.
Ejemplo en Funcionamiento
"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:
handleDeleteestá envuelto enuseCallbackcon dependencias[], por lo que su referencia nunca cambiaTodoItemestá envuelto enReact.memo, por lo que solo se re-renderiza cuando sus props cambian- Sin
useCallback, escribir en el input re-renderizaría cadaTodoItemporquehandleDeletesería una nueva función cada vez handleAdddepende deinput, por lo que se actualiza cuando el input cambia — eso es correcto
Análisis Profundo
Cómo Funciona
useCallback(fn, deps)es equivalente auseMemo(() => fn, deps)- En el primer renderizado, React almacena la función y sus dependencias
- En los renderizados posteriores, React compara cada dependencia usando
Object.is - Si todas las dependencias no han cambiado, React devuelve la función almacenada anteriormente (misma referencia)
- Si alguna dependencia cambió, React almacena y devuelve la nueva función
Parámetros y Valores de Retorno
| 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 |
Variaciones
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);
}, []);React.memo + useCallback Ejemplo Completo
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":
- El Padre se re-renderiza porque su estado
countcambió. - MemoizedChild NO se re-renderiza —
React.memohace una comparación superficial de props.onClickes la misma referencia (gracias auseCallback) ylabeles una cadena primitiva que no ha cambiado. No verás el console.log. - RegularChild SÍ se re-renderiza — los componentes funcionales normales siempre se re-renderizan cuando su padre lo hace, aunque la referencia de
onClicksea 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:
useCallbackhace la referencia de la función estableReact.memoverifica si los props son iguales (igualdad superficial) y omite el renderizado si lo son- Ambos son necesarios juntos para que la optimización funcione al pasar callbacks
React.memosolo hace comparación superficial — si pasas objetos o arrays, necesitasuseMemopara esos también
Notas de TypeScript
// 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;
}Trampas
-
useCallback sin React.memo — Envolver un callback en
useCallbackno hace nada si el hijo no está memoizado conReact.memo. Solución: Solo usauseCallbackcuando 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
useCallbackagrega sobrecarga cognitiva y uso de memoria. Solución: Solo memoiza cuando pasas a componentes hijos conReact.memo, o cuando se usa como dependencia deuseEffect/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
useMemoo reestructura para usar primitivos.
Alternativas
| 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.
Preguntas Frecuentes
¿Hace useCallback algo si el componente hijo no está envuelto en React.memo?
- No. Sin
React.memo, el hijo se re-renderiza en cada re-renderizado del padre independientemente de la estabilidad de props. useCallbacksolo previene re-renderizados cuando el consumidor verifica la igualdad de props (viaReact.memoo arrays de dependencias).- Comienza sin
useCallbacky agrégalo cuando el perfilado muestre re-renderizados innecesarios.
¿Cómo se relaciona useCallback con useMemo?
useCallback(fn, deps)es equivalente auseMemo(() => fn, deps).useCallbackcachea la función en sí;useMemocachea el valor de retorno de una función.- Usa
useCallbackpara funciones yuseMemopara valores que no son funciones.
¿Por qué useCallback con un array de dependencia vacío [] crea una referencia estable?
- Con
[], React nunca detecta un cambio de dependencia, por lo que devuelve la misma referencia de función para siempre. - Esto es seguro cuando el callback solo usa funciones actualizadoras (
setState(prev => ...)) que no dependen de valores externos. - Si el callback lee estado o props directamente, esos valores se vuelven obsoletos.
Trampa: ¿Qué sucede si omito una dependencia de useCallback?
- El callback se cierra sobre el valor obsoleto del renderizado cuando fue creado.
- Esto causa bugs donde el callback lee estado o props desactualizado.
- Incluye todos los valores reactivos en el array de dependencias, o usa funciones actualizadoras para evitar la dependencia.
¿Cuándo debería usar useCallback para estabilizar una función para useEffect?
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 cambia¿Por qué el dispatch de useReducer sería una mejor alternativa que useCallback en algunos casos?
dispatchdeuseReducersiempre es estable -- nunca cambia entre renderizados.- Puedes pasar
dispatcha múltiples hijos sin preocupaciones poruseCallbackoReact.memo. - Es ideal cuando múltiples hijos necesitan activar diferentes cambios de estado.
¿Cómo infiere TypeScript el tipo de una función useCallback?
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
},
[]
);
// Tipo: (e: React.ChangeEvent<HTMLInputElement>) => void- TypeScript infiere el tipo de callback de la firma de función que proporcionas.
Trampa: ¿Por qué cambia mi referencia useCallback en cada renderizado incluso con dependencias?
- Una de las dependencias es una referencia inestable (un nuevo objeto o array creado cada renderizado).
- Aunque los valores dentro del objeto no hayan cambiado,
Object.isve una nueva referencia. - Memoiza la dependencia con
useMemoo reestructura para usar valores primitivos.
¿Debería envolver cada manejador de eventos en useCallback?
- No. Envolver cada función agrega sobrecarga cognitiva y uso de memoria.
- Solo memoiza cuando pasas a componentes hijos con
React.memoo usas como dependencia deuseEffect/useMemo. - Para manejadores inline en elementos nativos, la sobrecarga de
useCallbackno vale la pena.
¿Cómo uso useCallback con un parámetro de tipo genérico en TypeScript?
const handleSelect = useCallback(
<T extends { id: string }>(item: T) => {
setSelectedId(item.id);
},
[]
);- El genérico se coloca en la función interna, no en
useCallbackmismo.
Relacionado
- useMemo — memoiza valores computados en lugar de funciones
- useEffect — callbacks estables previenen vueltas a ejecutar innecesarias del efecto
- useRef — alternativa para valores siempre actuales sin disparadores de re-renderizado
- Custom Hooks — custom hooks a menudo devuelven callbacks memoizados