25

React Hooks y Memoization: useCallback vs. useMemo

49090Puntos

hace 2 años

Los React Hooks no solo son estados y efectos. También disponemos de un gran set de otros hooks a los que puedes sacarles mucho provecho para que tus aplicaciones sean cada vez más fáciles de construir por el navegador.

La memoización es un concepto que te ayudará a alcanzar un mayor performance en tus aplicaciones. No solo en React o JavaScript, sino también con cualquier otro framework o lenguaje.

Lo que sí es propio de React son los hooks useCallback y useMemo, cuya función es hacer más fácil guardar la data necesaria para que por debajo tu app requiera menos computo (algunas veces).

¿Pero en qué se diferencia uno del otro?

Diferencias entre useCallback y useMemo

De manera muy puntual te puedo decir lo siguiente:

  • useMemo devuelve un valor memoizado:
const memoizedValue = useMemo(() => makeSomething(dependencies), [dependencies]);
  • Mientras que useCallback devuelve un callback memoizado:
const memoizedCallback = useCallback(
  () => {
    fn(dependencies);
  },
  [dependencies],
);

Por si te lo preguntas, sí: useCallback(fn, deps) es igual a useMemo(() => fn, deps). No es lo mejor, pero se puede.

¿Qué es memoization? 😦

La memoization es una técnica de optimización que se usa principalmente para acelerar los tiempos de cómputo, lo cual se hace guardando los resultados de las funciones costosas y regresando estos valores almacenados en caché cuando los mismos inputs son pasados otra vez.

💡 Solo podemos aplicar la Memoization cuando el mismo input regresa el mismo output.

Si lo quieres ver de otra manera, imagina que se te pregunta de manera repentina el resultado de 37 x 48, probablemente no lo puedas resolver en menos de un segundo (a menos que seas súper top en hacer cálculos) y te tome algo de tiempo encontrar el resultado, que en este caso sería 1776.

Luego imagina que se te pide anotar en la palma de tu mano este valor porque se te hará la misma pregunta en un futuro, así que la próxima vez podrás responder mucho más rápido porque tienes este valor guardado en tu mano (el cache).

Ya puedes dejar de imaginar, regresemos al tema.

¿Qué valores debería memoizar?

Solo debes memoizar los valores necesarios. Esto en React lo puedes saber fácilmente utilizando el profiler, el cual con detalle nos muestra cuánto tiempo tardan nuestros componentes en renderizarse. Desde ahí puedes empezar a explorar las áreas de oportunidad para implementar estos hooks.

Pero si estás trabajando con otra framework/librería, podrías empezar por revisar en cuánto tiempo y con cuánta memoria se lleva a cabo cierta acción o petición.

💡 No trates de aplicar esta técnica en toda tu app, solo en lo que sea realmente crítico y necesario.

Entonces… ¿Cuándo utilizar uno u otro?

useCallback

useCallback es de un uso más especifico, te va a ayudar a evitar crear referencias a funciones que ya tienes.

Tomando en cuenta el ejemplo que te di hace unos momentos, es hora de llevarlo al código, el cual quedaría algo así:

const createMultiFunc = () => (num1, num2) => num1 * num2 
const multi1 = createMultiFunc() 
const multi2 = createMultiFunc() 
multi1(37, 48) // 1776
multi2(37, 48) // 1776

Puedes decir que tanto multi1 como multi2 son la misma función porque ambas son creadas por la misma función y tienen el mismo comportamiento.

Entonces si en consola hacemos una comparación, el resultado debe ser true, ¿cierto?

Pues no. Si hacemos la comparación de multi1 === multi2, nos dará false porque ambas funciones, aunque son las mismas, se encuentran en diferentes lugares de la memoria.

Ya que tienes este background, te pondré un ejemplo real con lo que te puedes llegar a topar cuando desarrollas en React.

En algunas ocasionas tendrás que hacer un render de una lista de items que tal vez vengan de una API para alimentar tu web app y es posible que cada item quieres que tenga un evento onClick donde la lógica sea la misma, posiblemente se te ocurra hacer algo como esto:

import React from'react'import data from'./utils/data'const MyList = ({handleClick}) => {
  return (
    <ul>
      {data.map(item =><li>{item.name} <buttononClick={handleClick} >Display message in console</button></li>)}
    </ul>
  )
}

exportconst Home = () => {
  const hanleClick = (event) => {
	  console.log(`You are in ${event.currentTarget}`)
	}

  return<MyListhandleClick={handleClick} />
}

Y esto podría funcionar, solo que se crearía esa función después de cada render, cosa que se soluciona utilizando useCallback de la siguiente forma:

exportconst Home = () => {
  const hanleClick = useCallback(event => {
	  console.log(`You are in ${event.currentTarget}`)
	}, []}
	// No agregamos dependencias porque la lista no va a cambiarreturn<MyListhandleClick={handleClick} />
}

👷🏻‍♂️ Todo esto fue a manera de ejemplo. Antes de haber realizado todo, se debió analizar con el profiler si es necesario implementar esta lógica adicional. De no ser así, no hay necesidad de hacerlo.

useMemo

useMemo es de un uso más general y, como te mencione antes, también puedes utilizarlo para suplir a useCallback.

Pero mucho cuidado, aunque también puedes envolver componentes con useMemo, existe un High Order Component especialmente para este tipo de casos y te lo voy a presentar aquí abajo.

Contempla utilizar este cuando tengas una cantidad de data o tengas que hacer cálculos repetitivos que puedan tomar más tiempo y recursos de los esperados. En el ejemplo anterior se podría implementar en la data que era importada o, en su defecto, en data que pueda venir de una API y se tenga que filtrar en otros lados por querys.

React.memo()

Este es uno de los HOCs del core de React. Su función es volver a renderizar el componente que envuelve solo si cambian sus props. De no ser así, React optará por reutilizarlo siempre que pueda.

Para usarlo solo debes pasarle un componente y, de manera opcional, una función que evalúe si debe hacer el render o no.

Estas son algunas formas en las que puedas usar este HOC:

  1. La forma tradicional
exportconst MyComponent = React.memo(functionMyComponent(props) {
  ...
});
  1. Envolviendo tu componente al final
const MyComponent = (props) => {
	...
};

exportdefault React.memo(MyComponent)
  1. Envolviendo tu componente al final y agregando la función de comparación
const MyComponent = (props) => {
  ...
}
consteval = (prevProps, nextProps) => {
  ...
}
exportdefault React.memo(MyComponent, eval);

En este caso, considera utilizarlo en los siguientes escenarios:

  • Tu componente tiene muchos elementos de UI.
  • Tu componente siempre recibe los mismos inputs.
  • Tu componente se renderiza constantemente con los mismos props.

Retomando el ejemplo anterior, lo podríamos implementar en el componente de la lista para que se pueda reutilizar en otros componentes y así evitar hacer todo el computo:

exportconst MyList = ({handleClick}) => {
  return (
    <ul>
      {data.map(item =><li>{item.name} <buttononClick={handleClick} >Display message in console</button></li>)}
    </ul>
  )
}

React.memo(MyList)

Memoization under the hood

Ya que llegaste a esta parte del post, es momento de explicarte brevemente como es que todo esto funciona por debajo 👀

Esto no es magia, sino código. Pero específicamente es programación funcional, ya que solo hacemos uso de estos dos conceptos:

  • Closures
  • Higher Order Functions

Si ya viste el curso de Closures y scope en JavaScript, sabrás que los closures son las formas en las que se pueden “recordar” valores dentro de una función, te recomiendo mucho verlo para que tengas ejemplos visuales y todo quede más claro.

Mientras que las Higher Order Functions son funciones que retornan funciones. En React también existen los High Order Components (HOC), los cuales son una función que recibe un componente y devuelve uno nuevo (justo como ya vimos con React.memo).

Un último consejo

No es una garantía absoluta que estas técnicas optimicen tus componentes en un 100%. En algunas ocasiones incluso pueden hacer lo contrario. Es por eso que te recomiendo medir antes y después de tratar de usar estas optimizaciones. No son malas practicas, aunque hay que utilizarlas con mucho cuidado.

⚠️ Recuerda que implementar esto le añade más complejidad a tu código, lo cual puede resultar contraproducente.

Por último, no quiero dejarte perdido en este mundo lleno de closures, scopes, HOC’s, React hooks; así que te recomiendo esta serie de cursos para que los domines como toda una profesional:

#NuncaParesDeAprender

Leonardo de los angeles
Leonardo de los angeles
LeoCode0

49090Puntos

hace 2 años

Todas sus entradas
Escribe tu comentario
+ 2
Ordenar por:
2
6591Puntos

Algo que necesitaba desde hace mucho, muchas gracias crack

2
66010Puntos

¡Wow! ¡Vaya explicación, Leo!

En resumidas cuentas, ¿Usaríamos useCallback cuando queramos memorizar la referencia de una función y useMemo un valor?

Tengo una grandísima duda, digamos que tengo un componente Autocomplete.

En el onChange de ese Autocomplete, podría usar useCallback para memorizar la función ese evento y evitar re-renders entiendo, algo así:

const onChange = useCallback((e) => {
   setState(e.target.value)
}, []);

Pero si quisiera aplicar la función debounce de lodash (o una self-made, esto solo es ejemplo) para ralentizar las acciones de ese evento a 300 ms, ya que cuando el usuario escribe hace muchas llamadas a la API y me gustaría reducir la velocidad en la que el input recibe estos eventos. ¿Nos convendría más hacer memorización del dato o que de la función en el onChange?

1
49090Puntos
2 años

Hola Francisco!

Respondiendo a tu primer pregunta, sí, podríamos decir que Usaríamos useCallback cuando queramos memorizar la referencia de una función y useMemo realmente tiene un uso muy general

Respondiendo lo segundo, si tu evento onChange va a estar en varios componentes deberías usar useCallback, mientras que por ejemplo, si solo vas a tener contados componentes con ese evento onChange lo recomendable es que solo memorices el valor (con useMemo) que va a estar capturando cada 300ms para estar seguro que no vas a tener el mismo valor cuando el usuario deje de escribir.