Hook para memorizar funciones useCallback

Juan Correa
6 min readOct 11, 2020

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 escribir
  • FoodList: 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.

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 useCallbacken 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?

claps for useCallback

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 ⚡️🤘.

--

--

Juan Correa

Full Stack JS Engineer — Developero founder — Software Development Ebooks author. https://developero.io