No es ningún secreto que React.js se ha hecho muy popular en los últimos años. Ahora es la biblioteca JavaScript elegida por muchos de los actores más destacados de Internet, como Facebook y WhatsApp.
Una de las principales razones de su auge fue la introducción de los ganchos en la versión 16.8. Los ganchos de React te permiten aprovechar la funcionalidad de React sin tener que escribir componentes de clase. Ahora los componentes funcionales con ganchos se han convertido en la estructura a la que recurren los desarrolladores para trabajar con React.
En esta entrada del blog, profundizaremos en un gancho específico – useCallback
– porque toca una parte fundamental de la programación funcional conocida como memoización. Sabrás exactamente cómo y cuándo utilizar el gancho useCallback
y aprovechar al máximo sus capacidades de mejora del rendimiento.
¿Preparado? ¡Vamos a sumergirnos!
¿Qué es la memoización?
La memoización es cuando una función compleja almacena su salida para que la próxima vez que se llame con la misma entrada. Es similar al almacenamiento en caché, pero a un nivel más local. Puede omitir cualquier cálculo complejo y devolver la salida más rápidamente, ya que está calculada.
Esto puede tener un efecto significativo en la asignación de memoria y el rendimiento, y esa tensión es lo que el gancho useCallback
pretende aliviar.
UseCallback de React frente a useMemo
Llegados a este punto, merece la pena mencionar que useCallback
se empareja muy bien con otro gancho llamado useMemo
. Hablaremos de ambos, pero en este artículo vamos a centrarnos en useCallback
como tema principal.
La diferencia clave es que useMemo
devuelve un valor memoizado, mientras que useCallback
devuelve una función memoizada. Esto significa que useMemo
se utiliza para almacenar un valor calculado, mientras que useCallback
devuelve una función que puedes llamar más tarde.
Estos ganchos te devolverán una versión en caché a menos que cambie alguna de sus dependencias (por ejemplo, el estado o las props).
Veamos las dos funciones en acción:
import { useMemo, useCallback } from 'react'
const values = [3, 9, 6, 4, 2, 1]
// This will always return the same value, a sorted array. Once the values array changes then this will recompute.
const memoizedValue = useMemo(() => values.sort(), [values])
// This will give me back a function that can be called later on. It will always return the same result unless the values array is modified.
const memoizedFunction = useCallback(() => values.sort(), [values])
El fragmento de código anterior es un ejemplo artificial, pero muestra la diferencia entre las dos devoluciones de llamada:
memoizedValue
se convertirá en la matriz[1, 2, 3, 4, 6, 9]
. Mientras la variable de valores permanezca, también lo harámemoizedValue
, y nunca se volverá a calcular.memoizedFunction
será una función que devolverá el array[1, 2, 3, 4, 6, 9]
.
Lo bueno de estas dos devoluciones de llamada es que se almacenan en la caché y permanecen hasta que cambia la matriz de dependencia. Esto significa que, en caso de renderización, no se recolectará la basura.
Renderización y React
¿Por qué es importante la memoización en React?
Tiene que ver con la forma en que React renderiza sus componentes. React utiliza un DOM virtual almacenado en memoria para comparar datos y decidir qué actualizar.
El DOM virtual ayuda a React con el rendimiento y mantiene tu aplicación rápida. Por defecto, si cualquier valor de tu componente cambia, todo el componente se volverá a renderizar. Esto hace que React sea «reactivo» a la entrada del usuario y permite que la pantalla se actualice sin recargar la página.
No querrás renderizar tu componente porque los cambios no afectarán a ese componente. Aquí es donde la memoización a través de useCallback
y useMemo
resulta útil.
Cuando React vuelve a renderizar tu componente, también recrea las funciones que has declarado dentro de tu componente.
Ten en cuenta que al comparar la igualdad de una función con otra función, siempre serán falsas. Como una función también es un objeto, sólo se igualará a sí misma:
// these variables contain the exact same function but they are not equal
const hello = () => console.log('Hello Matt')
const hello2 = () => console.log('Hello Matt')
hello === hello2 // false
hello === hello // true
En otras palabras, cuando React vuelve a renderizar tu componente, verá cualquier función declarada en tu componente como una nueva función.
Esto está bien la mayor parte del tiempo, y las funciones simples son fáciles de calcular y no afectarán al rendimiento. Pero otras veces, cuando no quieras que la función se vea como una función nueva, puedes confiar en que useCallback
te ayuda.
Quizá pienses: «¿Cuándo no querría que una función se viera como una función nueva?» Bueno, hay ciertos casos en los que useCallback
tiene más sentido:
- Estás pasando la función a otro componente que también está memoizado (
useMemo
) - Tu función tiene un estado interno que necesita recordar
- Tu función es una dependencia de otro gancho, como
useEffect
por ejemplo
Ventajas de rendimiento de React useCallback
Cuando useCallback
se utiliza adecuadamente, puede ayudar a acelerar tu aplicación y evitar que los componentes se vuelvan a renderizar si no es necesario.
Digamos, por ejemplo, que tienes un componente que obtiene una gran cantidad de datos y que es responsable de mostrar esos datos en forma de gráfico, como éste:
Supongamos que el componente principal de tu visualización de datos se vuelve a renderizar, pero los cambios de accesorios o de estado no afectan a ese componente. En ese caso, es probable que no quieras o necesites volver a renderizarlo y recuperar todos los datos. Evitar este re-renderizado y refetch puede ahorrar el ancho de banda de tu usuario y proporcionar una experiencia de usuario más fluida.
Inconvenientes de React useCallback
Aunque este gancho puede ayudarte a mejorar el rendimiento, también tiene sus inconvenientes. Algunas cosas que debes tener en cuenta antes de utilizar useCallback
(y useMemo
) son:
- Recogida de basura: Las otras funciones que no estén ya memoizadas serán desechadas por React para liberar memoria.
- Asignación de memoria: De forma similar a la recogida de basura, cuantas más funciones memoizadas tengas, más memoria se necesitará. Además, cada vez que utilizas estas devoluciones de llamada, hay un montón de código dentro de React que necesita utilizar aún más memoria para proporcionarte la salida en caché.
- Complejidad del código: Cuando empiezas a envolver funciones en estos ganchos, aumentas inmediatamente la complejidad de tu código. Ahora se requiere una mayor comprensión de por qué se utilizan estos ganchos y la confirmación de que se utilizan correctamente.
Ser consciente de los anteriores puede ahorrarte el dolor de cabeza de tropezar con ellos tú mismo. Cuando consideres emplear useCallback
, asegúrate de que las ventajas de rendimiento serán mayores que los inconvenientes.
Ejemplo de React useCallback
A continuación se muestra una configuración sencilla con un componente Botón y un componente Contador. El Contador tiene dos partes de estado y genera dos componentes Botón, cada uno de los cuales actualizará una parte distinta del estado de los componentes Contador.
El componente Button recibe dos props: handleClick
y nombre. Cada vez que el Button se renderice, se registrará en la consola.
import { useCallback, useState } from 'react'
const Button = ({handleClick, name}) => {
console.log(`${name} rendered`)
return <button onClick={handleClick}>{name}</button>
}
const Counter = () => {
console.log('counter rendered')
const [countOne, setCountOne] = useState(0)
const [countTwo, setCountTwo] = useState(0)
return (
<>
{countOne} {countTwo}
<Button handleClick={() => setCountOne(countOne + 1)} name="button1" />
<Button handleClick={() => setCountTwo(countTwo + 1)} name="button1" />
</>
)
}
En este ejemplo, cada vez que hagas clic en uno de los dos botones, verás esto en la consola:
// counter rendered
// button1 rendered
// button2 rendered
Ahora, si aplicamos useCallback
a nuestras funciones handleClick
y envolvemos nuestro Button en React.memo
, podemos ver lo que nos proporciona useCallback
. React.memo
es similar a useMemo
y nos permite memoizar un componente.
import { useCallback, useState } from 'react'
const Button = React.memo(({handleClick, name}) => {
console.log(`${name} rendered`)
return <button onClick={handleClick}>{name}</button>
})
const Counter = () => {
console.log('counter rendered')
const [countOne, setCountOne] = useState(0)
const [countTwo, setCountTwo] = useState(0)
const memoizedSetCountOne = useCallback(() => setCountOne(countOne + 1), [countOne)
const memoizedSetCountTwo = useCallback(() => setCountTwo(countTwo + 1), [countTwo])
return (
<>
{countOne} {countTwo}
<Button handleClick={memoizedSetCountOne} name="button1" />
<Button handleClick={memoizedSetCountTwo} name="button1" />
</>
)
}
Ahora, cuando hagamos clic en cualquiera de los botones, sólo veremos el botón que hemos pulsado para entrar en la consola:
// counter rendered
// button1 rendered
// counter rendered
// button2 rendered
Hemos aplicado la memoización a nuestro componente botón, y los valores prop que se le pasan son vistos como iguales. Las dos funciones de handleClick
se almacenan en caché y serán vistas como la misma función por React hasta que cambie el valor de un elemento de la matriz de dependencia (por ejemplo, countOne
, countTwo
).
Resumen
Aunque useCallback
y useMemo
son geniales, recuerda que tienen casos de uso específicos: no deberías envolver todas las funciones con estos ganchos. Si la función es compleja desde el punto de vista computacional, una dependencia de otro gancho o un accesorio que se pasa a un componente memoizado son buenos indicadores de que quizá debas recurrir a useCallback
.
Esperamos que este artículo te haya ayudado a entender esta funcionalidad avanzada de React y a ganar más confianza con la programación funcional en el camino
Deja una respuesta