Javascript
Front End
React.js
hooks
performance
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.
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:
useEffect
y useMemo
)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.
Comparemos los dos enfoques:
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.
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.
Deberías considerar usar useCallback
cuando:
useEffect
o useMemo
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);
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}
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
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 memorizadauseMemo
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]);
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:
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
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
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});
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}
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}
Como con cualquier técnica de optimización, useCallback
tiene compensaciones:
useCallback
puede hacer que tu código sea más difícil de leer y mantenerNo uses useCallback
para cada función en tu componente por defecto. Solo úsalo cuando hayas identificado un problema específico de rendimiento o cuando:
React.memo
useEffect
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}
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:
useCallback
para memorizar manejadores de eventos para ordenamiento y filtradouseMemo
para procesar los datos basados en la configuración actual de ordenamiento y filtrosEl 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:
Puntos clave:
useCallback
para memorizar funciones que deberían mantener la misma referencia entre renderizadosReact.memo
para prevenir re-renderizados innecesarios de componentes hijosuseCallback
cuando hay un beneficio claro de rendimientoRecuerda 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.