Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
June 23, 2022 09:12 pm GMT

Cmo crear un buscador con efecto "debounce"?

Tabla de contenido

Introduccin.

Tecnologas a utilizar.

Qu es el efecto "Debounce"?

Creando el proyecto.

Primeros pasos.

Creando el input.

Manejando el estado del input.

Creando la funcin para la peticin a la API.

Creando el efecto Debounce.

Haciendo la llamada a la API.

Creando el componente Pokemon.tsx.

Usando nuestro componente Pokemon.

Limpiando la lgica de nuestro componente.

1. Manejando la lgica para controlar el input.

2. Manejando la lgica para la llamada a la API.

3. Manejando la lgica para el efecto Debounce.

Conclusin.

Cdigo fuente.

Introduccin

El propsito de este post es ensear una manera sencilla de como realizar un pequeo buscador con un efecto debounce.
Dicho proyecto puede extenderse de muchas maneras, pero tratare de hacerlo algo bsico pero eficiente.

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

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

Tecnologas a utilizar.

  • React JS (version 18)
  • Vite JS
  • TypeScript
  • Pokemon API
  • CSS vanilla (Los estilos los encuentras en el repositorio al final de este post)

Qu es el efecto "Debounce"?

El efecto rebote (debounce) es cuando no se ejecutan al momento de su invocacin. En lugar de eso, su ejecucin es retrasada por un periodo predeterminado de tiempo. Si la misma funcin es invocada de nuevo, la ejecucin 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

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

Luego instalamos las dependencias.

npm install

Despus abrimos el proyecto en un editor de cdigo (en mi caso VS code).

code .

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

Debera de verse as :

Title

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" />    </>  )}

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

Debera de verse as :

Input

Manejando el estado del input.

Nota: Esta NO es la nica forma para realizar este ejercicio, solo es una opcin! Si tienes una mejor manera, me gustara 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 peticin a la API y el efecto debounce.

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

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

2 - Creamos una funcin para que actualice el estado del input cuando el input haga un cambio.

Esta funcin recibe como parmetro 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); 

3 - Por consiguiente, toca mandar la funcin 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

4 - En el componente Input agregamos una interfaz para recibir las propiedades por parmetro en el archivo Input.tsx.

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

5 - Desestructuramos las propiedades y las agregamos al input.

La funcin 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}        />    </>  )}

Y as ya tenemos controlado el estado de nuestro input.

Creando la funcin para la peticin a la API.

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

Nota: La respuesta de la API tiene ms propiedades de las que se representa en la interfaz ResponseAPI

Esta funcin recibe dos parmetros:

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

Esta funcin 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    }}

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();

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);

Ahora, creamos un efecto para que cuando el valor del input cambie, ejecutamos la funcin setTimeout que actualizara el estado del debouncedValue enviando el nuevo valor del input, despus de 1 segundo, y asi obtendremos la palabra clave o sea el pokemon, para hacer la peticin a la API.

Al final del efecto, ejecutamos el mtodo de limpieza, que consiste en limpiar la funcin setTimeout, es por eso que la guardamos en una constante llamada timer

useEffect(() => {    const timer = setTimeout(() => setDebouncedValue(value), 1000)    return () => clearTimeout(timer)}, [value]);

Entonces por el momento nuestro archivo App.tsx quedara 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(() => {    const timer = setTimeout(() => setDebouncedValue(value), delay || 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

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 funcin 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 peticin HTTP, como mencionamos antes
Dentro del controller tenemos dos propiedades que nos interesan:

  • abort(): al ejecutarse, cancela la peticin.
  • signal: mantiene la conexin entre el controller y el la peticin 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();  }, []);

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

useEffect(() => {    const controller = new AbortController();    return () => controller.abort();  }, [debouncedValue])

Hacemos una condicin, en la que solo si el existe el valor de debouncedValue y tiene alguna palabra o numero, haremos la peticin.

useEffect(() => {    const controller = new AbortController();    if (debouncedValue) {    }    return () => controller.abort();  }, [debouncedValue])

Dentro del if llamamos la funcin de searchPokemon y le mandamos el valor de debouncedValue y tambin la propiedad signal del controller

useEffect(() => {    const controller = new AbortController();    if (debouncedValue) {        searchPokemon(debouncedValue, controller.signal)    }    return () => controller.abort();  }, [debouncedValue])

Y como la funcin 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 => {            console.log(data) //pokemon | null        })    }    return () => controller.abort();  }, [debouncedValue])

Al final debera 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(() => {    const timer = setTimeout(() => setDebouncedValue(value), delay || 500)    return () => clearTimeout(timer)  }, [value, delay]);  useEffect(() => {    const controller = new AbortController();    if (debouncedValue) {      searchPokemon(debouncedValue, controller.signal)        .then(data => {            console.log(data) //pokemon | 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

Creando el componente Pokemon.tsx.

1 - Primero creamos el componente funcional vaci.

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

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 | null }) => {  return (    <></>  )}

3 - Hacemos una evaluacin 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 | null }) => {  return (    <>      {        !pokemon          ? <span>No results</span>          : <div>            <h3>{pokemon.name}</h3>            <img src={pokemon.sprites.front_default} alt={pokemon.name} />          </div>      }    </>  )}

Debera de verse asi si esta cargando :
Loading

Debera de verse asi cuando no hay resultados :
No results

Debera de verse asi hay un pokemon :
Pokemon

4 - Y ahora por ultimo, agregamos una ultima condicin, 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 vaco "{}".

Si no colocaremos esa condicin, entonces la inicio de nuestra app, incluso sin haber tecleado nada en el input, ya nos aparecera el mensaje de "No results", y la idea es que aparezca despus 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 | null }) => {  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>      }    </>  )}

Asi quedara 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 vaco.
  • 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)

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

Despus, 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])

Una vez almacenado el pokemon, en el JSX colocamos la siguiente condicin:

  • 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, mandndole 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>  )

Y todo junto quedara 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 = () => {  const [pokemon, setPokemon] = useState<ResponseAPI | 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(() => {    const timer = setTimeout(() => setDebouncedValue(value), delay || 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

Es mucha lgica en un solo componente cierto?

Ahora nos toca refactorizar!

Limpiando la lgica de nuestro componente.

Tenemos mucha lgica en nuestro componente por lo que es necesario separarla en diversos archivos:

  • Lgica para controlar el input.
  • Lgica del debounce.
  • Lgica para hacer la llamada a la API y manejar el pokemon.Y como esta lgica 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 lgica para controlar el input.

Dentro de la carpeta src/hooks creamos el siguiente archivo useInput.ts
Y colocamos la lgica 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]}

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 = () => {  const [value, onChange] = useInput();  const [pokemon, setPokemon] = useState<ResponseAPI | null>({} as ResponseAPI);  const [isLoading, setIsLoading] = useState(false)  const [debouncedValue, setDebouncedValue] = useState(value);  useEffect(() => {    const timer = setTimeout(() => setDebouncedValue(value), delay || 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

2. Manejando la lgica para la llamada a la API.

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

Colocamos la lgica relacionada con hacer la peticin a la API y mostrar el pokemon.

Este custom hook recibe como parmetro un string llamado search, que es el nombre del pokemon o el ID. Y ese parmetro se lo enviamos a la funcin 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 | null>({} as ResponseAPI);    const [isLoading, setIsLoading] = useState(false)    useEffect(() => {        const controller = new AbortController();        if (search) {            setIsLoading(true);            searchPokemon(search, controller.signal)                .then(data => {                    setPokemon(data);                    setIsLoading(false);                });        }else { setPokemon({} as ResponseAPI) }        return () => controller.abort();    }, [search])    return {        pokemon,        isLoading    }}

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(() => {    const timer = setTimeout(() => setDebouncedValue(value), delay || 500)    return () => clearTimeout(timer)  }, [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

3. Manejando la lgica para el efecto Debounce.

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

Este custom hook, recibe 2 parmetros:

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

Nota: la propiedad delay la usamos el segundo parmetro de la funcin setTimeout, donde en caso de que delay sea undefined, entonces el tiempo por defecto sera 500ms.
Y tambin, 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(() => {        const timer = setTimeout(() => setDebouncedValue(value), delay || 500)        return () => clearTimeout(timer)    }, [value, delay]);    return debouncedValue}

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

Y asi nuestro componente App.tsx quedo mas limpio y fcil de leer.

Conclusin.

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.

Cdigo fuente.

GitHub logo Franklin361 / search-engine-debounce-effect

Creating a search engine with debounce effect with React JS

Search Engine - Debounce Effect

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 https://github.com/Franklin361/journal-app

2. Run this command to install the dependencies.

 npm install

3. Run this command to raise the development server.

 npm run dev

Links.


Demo of the app https://search-engine-debounce.netlify.app

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


Original Link: https://dev.to/franklin030601/como-crear-un-buscador-con-efecto-debounce-4jcp

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To