El hook useCallback y React.memo
Descubre lo bien que trabajan juntos este hooks y la utilidad React.memo para memorizar componentes
Hace unos meses escribí un artículo en donde hablaba de los hooks useCallback
y useMemo
que nos permiten optimizar el rendimiento de nuestra aplicación:
Hoy quiero escribir un artículo complementario en donde veamos una de las aplicaciones más interesantes que tiene el hook useCallback
, ya que creo que de este modo resulta mucho más fácil encontrarle utilidad y animarnos a usarlo más a menudo.
¡Comencemos!
El hook useCallback
Antes de comenzar recordemos cómo funciona el hook useCallback
. Este “hook” nos permite memorizar la función que le pasemos como argumento, devolviéndonos siempre la misma “instancia” render tras render hasta que cambie alguna de las dependencias que especifiquemos.
Las dependencias del “hook” useCallback
funcionan del mismo modo que las del hook useEffect
: son especificadas por medio de un array que pasamos como segundo argumento al hook. En el momento en que alguna de ellas cambie recibiremos una nueva versión de la función.
import {useCallback} from 'react';function Component() {
// ....
const onClick = useCallback(() => console.log('Click'); []);return (<div>
<MyButton onClick={onClick}>Púlsame</MyButton>
...
);
}
Su uso suele estar muy asociado a la creación de “custom hooks” ya que es una buena práctica devolver memorizadas las funciones que en ellos creemos para que puedan ser empleadas dentro de otros hooks (como el hook useEffect
) sin miedo a que causen problemas al emplearlas como dependencias.
import {useCallback, useState} from 'react';
export default function useCount(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(function() {
setCount(count => count + 1);
}, [setCount]);
return [count, increment];
}
Gracias a este uso del hook useCallback
podemos emplear la función increment
dentro de un efecto sin miedo a declararlo en las dependencias, ya que siempre obtendremos la misma función:
useEffect(function() {
// something
console.log(otherDep);
increment();
}, [increment, otherDep]);
Sin embargo, tiene otro uso muy interesante como estamos a punto de ver.
React.memo
La función React.memo
nos permite memorizar un componente. Esto permite a React evitar renderizarlo si sus propiedades no han cambiado, con la consiguiente optimización en lo que a rendimiento se refiere.
import React from 'react';function Footer({user}) {
return (...);
}export default React.memo(Footer);
En este caso, cuando empleemos el componente Footer
en nuestra aplicación, React evitará renderizarlo de nuevo siempre y cuando su propiedad user
no cambie.
🤓 Quizás ya adivines lo que estoy a punto de contarte.
Combinando useCallback
y React.memo
Para entender lo que estoy a punto de contarte es importante que recuerdes que las funciones que declaramos en nuestros componentes se crean cada vez que el componente se renderiza.
Esto quiere decir lo siguiente:
// 1️⃣ MyButton.jsfunction MyButton({onClick}) {
return <button onClick={onClick}>Pulsar</button>
}export default React.memo(MyButton);// 2️⃣ Component.jsfunction Component() {
// ....
function onClick() {
...
} return (<div>
<MyButton onClick={onClick}>Púlsame</MyButton>
...
);
}
Cada vez que el componente Component
se renderiza estamos creando una nueva función handleClick
y por tanto, MyButton
está recibiendo en cada render una nueva función handleClick
.
Esto significa que memorizar el componente MyButton
por medio de React.memo
no tendría efecto ya que la propiedad onClick
del render “N” sería distinta de la del render “N+1”.
De una forma simplificada puedes pensarlo de este modo:
function createFn() {
return function foo() { console.log('Hola'); }
}const fn1 = createFn();
const fn2 = createFn();
console.log(fn1 === fn2); // false
Sin embargo, gracias al hook useCallback
podemos memorizar las funciones para obtener en cada render la misma “instancia” siempre y cuando las dependencias no cambien.
// 1️⃣ MyButton.jsfunction MyButton({onClick}) {
return <button onClick={onClick}>Pulsar</button>
}export default React.memo(MyButton);// 2️⃣ Component.jsfunction Component() {
// ....
const onClick = useCallback(() => console.log('Click'); []); return (<div>
<MyButton onClick={onClick}>Púlsame</MyButton>
...
);
}
Como en este caso el array de dependencias del hook useCallback
se encuentra vacío, render tras render obtendremos la misma función onClick
, lo cual permitirá a React aprovechar la memorización del componente MyButton
y renderizarlo una única vez.
Ejemplo
Como al principio estas cosas son difíciles de ver he creado el siguiente CodeSandbox.
En él estoy definiendo un hook useCount
que devuelve una función increment
y su correspondiente versión memorizada.
El componente AppView
del proyecto pinta un botón que al pulsar invoca a la función que recibe como propiedad. Este componente AppView
se encuentra memorizado gracias a React.memo
.
El componente AppView
lo uso dos veces:
- Una pasándole la versión memorizada de la función
increment
. - Otra pasándole la versión sin memorizar.
Al pulsar los botones podrás ver como el AppView
que emplea la versión memorizada de increment
no vuelve a renderizarse, algo que sí sucede con el componente AppView
que usa la versión sin memorizar.
Conclusiones
Como has podido ver, el hook useCallback
nos mete de lleno en un terreno muy interesante como es la optimización de los renders que React realiza, algo que suele ser de vital importancia en aplicaciones de gran tamaño.
Emplearlo en conjunción con la utilidad React.memo nos puede ahorrar numerosos renders de componentes pesados (por ejemplo la cabecera y el footer), algo que terminaremos agradeciendo más pronto que tarde.
¿Quieres recibir más artículos como este?
Si te ha gustado este artículo te animo a que te suscribas a la newsletter que envío cada domingo con publicaciones similares a esta y más contenido recomendado: 👇👇👇
Apóyame en Patreon
🧡🧡🧡 Gracias a: Joseba, Óscar, Alex, Jorge y Carolina.