A tu propio ritmo

Explora nuestra extensa colección de cursos diseñados para ayudarte a dominar varios temas y habilidades. Ya seas un principiante o un aprendiz avanzado, aquí hay algo para todos.

Bootcamp

Aprende en vivo

Únete a nosotros en nuestros talleres gratuitos, webinars y otros eventos para aprender más sobre nuestros programas y comenzar tu camino para convertirte en desarrollador.

Próximos eventos en vivo

Catálogo de contenidos

Para los geeks autodidactas, este es nuestro extenso catálogo de contenido con todos los materiales y tutoriales que hemos desarrollado hasta el día de hoy.

Tiene sentido comenzar a aprender leyendo y viendo videos sobre los fundamentos y cómo funcionan las cosas.

Buscar en lecciones


← Regresar a lecciones

El Hook useCallback en React: Memorizando Funciones para Mejorar el Rendimiento

¿Qué es useCallback?
useCallback vs. Funciones Regulares
  • Sin useCallback:

En las aplicaciones React, los componentes funcionales se vuelven a renderizar cuando sus props o estado cambian. Durante cada renderizado, las funciones definidas dentro de tu componente se recrean. Esto generalmente está bien, pero puede causar problemas de rendimiento en ciertos escenarios. El hook useCallback ayuda a resolver este problema memorizando funciones de callback.

¿Qué es useCallback?

useCallback es un hook de React que devuelve una versión memorizada de la función callback que proporcionas. Esta función memorizada solo cambiará si una de sus dependencias ha cambiado. Esto puede ayudar a prevenir renderizados innecesarios de componentes que dependen de la igualdad de referencia de funciones.

1const memoizedCallback = useCallback( 2 () => { 3 doSomething(a, b); 4 }, 5 [a, b], 6);

En esta sintaxis:

  • El primer argumento es la función que quieres memorizar
  • El segundo argumento es un array de dependencias (similar a useEffect y useMemo)
  • El hook devuelve una versión memorizada de tu función que solo cambia cuando las dependencias cambian

¿Por qué necesitamos useCallback?

En React, cuando un componente se vuelve a renderizar, todo dentro de ese componente se recrea, incluidas las funciones. Esto significa:

1function ComponentePadre() { 2 // Esta función se recrea en cada renderizado 3 const handleClick = () => { 4 console.log('¡Clic!'); 5 }; 6 7 return <ComponenteHijo onClick={handleClick} />; 8}

Aunque handleClick hace lo mismo en cada renderizado, es un objeto de función diferente cada vez. Si ComponenteHijo está usando React.memo o está comparando props para igualdad de referencia, se volverá a renderizar innecesariamente.

useCallback vs. Funciones Regulares

Comparemos los dos enfoques:

Sin useCallback:

1function ComponentePadre() { 2 const [count, setCount] = useState(0); 3 4 // Se crea una nueva referencia de función en cada renderizado 5 const handleIncrement = () => { 6 setCount(count + 1); 7 }; 8 9 return ( 10 <div> 11 <p>Contador: {count}</p> 12 <button onClick={() => setCount(count + 1)}> 13 Incrementar 14 </button> 15 <ComponenteCostoso onIncrement={handleIncrement} /> 16 </div> 17 ); 18} 19 20// Usando React.memo para prevenir renderizados innecesarios 21const ComponenteCostoso = React.memo(({ onIncrement }) => { 22 console.log("ComponenteCostoso renderizado"); 23 return <button onClick={onIncrement}>Incrementar desde Hijo</button>; 24});

En este ejemplo, ComponenteCostoso se volverá a renderizar en cada renderizado de ComponentePadre porque handleIncrement es una nueva función cada vez.

Con useCallback:

1function ComponentePadre() { 2 const [count, setCount] = useState(0); 3 4 // La referencia de la función se mantiene igual entre renderizados 5 // siempre que count no cambie 6 const handleIncrement = useCallback(() => { 7 setCount(count + 1); 8 }, [count]); 9 10 return ( 11 <div> 12 <p>Contador: {count}</p> 13 <button onClick={() => setCount(count + 1)}> 14 Incrementar 15 </button> 16 <ComponenteCostoso onIncrement={handleIncrement} /> 17 </div> 18 ); 19} 20 21// Usando React.memo para prevenir renderizados innecesarios 22const ComponenteCostoso = React.memo(({ onIncrement }) => { 23 console.log("ComponenteCostoso renderizado"); 24 return <button onClick={onIncrement}>Incrementar desde Hijo</button>; 25});

Ahora, ComponenteCostoso solo se volverá a renderizar cuando count cambie, porque la referencia de la función handleIncrement se mantiene estable entre renderizados.

Cuándo Usar useCallback

Deberías considerar usar useCallback cuando:

  1. Estás pasando callbacks a componentes hijos optimizados que dependen de la igualdad de referencia para prevenir renderizados innecesarios
  2. El callback es una dependencia de otros hooks como useEffect o useMemo
  3. La función es compleja o crea closures sobre valores que podrían cambiar

Casos de Uso Comunes para useCallback

1. Optimizando Renderizados de Componentes Hijos

1function ComponenteBusqueda() { 2 const [query, setQuery] = useState(''); 3 const [results, setResults] = useState([]); 4 5 // Memorizar la función de búsqueda 6 const handleSearch = useCallback(async (searchQuery) => { 7 const data = await fetchSearchResults(searchQuery); 8 setResults(data); 9 }, []); // Deps vacías significa que esta función nunca cambia 10 11 return ( 12 <div> 13 <InputBusqueda 14 value={query} 15 onChange={setQuery} 16 onSearch={() => handleSearch(query)} 17 /> 18 <ListaResultadosMemorizada results={results} /> 19 </div> 20 ); 21} 22 23// ListaResultados está envuelta en React.memo para prevenir re-renderizados cuando las props no cambian 24const ListaResultadosMemorizada = React.memo(ListaResultados);

2. Previniendo Re-ejecuciones de Efectos

1function ObtenedorDatos({ itemId }) { 2 const [data, setData] = useState(null); 3 4 // Memorizar la función de obtención para que no cambie en cada renderizado 5 const fetchData = useCallback(async () => { 6 const result = await fetch(`/api/items/${itemId}`); 7 const json = await result.json(); 8 setData(json); 9 }, [itemId]); // Solo cambia cuando itemId cambia 10 11 // Ahora podemos usar fetchData como dependencia en useEffect 12 useEffect(() => { 13 fetchData(); 14 15 // Configurar polling 16 const intervalId = setInterval(() => { 17 fetchData(); 18 }, 10000); 19 20 return () => clearInterval(intervalId); 21 }, [fetchData]); // Las dependencias incluyen nuestro callback estable 22 23 return data ? <MostrarItem data={data} /> : <Cargando />; 24}

3. Manejadores de Eventos con Props o Estado

1function FormularioConValidacion({ onSubmit }) { 2 const [values, setValues] = useState({ name: '', email: '' }); 3 const [errors, setErrors] = useState({}); 4 5 // Memorizar la función de validación 6 const validate = useCallback(() => { 7 const newErrors = {}; 8 9 if (!values.name) newErrors.name = 'El nombre es requerido'; 10 if (!values.email) newErrors.email = 'El email es requerido'; 11 else if (!/\S+@\S+\.\S+/.test(values.email)) { 12 newErrors.email = 'El email es inválido'; 13 } 14 15 setErrors(newErrors); 16 return Object.keys(newErrors).length === 0; 17 }, [values]); 18 19 // Memorizar el manejador de envío 20 const handleSubmit = useCallback((e) => { 21 e.preventDefault(); 22 if (validate()) { 23 onSubmit(values); 24 } 25 }, [validate, values, onSubmit]); 26 27 return ( 28 <form onSubmit={handleSubmit}> 29 {/* Campos del formulario */} 30 </form> 31 ); 32}

useCallback y useMemo

useCallback está estrechamente relacionado con useMemo. De hecho, useCallback(fn, deps) es equivalente a useMemo(() => fn, deps). La diferencia está en la intención:

  • useCallback devuelve una función memorizada
  • useMemo devuelve un valor memorizado (el resultado de llamar a una función)

Usa useCallback cuando quieras memorizar una función en sí misma, y useMemo cuando quieras memorizar el resultado de una función.

1// Memorizar una función 2const handleClick = useCallback(() => { 3 console.log('¡Botón clicado!'); 4}, []); 5 6// Memorizar un valor calculado 7const expensiveValue = useMemo(() => { 8 return computeExpensiveValue(a, b); 9}, [a, b]);

Usando Actualizaciones Funcionales con useCallback

Cuando tu función callback depende del estado anterior, es mejor usar la forma de actualización funcional para evitar añadir el valor de estado como dependencia:

Enfoque menos óptimo:

1const [count, setCount] = useState(0); 2 3// Esto necesita count como dependencia 4const increment = useCallback(() => { 5 setCount(count + 1); 6}, [count]); // dependencia count significa que la función cambia cuando count cambia

Mejor enfoque:

1const [count, setCount] = useState(0); 2 3// Esto no necesita count como dependencia 4const increment = useCallback(() => { 5 setCount(prevCount => prevCount + 1); 6}, []); // Sin dependencias significa que la función es estable

Patrones Comunes y Mejores Prácticas

1. Manejadores de Eventos con Parámetros

1function Lista({ items }) { 2 // Memorizar el manejador con setItem como parámetro 3 const handleItemClick = useCallback((id) => { 4 console.log(`Item ${id} clicado`); 5 }, []); 6 7 return ( 8 <ul> 9 {items.map(item => ( 10 <ListItem 11 key={item.id} 12 item={item} 13 // Pasar la función memorizada con el id del item 14 onClick={() => handleItemClick(item.id)} 15 /> 16 ))} 17 </ul> 18 ); 19} 20 21// Este componente está optimizado para no re-renderizarse cuando el padre se re-renderiza 22const ListItem = React.memo(({ item, onClick }) => { 23 return <li onClick={onClick}>{item.name}</li>; 24});

2. Manejadores de Eventos con Debounce

1function InputBusqueda() { 2 const [query, setQuery] = useState(''); 3 4 // Crear una referencia estable a la función de búsqueda 5 const debouncedSearch = useCallback( 6 debounce((searchTerm) => { 7 // Realizar búsqueda 8 console.log('Buscando:', searchTerm); 9 }, 500), 10 [] // Esta función nunca cambia 11 ); 12 13 const handleChange = (e) => { 14 const value = e.target.value; 15 setQuery(value); 16 debouncedSearch(value); 17 }; 18 19 return ( 20 <input 21 type="text" 22 value={query} 23 onChange={handleChange} 24 placeholder="Buscar..." 25 /> 26 ); 27} 28 29// Implementación simple de debounce 30function debounce(fn, delay) { 31 let timeoutId; 32 return function(...args) { 33 clearTimeout(timeoutId); 34 timeoutId = setTimeout(() => { 35 fn(...args); 36 }, delay); 37 }; 38}

3. React.memo con useCallback

1const ComponenteCostoso = React.memo(function ComponenteCostoso({ onSave, data }) { 2 // Lógica de renderizado compleja 3 return ( 4 <div> 5 <h2>{data.title}</h2> 6 <p>{data.description}</p> 7 <button onClick={() => onSave(data)}>Guardar</button> 8 </div> 9 ); 10}); 11 12function Contenedor() { 13 const [items, setItems] = useState([/* datos iniciales */]); 14 15 // Manejador de guardado estable 16 const handleSave = useCallback((item) => { 17 // Lógica de guardado 18 console.log('Guardando:', item); 19 // Actualizar items 20 setItems(prevItems => { 21 return prevItems.map(i => 22 i.id === item.id ? {...i, saved: true} : i 23 ); 24 }); 25 }, []); // Sin dependencias, la función se mantiene estable entre renderizados 26 27 return ( 28 <div> 29 {items.map(item => ( 30 <ComponenteCostoso 31 key={item.id} 32 data={item} 33 onSave={handleSave} 34 /> 35 ))} 36 </div> 37 ); 38}

Consideraciones de Rendimiento

Como con cualquier técnica de optimización, useCallback tiene compensaciones:

  1. Uso de Memoria: Memorizar funciones consume un poco más de memoria
  2. Seguimiento de Dependencias: React aún tiene que verificar las dependencias en cada renderizado
  3. Complejidad: El uso excesivo de useCallback puede hacer que tu código sea más difícil de leer y mantener

Cuándo No Usar useCallback

No uses useCallback para cada función en tu componente por defecto. Solo úsalo cuando hayas identificado un problema específico de rendimiento o cuando:

  1. Una función se pasa a un componente envuelto en React.memo
  2. Una función se usa como dependencia en hooks como useEffect
  3. Una función es costosa de crear o tiene estado interno (por ejemplo, funciones con debounce)

useCallback Avanzado con Refs

Puedes combinar useCallback con useRef para mantener la identidad de la función entre renderizados mientras sigues accediendo a los últimos props o estado:

1function ComponenteBusqueda({ apiKey }) { 2 const [query, setQuery] = useState(''); 3 const apiKeyRef = useRef(apiKey); 4 5 // Actualizar ref cuando apiKey cambia 6 useEffect(() => { 7 apiKeyRef.current = apiKey; 8 }, [apiKey]); 9 10 // Esta función nunca cambia de identidad 11 const search = useCallback(() => { 12 // Acceder a la última apiKey vía ref 13 const currentApiKey = apiKeyRef.current; 14 // Realizar búsqueda con query y apiKey 15 console.log(`Buscando "${query}" con clave ${currentApiKey}`); 16 }, [query]); // Solo depende de query, no de apiKey 17 18 return ( 19 <div> 20 <input 21 value={query} 22 onChange={e => setQuery(e.target.value)} 23 /> 24 <button onClick={search}>Buscar</button> 25 </div> 26 ); 27}

Ejemplo Complejo: Cuadrícula de Datos con Ordenamiento y Filtrado

1import React, { useState, useCallback, useMemo } from 'react'; 2 3function CuadriculaDatos({ data, columns }) { 4 const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); 5 const [filters, setFilters] = useState({}); 6 7 // Memorizar la función de ordenamiento 8 const handleSort = useCallback((columnKey) => { 9 setSortConfig(prevSort => { 10 if (prevSort.key === columnKey) { 11 // Alternar dirección si es la misma columna 12 return { 13 key: columnKey, 14 direction: prevSort.direction === 'asc' ? 'desc' : 'asc' 15 }; 16 } 17 18 // Por defecto ascendente para nueva columna 19 return { key: columnKey, direction: 'asc' }; 20 }); 21 }, []); 22 23 // Memorizar la función de filtrado 24 const handleFilterChange = useCallback((columnKey, value) => { 25 setFilters(prevFilters => ({ 26 ...prevFilters, 27 [columnKey]: value 28 })); 29 }, []); 30 31 // Limpiar todos los filtros 32 const clearFilters = useCallback(() => { 33 setFilters({}); 34 }, []); 35 36 // Aplicar ordenamiento y filtrado con useMemo 37 const processedData = useMemo(() => { 38 // Primero aplicar filtros 39 let result = data.filter(item => { 40 // Comprobar cada filtro 41 return Object.entries(filters).every(([key, value]) => { 42 if (!value) return true; // Omitir filtros vacíos 43 44 const itemValue = String(item[key] || '').toLowerCase(); 45 return itemValue.includes(String(value).toLowerCase()); 46 }); 47 }); 48 49 // Luego ordenar 50 if (sortConfig.key) { 51 result = [...result].sort((a, b) => { 52 const aValue = a[sortConfig.key]; 53 const bValue = b[sortConfig.key]; 54 55 if (aValue < bValue) { 56 return sortConfig.direction === 'asc' ? -1 : 1; 57 } 58 if (aValue > bValue) { 59 return sortConfig.direction === 'asc' ? 1 : -1; 60 } 61 return 0; 62 }); 63 } 64 65 return result; 66 }, [data, filters, sortConfig]); 67 68 return ( 69 <div className="data-grid"> 70 <div className="filters"> 71 {columns.map(column => ( 72 <div key={column.key} className="filter"> 73 <label>Filtro {column.label}:</label> 74 <input 75 value={filters[column.key] || ''} 76 onChange={e => handleFilterChange(column.key, e.target.value)} 77 placeholder={`Filtrar por ${column.label}`} 78 /> 79 </div> 80 ))} 81 <button onClick={clearFilters}>Limpiar Filtros</button> 82 </div> 83 84 <table> 85 <thead> 86 <tr> 87 {columns.map(column => ( 88 <th 89 key={column.key} 90 onClick={() => handleSort(column.key)} 91 className={sortConfig.key === column.key ? sortConfig.direction : ''} 92 > 93 {column.label} 94 {sortConfig.key === column.key && ( 95 <span>{sortConfig.direction === 'asc' ? ' ▲' : ' ▼'}</span> 96 )} 97 </th> 98 ))} 99 </tr> 100 </thead> 101 <tbody> 102 {processedData.map((row, index) => ( 103 <tr key={row.id || index}> 104 {columns.map(column => ( 105 <td key={column.key}>{row[column.key]}</td> 106 ))} 107 </tr> 108 ))} 109 </tbody> 110 </table> 111 112 <div className="summary"> 113 Mostrando {processedData.length} de {data.length} filas 114 </div> 115 </div> 116 ); 117} 118 119// Uso 120function App() { 121 const columns = [ 122 { key: 'name', label: 'Nombre' }, 123 { key: 'age', label: 'Edad' }, 124 { key: 'occupation', label: 'Ocupación' } 125 ]; 126 127 const data = [ 128 { id: 1, name: 'Juan Pérez', age: 28, occupation: 'Desarrollador' }, 129 { id: 2, name: 'Ana García', age: 32, occupation: 'Diseñadora' }, 130 { id: 3, name: 'Roberto Rodríguez', age: 45, occupation: 'Gerente' }, 131 // Más datos... 132 ]; 133 134 return <CuadriculaDatos data={data} columns={columns} />; 135}

En este ejemplo complejo:

  1. Usamos useCallback para memorizar manejadores de eventos para ordenamiento y filtrado
  2. Usamos useMemo para procesar los datos basados en la configuración actual de ordenamiento y filtros
  3. Los componentes solo recalculan lo necesario cuando piezas específicas de estado cambian

Conclusión

El hook useCallback es una herramienta valiosa para optimizar aplicaciones React mediante la memorización de funciones para prevenir re-renderizados innecesarios. Es especialmente útil cuando:

  • Pasas funciones callback a componentes hijos optimizados
  • Usas funciones como dependencias en otros hooks
  • Creas referencias de función estables que mantienen la identidad entre renderizados

Puntos clave:

  • Usa useCallback para memorizar funciones que deberían mantener la misma referencia entre renderizados
  • Solo incluye dependencias que la función realmente utiliza
  • Considera usar el patrón de actualización funcional para evitar dependencias innecesarias
  • Combina con React.memo para prevenir re-renderizados innecesarios de componentes hijos
  • No abuses—solo aplica useCallback cuando hay un beneficio claro de rendimiento

Recuerda que la optimización prematura puede llevar a código más complejo sin ganancias significativas de rendimiento. Usa useCallback estratégicamente cuando hayas identificado problemas específicos de rendimiento o en los escenarios descritos en esta lección.