useMemo & useCallback
- views - 05-22-2023
Hola bienvenidos al blog 😀 este post veremos 2 Hooks relacionados al rendimiento que podemos aplicar a nuestras aplicaciones con React JS que son: useMemo y useCallback
Veremos progresivamente de qué tratan con conceptos y ejemplos prácticos para poder entender de forma clara de que se trata cada uno de ellos.
Hooks y Rendimiento
Antes de comenzar debemos tener claro 2 conceptos, el primer concepto es:
Memoization 🧠
Es una técnica de programación para optimización. Un ejemplo un niño debe realizar una tarea de potencias, estas van de 2 en 2.
🗒️ -> 🧠
2 elevado 0 = 1
2 elvado 3 = 8
2 elevado 4 = 16
2 elevado 5 = 32
2 elevado 5 = 64
Así va creciendo sucesivamente, en este caso puedes tomar el último valor y multiplicarlo por 2 para saber el próximo resultado.
Entonces esto es lo que si intenta hacer con Memoization
Realizar una tarea, la escribe en memoria, luego para un posterior resultado o para el mismo, lo va a sacar de memoria.
Entonces la computadora no va a tener que hacer un cálculo tan grande.
Por ejemplo:
2 x 2 x 2 x 2 x 2 x 2
Si es que ya tenemos unos pasos anteriores, se van a liberar y solo vas a multiplicar lo que resta.
Entonces eso es Memoization, memorizarse en un valor un resultado anterior o un mismo resultado, lo toma y lo usa. Es por eso que en React es utlizado en algunos casos normalmente se utiliza en algoritmos.
Al ocupar Memoization hay que tener en claro, como vamos a hacer varios cálculos y resultados, esta técnica va a ocupar más memoria y se concentra menos en procesamiento. Entonces lo compensa de alguna u otra forma.
Vamos a ver un ejemplo muy común.
function factorial(n) {
if (n === 1) {
console.log("return 1");
return 1;
} else {
console.log(`return ${n} * factorial(${n} - 1)`);
return n * factorial(n - 1);
}
}
factorial(5);
Esta es una función factorial la cual recibe un valor, si el valor es igual 1 terminamos nuestra función y le devolvemos 1. Si no tomamos el valor actual luego vamos a hacer uso de recursividad y le vamos a mandar el anterior valor.
console.log(factorial(5)); //120
Esto es óptimo pero va a funcionar hasta cierta escala, pero que pasa si queremos hacer un cálculo mucho mayor de 1000 o 999999, el algoritmo va a funcionar, le va a costar un poco a la máquina, no va a ser óptimo, va a tardar su tiempo.
Para mejorar esta lógica nos vamos a crear un store.
const memo = [];
eport function memoFactorial(n) {
if(n === 1) {
return 1;
} else if (!memo[n]) {
memo[n] = n * memoFactorial(n -1)
}
return memo[n]
}
Si es que no tenemos un número requerido no hemos realizado la operación, como no lo tienes en ese espacio de memoria, lo que vas hacer es rellenarlo del resultado y ahí le mandas el cálculo.
else if (!memo[n]) {
memo[n] = n * memoFactorial(n -1)
}
Va a ser el indice del array ➡️ su contenido va ser el resultado esperado
const memo = [];
export function memoFactorial(n) {
if (n === 1) {
console.log("return 1");
return 1;
} else if (memo[n]) {
console.log(
`$memoFactorial(${n + 1} - 1) está memoizado en memo[${n}] (${memo[n]})`
);
}
if (!memo[n]) {
console.log(`memo[${n}] = ${n} * factorial(${n} - 1)`);
memo[n] = n * memoFactorial(n - 1);
}
return memo[n];
}
console.log("%c Factorial de 5", "color: hotpink");
console.log({ res: memoFactorial(5) });
console.log("%c Factorial de 10", "color: greenyellow");
console.log({ res: memoFactorial(10) });
console.log("%c Factorial de 10", "color: skyblue");
console.log("Factorial de 10");
console.log({ res: memoFactorial(10) });
luego vamos a ver esto en consola
- Primero vamos a hacer el factorial de 5. El de color "color: hotpink" su respuesta va a ser ➡️ 120
- Segundo vamos a hacer el factorial de 10, el de color "color: greenyellow", va a realizar todo el procedimiento, pero ojo en esta parte 👀 Hemos encontrado que:
$memoFactorial(6 - 1) está memoizado en memo[5] (120)
Del anterior cálculo ya está memorizado en el espacio 5
- Último paso vamos a hacer el factorial de 10, el de color "color: skyblue" como ya está memorizado en memo 10 lo retornamos.
La segundo vez que aplicamos procedimiento, no ha sido necesario hacer el cálculo por la memorización.
“Piense en la memorización como el almacenamiento en caché de un valor para que no sea necesario volver a calcularlo”
Aplicando memoization 🧠
Hay reglas importantísimas para aplicar memoization:
Solo se puede aplicar en funciones puras.
En react existe diferentes tipos de estados:
- Estado simple ➡️ useState
- Estado compuesto ➡️ Componentes de clases: está compuesto por un objeto con todos los valores
- Estado derivado ➡️ useState - React.Component
- Estado imperativo ➡️ le decimos que hacer
- Estado declarativo ➡️ useReducer solo le mandamos el cómo
Estado Derivado
tenemos un useState y constante, que a partir de este estado inicial, estamos obteniendo un nombre y un apellido ➡️ ese es un estado derivado.
function App() {
const [form, setForm] = useState({ name: "", secondName: "" });
const fullName = `${form.name} ${form.secondName}`;
return (...);
}
export default App;
useCallback
useCallback es un hook de React que devuelve una versión memorizada de una función. Se utiliza para optimizar el rendimiento al evitar la creación innecesaria de nuevas instancias de funciones en componentes funcionales. Al memorizar la función, se garantiza que solo se vuelva a crear cuando las dependencias especificadas cambian.
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
- Primer parametro un callback lo que vamos a hacer
- Segundo va a ser una arreglo de dependencias, muy similar a lo que es useEffect.
“useCallback devolverá una versión memorizada del callback que solo cambia si una de las dependencias ha cambiado”
⚠️ Importante
UseCallback comúnmente no se utiliza con fines de optimización, sino con fines de mantener referencia del callback.
Caso de uso
Vamos a crear nuestra App para poder ver esto de forma práctica.
App.jsx
🔗 codesandbox link to see the App useCallback
import { useState } from "react";
import { ListNum } from "./ListNum";
const App = () => {
const [dark, setDark] = useState(false);
const [number, setNumber] = useState(1);
// Estado Derivado
const styles = dark
? {
backgroundColor: "#333",
color: "#fff",
}
: {};
const getNumbers = () => {
return [number, number + 1, number + 2];
};
console.log("👾 Render");
return (
<div style={styles} className="App">
<section className="wrapper">
<div style={{ display: "flex", justifyContent: "space-between" }}>
<h1>🔢 Numeros App</h1>
<input
type="number"
placeholder="Ingresa un Número"
onChange={(e) => setNumber(Number(e.target.value))}
value={number === 0 ? "" : number}
/>
<button onClick={() => setDark(!dark)}>Toggle dark mode</button>
</div>
<ListNum getNumbers={getNumbers} />
</section>
</div>
);
};
export default App;
const getNumbers = () => {
console.log("Numeros Llamados");
return [numero, numero + 1, numero + 2];
};
Esta función la vamos a monitorear dentro de un useEffect porque nos importa saber cuando ha cambiado, se la vamos a pasar el componente ListNum.jsx
<ListNum getNumbers={getNumbers} />
Que está compuesto de la siguiente forma:
import { useEffect, useState } from "react";
export const ListNum = ({ getNumbers }) => {
const [numeros, setNumeros] = useState([]);
useEffect(() => {
console.log("the function getNumber had changed");
setNumeros(getNumbers());
}, [getNumbers]);
return (
<ul>
{numeros.map((numero) => (
<li key={numero}>{numero}</li>
))}
</ul>
);
};
👀 Acá es cuando tenemos el caso de uso, que es cuando tenemos un componente padre que le esta pasando un callback a un componente hijo.
El código consiste en mostrar los siguientes 2 números del número que ingresemos al input, necesitamos estar siempre atento a números porque no sabemos cuando va a cambiar, es por eso que usamos un useEffect.
Vamos a cambiar el console.log del componente ListNum.
useEffect(() => {
console.log("the function getNumber had changed");
setNumeros(getNumbers());
}, [getNumbers]);
vamos a revisar nuestra consola
Aquí damos click en el button del dark mode, porque se ejecuta la función getNumbers() si el estado del dark mode no tiene nada que ver con obtener los números. Estamos volviendo hacer un proceso que no tiene nada que ver pero si está afectando.
¿Qué es lo que está pasando aquí a nivel de react?
Lo que esa pasando es que cada vez que hacemos un cambio en el estado, ya sea en el modo oscuro o en los números, va a renderizar todo y la función se está volviendo a generar, no va a ser la misma que la primera vez anterior.
Que pasa si la función cambia este efecto se está ejecutando
useEffect(() => {
console.log("the function getNumber had changed");
setNumeros(getNumbers());
}, [getNumbers]);
Si cambia el estado del componente principal, nuestro useEffect está disparando por efecto secundario esta función y se vuelve a recrear, useEffect está atento al cambio de la referencia.
Solo la referencias se mantienen en su valor, pero el resto se vuelve a construir de nuevo. Su referencia en memoria ha cambiado.
Aquí va a ser cuando aplicamos el hook
const getNumbers = useCallback(() => {
return [number, number + 1, number + 2];
}, [number]);
Hacer esto mismo, pero con una llamada a una API esperar que responda y cargue va a ser un montón de tiempo si es que aplico el dark mode por eso es que mejor pasar el callback como referencia usando useCallback.
cuando nos intereza que esta función cambie, solamente cuando sus dependencias han sido modificadas ➡️ el estado que nos intereza cuando cambie es number. Es aquí donde aplicamos useCallback y ahora entender useMemo va a ser mucho mas simple ya que entendemos este concepto.Es mas que nada controlar el algoritmo de reconstrucción de react es decirle que mantenga esa referencia
useMemo
useMemo es muy similar a useCallback, solo que ya no devuelve un callback devuelve un valor.
“useMemo solo volverá a calcular el valor memorizado cuando una de las dependencias haya cambiado”
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Vamos a ver de forma práctica esto en nuestra aplicación
🔗 codesandbox link to see the App useMemo
import { useState, useCallback } from "react";
import { ListaNumeros } from "./ListaNumeros";
const calcularDoblePesado = (num) => {
console.log("Ejecutando cálculo pesado");
for (let i = 0; i < 999999999; i++);
return num * 2;
};
const App = () => {
const [dark, setDark] = useState(false);
const [numero, setNumero] = useState(1);
// Estado Derivado
const dobleNumeroActual = () => calcularDoblePesado(numero);
const styles = dark
? {
backgroundColor: "#333",
color: "#fff",
}
: {};
const obtenerNumeros = useCallback(() => {
return [numero, numero + 1, numero + 2];
}, [numero]);
return (
<div style={styles} className="App">
<section className="wrapper">
<div style={{ display: "flex", justifyContent: "space-between" }}>
<h1>🔢 Numeros App</h1>
<input
type="number"
placeholder="Numero"
onChange={(ev) => setNumero(Number(ev.target.value))}
value={numero}
/>
<button onClick={() => setDark(!dark)}>Toggle dark mode</button>
</div>
<ListaNumeros obtenerNumeros={obtenerNumeros} />
<span>El doble del numero es: {dobleNumeroActual()}</span>
</section>
</div>
);
};
export default App;
- Tenemos una función calcularDoblePesado ➡️ este es un proceso bastante pesado para la maquina.
- Básicamente, lo que está haciendo es un for bastante grande
- Finalmente un:
return num * 2;
- los estamos usando acá en este estado derivado:
<span>El doble del numero es: {dobleNumeroActual}</span>
- ahora mucho ojo aquí 👀 voy a aumentar el número y si está demorando un montón, podemos ver un lack.
- También vamos a darle click al button del toggle dark mode.
No tienen nada que el dobleNumeroActual con el dark mode.
Qué está pasando aquí, es que el estado derivado está volviendo a construir todo de nuevo, volvemos a llamar al estado y vuelve a cambiar esto.
const dobleNumeroActual = () => calcularDoblePesado(numero);
¿Cómo podemos solucionar esto?
Vamos a usar useMemo
Le vamos a decir usa useMemo cuando el número recién cambie.
La gran diferencia entre useCallback useMememo es que:
- useCallback retorna ➡️ un useCallback
- useMemo retorna ➡️ un valor
obtenerNumeros vamos a utilizarlo de esta manera
const obtenerNumeros = useCallback(() => {
return [numero, numero + 1, numero + 2];
}, [numero]);
obtenerNumeros();
en cambio, dobleNumeroActual directamente lo podemos usar y listo.
const dobleNumeroActual = () => calcularDoblePesado(numero);
dobleNumeroActual;
Ahora que estamos usando useMemo solo se ejecuta cuando lo llamamos y no está ejecutando el cálculo pesado si es que doy click en el toogle del dark mode. En teoría tenemos una memorización del resultado.
Puedes confundirte con estos Hooks, ya que usan las mismas sintaxis, pero el fin es distinto.
Ahora que estamos usando useMemo solo se ejecuta cuando lo llamamos y no está ejecutando el cálculo pesado si es que doy click en el toogle del dark mode. En teoría tenemos una memorización del resultado.
Si incrementamos el valor el input se aplica dobleNumeroActual y se ve un poco más rápido. Esto es porque utiliza memorización a medias.
useMemo si bien esta inspirado en esta técnica de memoization, no aplica completamente. Por qué no va a memorizar valores en si, si no lo que esta haciendo, es memorizarlo para los renders.
La desventaja es que cuando React vea conveniente que le falta espacio, lo va a liberar, va a borrar todos los índices y es ahí cuando perdemos todas las memorizaciones, por eso es una implementación a medias. Borra el store que hemos calculado cuando vea conveniente.
Si bien esto nos sirve, ahora queremos estar atento a otro estado derivado.
dark mode
Solo queremos cambiar la referencia cuando cambie el dark cambie. Pero además la estamos manteniendo la referencia y no se mezcla lo del dobleNumeroActual ya no se vuelve actual ejecutar.
const styles = useMemo(
() =>
dark
? {
backgroundColor: "#333",
color: "#fff",
}
: {},
[dark]
);
Cuando dark cambie recién vamos a cambiar la referencia de todos los estilos. Si no, no modifica nada. Si queremos mantener las referencias en ciertas condiciones es ahí donde se utiliza useMemo, más que nada utiliza cuando queremos conservar esos valores y evitar esos cálculos pesados.
Código final de la App
import { useState, useCallback, useMemo } from "react";
import { ListaNumeros } from "./ListaNumeros";
const calcularDoblePesado = (num) => {
console.log("Ejecutando cálculo pesado");
for (let i = 0; i < 999999999; i++);
return num * 2;
};
const App = () => {
const [dark, setDark] = useState(false);
const [numero, setNumero] = useState(1);
// Estado Derivado
const dobleNumeroActual = useMemo(
() => calcularDoblePesado(numero),
[numero]
);
const styles = useMemo(
() =>
dark
? {
backgroundColor: "#333",
color: "#fff",
}
: {},
[dark]
);
const obtenerNumeros = useCallback(() => {
return [numero, numero + 1, numero + 2];
}, [numero]);
useEffect(() => {
console.log("Cambio la referencia del tema");
}, [styles]);
return (
<div style={styles} className="App">
<section className="wrapper">
<div style={{ display: "flex", justifyContent: "space-between" }}>
<h1>🔢 Numeros App</h1>
<input
type="number"
placeholder="Numero"
onChange={(ev) => setNumero(Number(ev.target.value))}
value={numero}
/>
<button onClick={() => setDark(!dark)}>Toggle dark mode</button>
</div>
<ListaNumeros obtenerNumeros={obtenerNumeros} />
<span>El doble del numero es: {dobleNumeroActual}</span>
</section>
</div>
);
};
export default App;
Esta optimización ayuda a evitar cálculos costosos en cada render
🛑 Sin el useMemo hubiera reventado nuestra aplicación, cada vez que cambie en el dark mode, además se hubiera vuelto a realizar ese cálculo pesado, que es algo que no tiene relación.
ahora hay que tener una mentalidad de que, cada vez que ejecutamos algo en useState se va volver a reconstruir todo lo que no estaba en useState y useRef, luego los useCallback o useMemo se mantienen, los cuales estan más condicionados a sus dependencias. Si dependencias cambian se reconstruyen.
Recomendaciones
Primero crea tus componentes sin utilizar React.useMemo Porque React.js puede olvidar valores previamente calculados como medida de optimización
- 💰 Las optimizaciones de rendimiento no son gratuitas. siempre vienen con un costo, pero no siempre vienen con un beneficio para compensar ese costo.
- 👀 La Mayoría de las veces React no debería molestarte para optimizar los rénderes innecesarios.
- ⚡ React es realmente rápido, y desea que te preocupes realmente por las cosas importantes
- 📊 Pero pueden volver lento tu aplicación, cosas que son pesadas de renderizar (gráficas sumamente interactivas, estadísticas, animaciones, etc.)
- ⚠️ Mucho cuidado con el uso de estas porque es complicado saber cuando es un beneficio o una perdida.
Todo el contenido armado de este post del blog fue armado en base una mentoría donde profundice acerca de los Hooks useMemo y useCallback.
🌟⚛️ Espero que este post haya sido de ayuda, lo arme como mucha investigación y ejemplos prácticos para poder aplicar la teoría. Me ayuda mucho si los compartes o se lo enseñas a algún amigo que esté usando los hooks en React.
¡Muchas gracias por tu lectura dejo el link de cafecito apoyo voluntario, tu contribución me motiva a seguir creando contenido de alta calidad. ¡Muchas gracias por tu apoyo!
