Hook para memorizar funciones useCallback
--
El hook useCallback
sirve para mejorar el rendimiento de nuestros componentes en base a memorizar funciones que se usan como callbacks.
Así es como luce:
1 const memoizedCallback = useCallback(
2 () => {
3 doSomething(a, b);
4 },
5 [a, b],
6 );
Este ejemplo es sólo para que te familiarices con la manera en que se define este hook.
Antes de continuar es importante tener en cuenta ciertos detalles.
Cuando un prop cambia, se ejecuta un re render. ¿Cierto?
Pues bien, este hook es útil en el escenario en el que pasas funciones por props a componentes hijos.
Memorizar una función ayuda a evitar re renders innecesarios debido a la manera en que funciona Javascript como lenguaje.
1 true === true // true
2 false === false // true
3 1 === 1 // true
4 'a' === 'a' // true
5 {} === {} // false
6 [] === [] // false
7 () => {} === () => {} // false
8 const z = {}
9 z === z // true
Nota: las funciones para actualizar el estado que retorna useState
tienen garantía de que serán estables todo el tiempo, por lo que nos podemos despreocupar de ellas.
Al usar un callback memorizado logramos que no se vuelva a definir ese callback en cada render a menos que alguna de sus dependencias cambie.
Si te suena confuso no te preocupes que para eso está este post :)
Vamos a ver un ejemplo a continuación, sigue leyendo :D
Ejemplo de useCallback
Antes mencioné que este hook es útil cuando pasas funciones como props a componentes hijos.
Para ilustrar bien este ejemplo, vamos a hacer una mini app que consiste en tener una lista de comida con la capacidad de remover elementos de dicha lista.
Primero veremos el problema de los re renders innecesarios y después lo resolveremos con React.memo
y el hook useCallback
.
Nota: React.memo
no es el hook useMemo
que veremos en su post correspondiente. Si no lo conoces, da click aquí para ver su documentación.
Vamos a hacer los siguientes componentes:
FoodContainer
: estado de lista y un input text para escribirFoodList
: renderiza el listado de comida.FoodItem
: renderiza un elemento de la lista de comida.
Lista de alimentos:
1 const food = [
2 { id: 1, name: "pizza" },
3 { id: 2, name: "hamburger" },
4 { id: 3, name: "hot-dog" },
5 { id: 4, name: "tacos" },
6 { id: 5, name: "pizza again :)" }
7 ];
FoodListContainer
será un componente algo grande.
1 const FoodContainer = () => {
2 console.log("FoodContainer rendered");
3
4 const [foodList, setFoodList] = useState(food);
5 const [textInput, setTextInput] = useState("");
6
7 const handleChange = ({ target }) =>
8 setTextInput(target.value);
9
10 const removeItem = (id) =>
11 setFoodList(foodList.filter((foodItem) =>
12 foodItem.id !== id));
13
14 return (
15 <>
16 <h2>My Food List</h2>
17 <p>
18 New food
19 <input
20 value={textInput}
21 onChange={handleChange}
22 />
23 </p>
24 <FoodList
25 foodList={foodList}
26 removeItem={removeItem}
27 />
28 </>
29 );
30 };
Ahora FoodList
y FoodItem
.
1 const FoodList = ({ foodList, removeItem }) => {
2 console.log("FoodList rendered");
3 return (
4 <ul>
5 {foodList.map((item) => (
6
7 key={item.id}
8 item={item}
9 removeItem={removeItem}
10 />
11 ))}
12 </ul>
13 );
14 }; 1 const FoodItem = ({ item, removeItem }) => {
2 console.log("FoodItem rendered");
3 return (
4 <>
5 <li>{item.name}</li>
6 <button
7 onClick={() => removeItem(item.id)}>
8 Remove :(
9 </button>
10 </>
11 );
12 };
Escribe en el input text y observa los console logs desde la herramienta donde se ejecuta el código, no de la consola de tu navegador.
Como puedes ver, se hacen re renders en los componentes debido a que cambia la variable de estado que usamos para controlar el input, aunque realmente no cambien los valores de la lista de alimentos.
El primer cambio que haremos es envolver los componentes FooList
y FoodItem
con React.memo
para que no se re rendericen dados los mismos props.
Nota: hacemos import de memo del modo: import React, { memo } from 'react';
o usar como React.memo
.
1 const FoodList = React.memo(({ foodList, removeItem }) => {
2 console.log("FoodList rendered");
3 return (
4 <ul>
5 {foodList.map((item) => (
6 <FoodItem
7 key={item.id}
8 item={item}
9 removeItem={removeItem} />
10 ))}
11 </ul>
12 );
13 });1 const FoodItem = React.memo(({ item, removeItem }) => {
2 console.log("FoodItem rendered");
3 return (
4 <>
5 <li>{item.name}</li>
6 <button
7 onClick={() => removeItem(item.id)}>
8 Remove :(
9 </button>
10 </>
11 );
12 });
Haz este cambio en el link del código que compartí anteriormente.
Si escribes de nuevo en el input, ¡verás que se siguen re renderizando todos los componentes! ¿Por qué?
La razón es que React considera que el prop removeItem
no es equivalente al anterior prop a través de los re renderizados.
¿Recuerdas la parte de este capítulo donde está el ejemplo de la igualdad referencial?
Pondré la parte importante de este ejemplo:
() => {} === () => {} // false
Para resolver esto ahora pasamos a usar useCallback
dentro del componente FoodContainer
.
1 const FoodContainer = () => {
2 console.log("FoodContainer rendered");
3
4 const [foodList, setFoodList] = useState(food);
5 const [textInput, setTextInput] = useState("");
6
7 const handleChange = ({ target }) =>
8 setTextInput(target.value);
9
10 const removeItem = useCallback((id) =>
11 setFoodList(foodList.filter((foodItem) =>
12 foodItem.id !== id)), [foodList]);
13
14 return (
15 <>
16 <h2>My Food List</h2>
17 <p>
18 New food
19 <input
20 value={textInput}
21 onChange={handleChange}
22 />
23 </p>
24 <FoodList
25 foodList={foodList}
26 removeItem={removeItem}
27 />
28 </>
29 );
30 };
Usando en React.memo
y el hook useCallback
ahora hemos logrado optimizar los re renderizados de este ejemplo.
A decir verdad, este es un ejemplo semi forzado porque la estructura de los componentes es candidata a ser re factorizada para no tener la necesidad de pasar todos los props en el árbol de componentes.
Pero he querido hacerlo de este modo para poder ilustrar el ejemplo lo más fácil posible.
¿Debo aplicar useCallback todo el tiempo?
No. Es muy importante elegir sabiamente en base a la relación costo — beneficio.
El costo por memorizar funciones es que agregamos más sentencias de código que necesitarán memoria y que además hacen nuestro código un poco verboso.
React es realmente muy rápido en los renderizados.
Probablemente notarás mejoras en el performance cuando tengas una gran cantidad de componentes cuyo costo por un re renderizado implique una gran cantidad de cálculos.
Sin embargo, es bueno que conozcas useCallback
para que lo tengas en cuenta en tus aplicaciones.
🔔 Bonus: ¿Te gustaría dar un paso al siguiente nivel?
👨💻 Curso TDD con React JS — Jest y React testing library
👨💻 Curso Guía definitiva: Aprende los 9 Patrones Avanzados en ReactJS
📖 Patrones avanzados en React JS — Kindle ebook
Recapitulación del hook useCallback
- Usa este hook en las funciones que pases por props a componentes hijos que ocasionen re renderizados innecesarios.
- Si la función depende de otras variables, pásalas en el array de dependencias de este hook.
- Puedes usar
React.memo
en conjunto para complementar una optimización de re renders. - NO (en mayúsculas) necesitas aplicar
useCallback
en cada función que definas ya que tiene un costo. Hazlo sólo cuando realmente exista una mejora significativa en el performance.
Este ha sido un hook interesante, ¿No lo crees?
Nota: Extracto del ebook publicado en Amazon: React Hooks Manual Desde Cero. Lo puedes descargar GRATIS a través de mi sitio web 🤓.
¿Te ha gustado el contenido?
Te invito a darle clap (1, 5, 10 los que quieras!)👏 y compartirlo! 😃 Puedes subscribirte a mi canal de YouTube y al blog de Developero para más contenidos de este tipo ⚡️🤘.