debounce y throttle en JavaScript y React
debounce
y throttle
son dos funciones muy utilizadas en JavaScript por temas de rendimiento pero en React hay que tener especialmente cuidado con ellas porque pueden dar más de un dolor de cabeza.
Pero primero las presentaciones. Para el código usaremos las funciones debounce
y throttle
que trae ya la librería Lodash, ¡No vamos a ponernos a crearlas hoy! De todas formas, internamente utilizan timeouts e intervals, no hay ningún secreto en ello. Además, utilizaremos una función para llamara a una REST API de Star Wars que devuelve personajes de las películas, que está muy bien para probar.
function fetchStarWarsPeople (search) {
fetch('https://swapi.dev/api/people/?search=' + search)
.then((response) => response.json())
.then((data) => data.results.map((item) => item.name))
.then(console.log)
}
Esta función recibe un parámetro de búsqueda y al final muestra por consola los resultados.
El HTML para las pruebas será algo tal que así:
...
<head>
...
<script src="https://cdn.jsdelivr.net/lodash/4/lodash.min.js"></script>
...
</head>
<body>
<input type="text" id="input"/>
</body>
Poco que comentar aquí. Utilizamos Lodash y una caja de texto, sin más.
debounce
Ayuda a que una función no se ejecute a menos que haya pasado un tiempo después de la última llamada. Imaginemos que la caja de texto sirve para buscar personajes de Star Wars. Lo podríamos hacer así:
const input = document.getElementById('input');
function onKeyUp () {
fetchStarWarsPeople(this.value)
}
input.addEventListener('keyup', onKeyUp);
El problema es que cada vez que el usuario pulse una tecla, llamará a la REST API. Esto es muy poco eficiente y podría colapsar el servidor de la REST API. Lo que queremos hacer es llamarla un tiempo después de que el usuario deje de teclear:
const debounceOnKeyUp = _.debounce( onKeyUp, 500 );
input.addEventListener('keyup', debounceOnKeyUp);
debounce
envuelve la función onKeyUp
y no la ejecutará a menos que hayan pasado 500ms después de la última llamada. Así reducimos muchísimo las llamadas al servidor.
throttle
Se usa bastante menos que debounce
y creo que nunca lo he utilizado pero reduce el número de veces que una función se ejecutará dentro de un intervalo de tiempo. Si tenemos un evento click sobre el botón tal que así:
button.addEventListener('click', onKeyUp);
Cada vez que se haga click, se hará una llamada a la REST API. Si hacemos click 100 veces, se harán 100 llamadas y eso está regular. Con throttle
podemos obligar a que sólo se haga la llamada una vez cada x segundos como máximo.
Un caso de uso podría ser el evento scroll. Si quisiéramos ejecutar una función cada vez que se hace scroll, es recomendable establecer un número máximo de veces que se llama en un espacio de tiempo.
window.addEventListener( 'scroll', _.throttle( function() {
// Código a ejecutar cuando se hace scroll
}, 500 ) );
De esta forma conseguimos que cuando se haga scroll de forma continuada, sólo se ejecute el código una vez cada medio segundo como máximo.
debounce y React
En React hay que atender a algo en especial si queremos usar estas funciones. Esto no funciona:
const App = () => {
const [ value, setValue ] = useState( '' );
const debouncedFetch = _.debounce(fetchStarWarsPeople, 1000);
const onChange = (e) => {
setValue( e.target.value );
debouncedFetch(e.target.value);
};
return (
<div>
<input
onChange={onChange}
value={value}
/>
</div>
);
};
Cada vez que la caja de texto cambia, el componente App vuelve a renderizarse y la referencia debouncedFetch
vuelve a crearse, no se mantiene entre un renderizado y otro y debounce
, que al final no es más que un timeout de JavaScript, pone el contador a cero de nuevo y genera una nueva referencia. Necesitamos crear una referencia que se mantenga entre los distintos renderizados.
Para eso se usa el hook useCallback. Éste recibe como primer argumento una función y como segundo un array de dependencias. Durante subsecuentes renderizados, la función queda memorizada y su referencia en memoria no cambia por lo que para el componente, la función es “la misma” que en el renderizado anterior. Sólo cambiará la referencia si algún elemento del array ha cambiado.
const App = () => {
const [ value, setValue ] = useState( '' );
const debouncedFetch = useCallback( _.debounce(fetchStarWarsPeople, 1000), [] );
const onChange = (e) => {
setValue( e.target.value );
debouncedFetch(e.target.value);
};
return (
<div>
<input
onChange={onChange}
value={value}
/>
</div>
);
};
En este caso, el array de dependencias está vacío ya que quiero que la función quede memorizada siempre y no cambie su referencia en ningún momento.
¿Y throttle
? Bueno, funciona exactamente igual :).