¿Cómo crear un buscador con efecto “debounce”? 🔎

by:

Softwares


Tabla de contenido

📌 Introducción.

📌 Tecnologías a utilizar.

📌 ¿Qué es el efecto “Debounce”?

📌 Creando el proyecto.

📌 Primeros pasos.

📌 Creando el input.

📌 Manejando el estado del input.

📌 Creando la función para la petición a la API.

📌 Creando el efecto Debounce.

📌 Haciendo la llamada a la API.

📌 Creando el componente Pokemon.tsx.

📌 Usando nuestro componente Pokemon.

📌 Limpiando la lógica de nuestro componente.

📌 1. Manejando la lógica para controlar el input.

📌 2. Manejando la lógica para la llamada a la API.

📌 3. Manejando la lógica para el efecto Debounce.

📌 Conclusión.

📌 Código fuente.

 



🎈 Introducción

El propósito de este post es enseñar una manera sencilla de como realizar un pequeño buscador con un efecto debounce.
Dicho proyecto puede extenderse de muchas maneras, pero tratare de hacerlo algo básico pero eficiente.

🚨 Nota: Este post requiere que sepas las bases de React con TypeScript (hooks básicos y peticiones con fetch).

Cualquier tipo de Feedback es bienvenido, gracias y espero disfrutes el articulo.🤗

 



🎈 Tecnologías a utilizar.

 



🎈 ¿Qué es el efecto “Debounce”?

El efecto rebote (debounce) es cuando no se ejecutan al momento de su invocación. En lugar de eso, su ejecución es retrasada por un periodo predeterminado de tiempo. Si la misma función es invocada de nuevo, la ejecución previa es cancelada y el tiempo de espera se reinicia.

 



🎈 Creando el proyecto.

Al proyecto le colocaremos el nombre de: search-debounce (opcional, tu le puedes poner el nombre que gustes).

npm init vite@latest
Enter fullscreen mode

Exit fullscreen mode

Creamos el proyecto con Vite JS y seleccionamos React con TypeScript.

Luego ejecutamos el siguiente comando para navegar al directorio que se acaba de crear.

cd search-debounce
Enter fullscreen mode

Exit fullscreen mode

Luego instalamos las dependencias.

npm install
Enter fullscreen mode

Exit fullscreen mode

Después abrimos el proyecto en un editor de código (en mi caso VS code).

code .
Enter fullscreen mode

Exit fullscreen mode

 



🎈 Primeros pasos.

Dentro de la carpeta src/App.tsx borramos todo el contenido del archivo y colocamos un componente funcional que muestre un titulo.

const App = () => 
  return (
    <div className="container">
      <h1> <span>Search Engine</span> whit <span>Debounce Effect</span> </h1>
    </div>
  )

export default App
Enter fullscreen mode

Exit fullscreen mode

Debería de verse así 👀:

 



🎈 Creando el input.

Ahora creamos la carpeta src/components y dentro de la carpeta creamos el archivo Input.tsx y dentro agregamos lo siguiente:

export const Input = () => 
  return (
    <>
        <label htmlFor="pokemon">Name or ID of a Pokemon</label>
        <input type="text" id="pokemon" placeholder="Example: Pikachu" />
    </>
  )

Enter fullscreen mode

Exit fullscreen mode

Una vez hecho, lo importamos en el archivo App.tsx

import  Input  from "./components/Input"

const App = () => 

  return (
    <div className="container">
      <h1> <span>Search Engine</span> whit <span>Debounce Effect</span> </h1>

      <Input/>
    </div>
  )

export default App
Enter fullscreen mode

Exit fullscreen mode

Debería de verse así 👀:

Input

 



🎈 Manejando el estado del input.

🚨 Nota: Esta NO es la única forma para realizar este ejercicio, solo es una opción! Si tienes una mejor manera, me gustaría que la compartieras por los comentarios por favor. 😌

En este caso voy a manejar el estado del input en un nivel superior, o sea, el componente App del archivo App.tsx

Esto lo haremos, debido a que necesitamos el valor del input disponible en App.tsx, ya que ahi se hará la petición a la API y el efecto debounce.

1 – Primero creamos el estado para manejar el valor del input.

const [value, setValue] = useState('');
Enter fullscreen mode

Exit fullscreen mode

2 – Creamos una función para que actualice el estado del input cuando el input haga un cambio.

Esta función recibe como parámetro el evento que emite el input, de dicho evento obtendremos la propiedad target y luego la propiedad value, la cual es la que mandaremos a nuestro estado.

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value); 
Enter fullscreen mode

Exit fullscreen mode

3 – Por consiguiente, toca mandar la función y el valor del estado al input.

const App = () => 

  return (
    <div className="container">
      <h1> <span>Search Engine</span> whit <span>Debounce Effect</span> </h1>

      <Input ...value, onChange/>
    </div>
  )

export default App
Enter fullscreen mode

Exit fullscreen mode

4 – En el componente Input agregamos una interfaz para recibir las propiedades por parámetro en el archivo Input.tsx.

interface Props 
   value: string;
   onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;

Enter fullscreen mode

Exit fullscreen mode

5 – Desestructuramos las propiedades y las agregamos al input.

La función onChange, la colocamos en la propiedad onChange del input y lo mismo con el la value propiedad value.

interface Props 
   value: string;
   onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;


export const Form = ( onChange, value :Props) => 

  return (
    <>
        <label htmlFor="pokemon">Name of a Pokemon</label>
        <input 
          type="text" 
          id="pokemon" 
          placeholder="Example: Pikachu" 
          value=value
          onChange=onChange
        />
    </>
  )

Enter fullscreen mode

Exit fullscreen mode

Y así ya tenemos controlado el estado de nuestro input. 🥳

 



🎈 Creando la función para la petición a la API.

Ahora creamos la carpeta src/utils y dentro colocamos un archivo llamado searchPokemon.ts y agregamos la siguiente función para hacer la petición, y buscar un pokemon por su nombre o ID.

🚨 Nota: La respuesta de la API tiene más propiedades de las que se representa en la interfaz ResponseAPI

Esta función recibe dos parámetros:

  • pokemon: es el nombre o ID del pokemon.
  • signal: permite establecer escuchadores de eventos. En otras palabras, nos ayudara a cancelar la petición HTTP cuando el componente se desmonte o haga un cambio en el estado.

Esta función retorna la data del pokemon si todo sale bien o null si algo sale mal.

export interface ResponseAPI 
    name: string;
    sprites:  front_default: string 


export const searchPokemon = async (pokemon: string, signal?: AbortSignal): Promise<ResponseAPI | null> => 
    try 

        const url = `https://pokeapi.co/api/v2/pokemon/$pokemon.toLowerCase().trim()`
        const res = await fetch(url,  signal );

        if(res.status === 404) return null

        const data: ResponseAPI = await res.json();
        return data

     catch (error) 
        console.log((error as Error).message);
        return null
    

Enter fullscreen mode

Exit fullscreen mode

 



🎈 Creando el efecto Debounce.

En el archivo App.tsx creamos un estado, que nos servirá para guardar el valor del input.

const [debouncedValue, setDebouncedValue] = useState();
Enter fullscreen mode

Exit fullscreen mode

Como estado inicial le mandamos el valor del estado del input (value).

const [value, setValue] = useState('');

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value);

const [debouncedValue, setDebouncedValue] = useState(value);
Enter fullscreen mode

Exit fullscreen mode

Ahora, creamos un efecto para que cuando el valor del input cambie, ejecutamos la función setTimeout que actualizara el estado del debouncedValue enviando el nuevo valor del input, después de 1 segundo, y asi obtendremos la palabra clave o sea el pokemon, para hacer la petición a la API.

Al final del efecto, ejecutamos el método de limpieza, que consiste en limpiar la función setTimeout, es por eso que la guardamos en una constante llamada timer

useEffect(() => 
    const timer = setTimeout(() => setDebouncedValue(value), 1000)

    return () => clearTimeout(timer)
, [value]);
Enter fullscreen mode

Exit fullscreen mode

Entonces por el momento nuestro archivo App.tsx quedaría de la siguiente manera:

import  useEffect, useState  from 'react';
import  Input  from "./components/Input"

const App = () => 

  const [value, setValue] = useState('');
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value);

  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() =>  500)

    return () => clearTimeout(timer)
  , [value, delay]);

  return (
    <div className="container">
      <h1> <span>Search Engine</span> whit <span>Debounce Effect</span> </h1>

      <Input ... value, onChange  />
    </div>
  )

export default App
Enter fullscreen mode

Exit fullscreen mode

 



🎈 Haciendo la llamada a la API.

Una vez que tenemos el valor del input ya con el efecto debounce, toca hacer la llamada a la API.

Para eso usaremos al función que creamos con anterioridad, searchPokemon.tsx.

Para ello, vamos a usar un efecto.
Primero creamos el controller el cual es el que nos ayudara a cancelar la petición HTTP, como mencionamos antes
Dentro del controller tenemos dos propiedades que nos interesan:

  • abort(): al ejecutarse, cancela la petición.
  • signal: mantiene la conexión entre el controller y el la petición para saber cual se debe cancelar.

El abort() lo ejecutamos al final, al momento de que se desmonte el componente.

useEffect(() => 

    const controller = new AbortController();

    return () => controller.abort();

  , []);
Enter fullscreen mode

Exit fullscreen mode

La dependencia de este efecto sera el valor del debouncedValue, ya que cada vez que cambie este valor, debemos hacer una nueva petición para buscar al nuevo pokemon.

useEffect(() => 
    const controller = new AbortController();

    return () => controller.abort();

  , [debouncedValue])
Enter fullscreen mode

Exit fullscreen mode

Hacemos una condición, en la que solo si el existe el valor de debouncedValue y tiene alguna palabra o numero, haremos la petición.

useEffect(() => 
    const controller = new AbortController();

    if (debouncedValue) 

    

    return () => controller.abort();
  , [debouncedValue])
Enter fullscreen mode

Exit fullscreen mode

Dentro del if llamamos la función de searchPokemon y le mandamos el valor de debouncedValue y también la propiedad signal del controller

useEffect(() => 
    const controller = new AbortController();

    if (debouncedValue) 
        searchPokemon(debouncedValue, controller.signal)
    

    return () => controller.abort();
  , [debouncedValue])
Enter fullscreen mode

Exit fullscreen mode

Y como la función searchPokemon regresa una promesa y dentro del efecto no es permitido usar async/await, usaremos .then para resolver la promesa y obtener el valor que retorna.

useEffect(() => 
    const controller = new AbortController();

    if (debouncedValue) 
        searchPokemon(debouncedValue, controller.signal)
            .then(data =>  null
        )
    

    return () => controller.abort();
  , [debouncedValue])
Enter fullscreen mode

Exit fullscreen mode

Al final debería de verse asi. 👀

import  useEffect, useState  from 'react';
import  Input  from "./components/Input"
import  searchPokemon  from "./utils/searchPokemon";

const App = () => 

  const [value, setValue] = useState('');
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value);

  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => , [value, delay]);


  useEffect(() => 

    const controller = new AbortController();

    if (debouncedValue) 
      searchPokemon(debouncedValue, controller.signal)
        .then(data =>  null
        )
    

    return () => controller.abort();

  , [debouncedValue])


  return (
    <div className="container">
      <h1> <span>Search Engine</span> whit <span>Debounce Effect</span> </h1>
      <Input ... value, onChange  />

    </div>
  )

export default App
Enter fullscreen mode

Exit fullscreen mode

 



🎈 Creando el componente Pokemon.tsx.

1 – Primero creamos el componente funcional vació.

export const Pokemon = () => 
  return (
    <></>
  )

Enter fullscreen mode

Exit fullscreen mode

2 – Agregamos la interfaz de ResponseAPI ya que vamos a recibir por props el pokemon, el cual puede contener la data del pokemon o un valor nulo.

import  ResponseAPI  from "../utils/searchPokemon"

export const Pokemon = ( pokemon :  pokemon: ResponseAPI ) => 

  return (
    <></>
  )

Enter fullscreen mode

Exit fullscreen mode

3 – Hacemos una evaluación donde:

  • Si el la propiedad pokemon es nula, mostramos el mensaje de “No results”.
  • Si la propiedad pokemon contiene la data del pokemon, mostramos su nombre y una imagen
import  ResponseAPI  from "../utils/searchPokemon"

export const Pokemon = ( pokemon :  pokemon: ResponseAPI ) => 

  return (
    <>
      
        !pokemon
          ? <span>No results</span>
          : <div>
            <h3>pokemon.name</h3>
            <img src=pokemon.sprites.front_default alt=pokemon.name />
          </div>
      
    </>
  )

Enter fullscreen mode

Exit fullscreen mode

Debería de verse asi si esta cargando 👀:
Loading

Debería de verse asi cuando no hay resultados 👀:
No results

Debería de verse asi hay un pokemon 👀:
Pokemon

4 – Y ahora por ultimo, agregamos una ultima condición, en donde evaluamos si el pokemon existe (o sea que no es nula) y si es un objeto vació retornamos un fragmento.

Esto es así ya que el estado inicial para almacenar a los pokemon sera un objeto vacío ““.

Si no colocaremos esa condición, entonces la inicio de nuestra app, incluso sin haber tecleado nada en el input, ya nos aparecería el mensaje de “No results”, y la idea es que aparezca después de que hayamos tecleado algo en el input y se haya hecho la llamada a la API.

import  ResponseAPI  from "../utils/searchPokemon"

export const Pokemon = ( pokemon :  pokemon: ResponseAPI ) => 

  if(pokemon && Object.keys(pokemon).length === 0) return <></>;

  return (
    <>
      
        !pokemon
          ? <span>No results</span>
          : <div>
            <h3>pokemon.name</h3>
            <img src=pokemon.sprites.front_default alt=pokemon.name />
          </div>
      
    </>
  )

Enter fullscreen mode

Exit fullscreen mode

Asi quedaría nuestro componente pokemon, es hora de usarlo. 😌

 



🎈 Usando nuestro componente Pokemon.

En el archivo App.tsx agregaremos 2 nuevos estados:

  • Para almacenar el pokemon encontrado, que tendrá un valor inicial de un objeto vacío.
  • Para manejar un loading en lo que se hace la llamada a la API, que tendrá un valor inicial de falso.
const [pokemon, setPokemon] = useState<ResponseAPI | null>( as ResponseAPI);
const [isLoading, setIsLoading] = useState(false)
Enter fullscreen mode

Exit fullscreen mode

Ahora dentro del efecto donde hacemos la llamada a la API mediante la función searchPokemon, antes de hacer la llamada enviamos el valor de true al setIsLoading para activar el loading.

Después, una vez obtenida la data dentro del .then le enviamos dicha data al setPokemon (el cual puede ser el pokemon o un valor nulo).
Y finalmente enviamos el valor de false al setIsLoading para quitar el loading.

useEffect(() => 

    const controller = new AbortController();

    if (debouncedValue) 

      setIsLoading(true)

      searchPokemon(debouncedValue, controller.signal)
        .then(data => 
          setPokemon(data);
          setIsLoading(false);
        )
    

    return () => controller.abort();
  , [debouncedValue])
Enter fullscreen mode

Exit fullscreen mode

Una vez almacenado el pokemon, en el JSX colocamos la siguiente condición:

  • Si el valor del estado isLoading es verdadero, mostramos el mensaje de “Loading Results…”
  • Si el valor del estado isLoading es falso, mostramos el componente Pokemon, mandándole el pokemon.
return (
    <div className="container">
      <h1> <span>Search Engine</span> whit <span>Debounce Effect</span> </h1>
      <Input ... value, onChange  />
      
        isLoading 
          ? <span>Loading Results...</span>
          : <Pokemon pokemon=pokemon/>
      
    </div>
  )
Enter fullscreen mode

Exit fullscreen mode

Y todo junto quedaría asi 👀:

import  useEffect, useState  from 'react';

import  Input  from "./components/Input"
import  Pokemon  from "./components/Pokemon";

import  searchPokemon  from "./utils/searchPokemon";

import  ResponseAPI  from "./interface/pokemon";

const App = () =>  null>( as ResponseAPI);
  const [isLoading, setIsLoading] = useState(false)

  const [value, setValue] = useState('');
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value);

  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() =>  500)

    return () => clearTimeout(timer)
  , [value, delay]);

  useEffect(() => 

    const controller = new AbortController();

    if (debouncedValue) 

      setIsLoading(true)

      searchPokemon(debouncedValue, controller.signal)
        .then(data => 
          setPokemon(data);
          setIsLoading(false);
        )
    

    return () => controller.abort();
  , [debouncedValue])


  return (
    <div className="container">
      <h1> <span>Search Engine</span> whit <span>Debounce Effect</span> </h1>
      <Input ... value, onChange  />
      
        isLoading 
          ? <span>Loading Results...</span>
          : <Pokemon pokemon=pokemon/>
      

    </div>
  )

export default App
Enter fullscreen mode

Exit fullscreen mode

Es mucha lógica en un solo componente cierto? 😱

Ahora nos toca refactorizar!

 



🎈 Limpiando la lógica de nuestro componente.

Tenemos mucha lógica en nuestro componente por lo que es necesario separarla en diversos archivos:

  • Lógica para controlar el input.
  • Lógica del debounce.
  • Lógica para hacer la llamada a la API y manejar el pokemon.
    Y como esta lógica hace uso de hooks como es useState y useEffect, entonces debemos colocarlos en un custom hook.

Lo primero sera crear una nueva carpeta src/hooks



1. Manejando la lógica para controlar el input.

Dentro de la carpeta src/hooks creamos el siguiente archivo useInput.ts
Y colocamos la lógica correspondiente al manejo del input.

import  useState  from 'react';

export const useInput = (): [string, (e: React.ChangeEvent<HTMLInputElement>) => void] => 

    const [value, setValue] = useState('');

    const onChange = (e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value);

    return [value, onChange]

Enter fullscreen mode

Exit fullscreen mode

Luego llamamos al useInput en el archivo App.tsx

import  useEffect, useState  from 'react';

import  Input  from "./components/Input"
import  Pokemon  from "./components/Pokemon";

import  useInput  from "./hooks/useInput";

import  searchPokemon  from "./utils/searchPokemon";

import  ResponseAPI  from "./interface/pokemon";

const App = () =>  null>( as ResponseAPI);
  const [isLoading, setIsLoading] = useState(false)

  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() =>  500)

    return () => clearTimeout(timer)
  , [value, delay]);

  useEffect(() => 

    const controller = new AbortController();

    if (debouncedValue) 

      setIsLoading(true)

      searchPokemon(debouncedValue, controller.signal)
        .then(data => 
          setPokemon(data);
          setIsLoading(false);
        )
    

    return () => controller.abort();
  , [debouncedValue])


  return (
    <div className="container">
      <h1> <span>Search Engine</span> whit <span>Debounce Effect</span> </h1>
      <Input ... value, onChange  />
      
        isLoading 
          ? <span>Loading Results...</span>
          : <Pokemon pokemon=pokemon/>
      

    </div>
  )

export default App
Enter fullscreen mode

Exit fullscreen mode

 



2. Manejando la lógica para la llamada a la API.

Dentro de la carpeta src/hooks creamos el siguiente archivo useSearchPokemon.ts.

Colocamos la lógica relacionada con hacer la petición a la API y mostrar el pokemon.

Este custom hook recibe como parámetro un string llamado search, que es el nombre del pokemon o el ID. Y ese parámetro se lo enviamos a la función que hace la llamada a la API searchPokemon

🚨 Nota: Observe la parte del If en el efecto, al final colocamos un else donde si el debouncedValue esta vació,nno haremos una llamada a la API y le mandamos el valor de un objeto vació a setPokemon

import  useState, useEffect  from 'react';
import  ResponseAPI  from '../interface/pokemon';
import  searchPokemon  from '../utils/searchPokemon';

export const useSearchPokemon = (search: string) => 

    const [pokemon, setPokemon] = useState<ResponseAPI 
Enter fullscreen mode

Exit fullscreen mode

Luego llamamos al useSearchPokemon en el archivo App.tsx

import  useEffect, useState  from 'react';

import  Input  from "./components/Input"
import  Pokemon  from "./components/Pokemon";

import  useInput  from "./hooks/useInput";
import  useSearchPokemon  from "./hooks/useSearchPokemon";

import  searchPokemon  from "./utils/searchPokemon";

import  ResponseAPI  from "./interface/pokemon";

const App = () => 

  const [value, onChange] = useInput();

  const [debouncedValue, setDebouncedValue] = useState(value);

  const  isLoading, pokemon  = useSearchPokemon(debouncedValue)

  useEffect(() => , [value, delay]);



  return (
    <div className="container">
      <h1> <span>Search Engine</span> whit <span>Debounce Effect</span> </h1>
      <Input ... value, onChange  />
      
        isLoading 
          ? <span>Loading Results...</span>
          : <Pokemon pokemon=pokemon/>
      

    </div>
  )

export default App
Enter fullscreen mode

Exit fullscreen mode

 



3. Manejando la lógica para el efecto Debounce.

Dentro de la carpeta src/hooks creamos el siguiente archivo useDebounce.ts y colocamos toda la lógica para manejar el efecto debounce.

Este custom hook, recibe 2 parámetros:

  • value: es el valor del estado del input.
  • delay: es la cantidad de tiempo que quieres retrasar la ejecución del debounce y es opcional.

🚨 Nota: la propiedad delay la usamos el segundo parámetro de la función setTimeout, donde en caso de que delay sea undefined, entonces el tiempo por defecto sera 500ms.
Y también, agregamos la propiedad delay al arreglo de dependencias del efecto.

import  useState, useEffect  from 'react';

export const useDebounce = (value:string, delay?:number) => 

    const [debouncedValue, setDebouncedValue] = useState(value);

    useEffect(() => , [value, delay]);

    return debouncedValue

Enter fullscreen mode

Exit fullscreen mode

Luego llamamos al useDebounce en el archivo App.tsx

import  useEffect, useState  from 'react';
import  Input  from "./components/Input"
import  Pokemon  from "./components/Pokemon";
import  useInput  from "./hooks/useInput";
import  useSearchPokemon  from "./hooks/useSearchPokemon";
import  useDebounce  from "./hooks/useDebounce";
import  searchPokemon  from "./utils/searchPokemon";
import  ResponseAPI  from "./interface/pokemon";

const App = () => 

  const [value, onChange] = useInput();

  const debouncedValue = useDebounce(value, 1000);  

  const  isLoading, pokemon  = useSearchPokemon(debouncedValue)

  return (
    <div className="container">
      <h1> <span>Search Engine</span> whit <span>Debounce Effect</span> </h1>

      <Input ... value, onChange  />

      
        isLoading 
          ? <span>Loading Results...</span>
          : <Pokemon pokemon=pokemon/>
      

    </div>
  )

export default App
Enter fullscreen mode

Exit fullscreen mode

Y asi nuestro componente App.tsx quedo mas limpio y fácil de leer. 🥳

 



🎈 Conclusión.

Todo el proceso que acabo de mostrar, es una de las formas en que se puede hacer un buscador con efecto debounce. 🔎

Espero haberte ayudado a entender como realizar este ejercicio,muchas gracias por llegar hasta aquí! 🤗

Te invito a que comentes si es que conoces alguna otra forma distinta o mejor de como hacer un efecto debounce para un buscador. 🙌

 



🎈 Código fuente.

Creating a search engine with debounce effect with React JS 🚀

Creating a search engine with debounce effect with React JS and Pokemon API 🚀

Page

Technologies 🧪

  • React JS
  • Typescript
  • Vite JS

Instalation. 🚀

1. Clone the repository

 git clone 
Enter fullscreen mode

Exit fullscreen mode

2. Run this command to install the dependencies.

 npm install
Enter fullscreen mode

Exit fullscreen mode

3. Run this command to raise the development server.

 npm run dev
Enter fullscreen mode

Exit fullscreen mode


Links. ⛓️

Demo of the app 🔗

Link to tutorial post 🔗 How to make a search engine with debounde effect?

Leave a Reply

Your email address will not be published.