Skip to main content

Implementando Debounce en Vainilla Javascript

Publicado hace
Actualizado hace
7 minutos de lectura

Probablemente una de las funciones más utilizadas en el mundo de Javascript es (además de throttle) debounce. Tanto que muchas veces es la única función que llego a importar de lodash, pero, por muy pequeña que sea la librería de lodash, ¿no es demasiado importar una librería nueva sólo para una función?

Es por eso que muchas veces puede ser más beneficioso implementar nuestras propias versiones de este tipo de utilidades, y lo mejor de todo es que debounce es muy simple de utilizar.

Note

¿Qué es debounce?

Cuando hablamos de debounce, nos referimos a una técnica utilizada en programación (en Javascript principalmente) en la que evitamos que un evento se ejecute hasta que cierto tiempo ha pasado. Esto nos ayuda a prevenir que un evento se ejecute antes de que tengamos las herramientas que necesita.

El Problema

Antes de comenzar, imaginemos que tenemos un formulario de búsqueda en nuestra aplicación, este enviará una petición a nuestra API para regresar resultados relacionados con las palabras clave que el usuario elija. Para ayudar al usuario a ahorrar preciosos milisegundos y un click, no agregaremos un botón de búsqueda, el formulario lo hará automáticamente:

1<form>
2 <label
3 htmlFor='search1'
4 className='block text-sm font-medium text-gray-700'
5 >
6 Ingresa una palabra clave
7 </label>
8 <input
9 type='text'
10 id='search1'
11 name='search1'
12 className='mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm'
13 />
14</form>
15

Esto resultará en:

Ahora, digamos que antes de conectar nuestro formulario a nuestra API, queremos asegurarnos de que funcione, así que queremos imprimir el valor de este campo en la consola (puedes abrir la consola del navegador y probar el campo tu mismo 😉️)

Simplemente agregamos un console.log a la función de cambio:

1<form>
2 <label
3 htmlFor='search1'
4 className='block text-sm font-medium text-gray-700'
5 >
6 Ingresa una palabra clave
7 </label>
8 <input
9 type='text'
10 id='search1'
11 name='search1'
12 className='mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm'
13 onChange={(e) => {
14 console.log(e.target.value);
15 }}
16 />
17</form>
18

Console log de cada cambio en el campo de búsqueda

Warning

¿Puedes ver el problema? Cada vez que presionamos una tecla nuestra función de cambio es ejecutada, imagínate que hubieramos conectado nuestro formulario al API, cada uno de esos logs sería una petición a la API, y ¡de sólo un usuario!

Ahora imagínate que tuvieramos 10,000 usuarios al día…

La Solución

Aquí es donde entra debounce, lo que vamos a hacer es esperar a que el usuario termine de escribir antes de ejecutar nuestra función de cambio, no necesitamos esperar mucho, sólo algunos milisegundos. De esta forma nos aseguramos de que solo ejecutamos nuestras peticiones cuando las necesitemos.

La lógica de debounce no es muy complicada, se basa en algunas pautas muy simples, que son:

  1. Se ejecuta un evento (como presionar una tecla)
  2. Se inicia un timer
  3. Si el mismo evento vuelve a ser ejecutado:
    • El timer se elimina
    • Un nuevo timer se inicia
  4. Si el timer logra llegar a 0, la función finalmente se ejecuta

Ya que tenemos la lógica que necesitamos, podemos pasar a la parte divertida.

La implementación

Ya que este es un tutorial para mostrar el funcionamiento de debounce, no recurriré a mi método habitual para utilizarlo (ya que mi método habitual es simplemente usar lodash), en lugar de eso, tomaremos la lógica que escribimos en los párrafos anteriores y la traduciremos a Javascript.

Comencemos con el primer paso

Se ejecuta un evento (como presionar una tecla)

Por fortuna, en nuestro ejemplo estamos utilizando el evento onChange, así que esto ya se encarga de ese paso.

Caution

Este blog, y sus ejemplos, están escritos en React.

onChange es un evento que es agregado automáticamente a los elementos input cuando son creados en React, esto no está disponible en Javascript convencional, pero en ese caso podemos utilizar el equivalente:

1const input = document.querySelector('#search1');
2input.addEventListener('change', (event) => {
3 console.log(event.target.value); // Aquí va nuestra lógica
4});
5

Antes se acercarnos al evento de cambio, es una buena idea guardar nuestra función para que podamos utilizarla en varios lugares, así que crearemos la función:

1const debounce = () => {};
2

Bastante simple, ahora veamos.

Se inicia un timer

Para crear el timer, primero necesitamos guardarlo en una referencia (así podemos limpiarlo si es necesario):

1const debounce = (callback, delay) => {
2 let timer;
3
4 return (...args) => {
5 // Creamos un nuevo timer.
6 timer = setTimeout( () => {
7 // Ejecutamos la función con los argumentos.
8 callback( ...args );
9 }, delay );
10 };
11};
12

Note

¿return (...args)?

Si se te hace raro que creemos una función que regresa otra función, la razón es que necesitamos una función que reciba los argumentos del evento, pero que se ejecute después de nuestro timer. Cuando vemos: addEventListener('change', debounced), lo que sucede es que debounced se ejecuta primero, y después change envía los argumentos a la función que debounced regresa.

Si el mismo evento vuelve a ser ejecutado:

  • El timer se elimina
  • Un nuevo timer se inicia

Hasta ahora ya tenemos nuestro timer, y hemos especificado que al terminar ejecute nuestra función, (esto se encarga del punto 4), pero nos falta el punto 3, limpiar el timer si el evento se repite. Como ya tenemos una referencia a nuestro timer, podemos limpiarlo con clearTimeout:

1const debounce = ( callback, delay ) => {
2 // Guardamos referencia al timer.
3 let timer;
4
5 return (...args) => {
6 // Limpiamos el timer si existe.
7 clearTimeout( timer );
8
9 // Creamos un nuevo timer.
10 timer = setTimeout( () => {
11 // Ejecutamos la función con los argumentos.
12 callback( ...args );
13 }, delay );
14 };
15};
16

¡Nuestra función está completa! Para utilizarla con nuestro ejemplo, en lugar de iniciar nuestra función original, lo que hacemos es envolverla en nuestra función de debounce, de esta forma:

1const debounced = debounce((e) => {
2 console.log(e.target.value);
3}, 500);
4

Note

Traduciendo esto, quedaría de esta forma:

1(e) => {
2 console.log(e.target.value);
3}
4

Sería nuestro callback, y 500 sería el tiempo de espera en milisegundos, nuestro delay.

Ahora, en lugar de utilizar nuestra función original, pasamos nuestra función debounced al evento onChange:

1<input
2 type='text'
3 id='search1'
4 name='search1'
5 className='mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm'
6 onChange={debounced}
7/>
8

O sin usar react:

1input.addEventListener('change', debounced);
2

Ahora, si volvemos a probar nuestra forma de búsqueda, tendremos algo similar a esto:

Tip

Puedes probar el ejemplo anterior en la consola del navegador, 😉

Ejemplo con debounce

Después de esto, podemos ver una gran mejoría en nuestras llamadas, ya no se registra todas y cada una de las letras que ponemos en el campo, si no que se espera a que el usuario termine de escribir antes de hacer la llamada, lo que resulta en muchas menos y más completas llamadas a nuestra API.

Conclusión

Como podemos ver, esta pequeña función puede llegar a ser de gran ayuda, especialmente cuando ponemos atención a la experiencia del usuario, en muchos casos, nos puede beneficiar el pausar un momento y esperar a que el usuario nos de algo de feedback antes de continuar con nuestros cálculos.

Si tienes alguna duda o comentario, no dudes en contactarme, ¡me encantaría saber tu opinión!