¿Cómo crear un buscador con 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.🤗
Tabla de contenido
📌 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.
🎈 Tecnologías a utilizar.
▶️ React JS (version 18) ▶️ Vite JS ▶️ TypeScript ▶️ Pokemon API
▶️ CSS 바닐라(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 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
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
Después abrimos el proyecto en un editor de código (en mi caso VS code).
code .
🎈프리메로스 파소.
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
Debería de verse así 👀:
🎈 Creando 엘 입력.
Ahora creamos la carpeta
src/components
y dentro de la carpeta creamos el archivoInput.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
Debería de verse así 👀:
🎈 Manejando el estado del 입력.
🚨 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('');
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);
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
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; }
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} /> </> ) }
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 llamadosearchPokemon.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 } }
🎈 Creando el efecto 디바운스.
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 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]);
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(() => { 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 función que creamos con anterioridad,searchPokemon.tsx
.Para ello, vamos a usar un efecto.
Primero creamos elcontroller
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(); }, []);
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])
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])
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])
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 => { console.log(data) //pokemon | null }) } return () => controller.abort(); }, [debouncedValue])
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(() => { 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
🎈 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 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
1061709610 1061906import { 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> } </> ) }
Debería de verse asi cuando no hay resultados 👀:
Debería de verse asi si esta cargando 👀:
Debería de verse asi hay un 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 s 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 | 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 quedaría nuestro componente pokemon, es hora de usarlo. 😌
🎈 우산도 누에스트로 구성 포켓몬.
En el archivo App.tsx agregaremos 2 새로운 항목: Para almacenar el pokemon encontrado, que tendrá un valor inicial de un objeto vacío. Para manejar unloading en lo que se hace la llamada a la API, que tendrá un valor inicial de falso. const [포켓몬, setPokemon] = useState<ResponseAPI | null>({} as ResponseAPI); const [isLoading, setIsLoading] = useState(거짓) API를 사용하여 searchPokemon을 중재하고, 진정한 용맹을 설정하기 위한 비용을 지불하기 위해 활성화 로드 중 로드하는 중입니다. 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 컨트롤러 = new AbortController(); if (debouncedValue) { setIsLoading(참) searchPokemon(debouncedValue, controller.signal) .then(데이터 => { setPokemon(데이터); setIsLoading(거짓); }) } return () => controller.abort(); }, [디바운스된 값]) 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. 반품 ( <사업부 클래스 이름="컨테이너"> <h1> <span>검색 엔진</span> 및 <span>디바운스 효과</span> </h1> <입력 {...{ 값, onChange }} /> { isLoading ? <span>결과 로드 중...</span> : <포켓몬 포켓몬={포켓몬}/> } </div> ) Y todo junto quedaría asi 👀: import { useEffect, useState } from 'react'; "./components/Input"에서 가져오기 { 입력 } "./components/Pokemon"에서 { 포켓몬 } 가져오기; "./utils/searchPokemon"에서 가져오기 { searchPokemon }; import { ResponseAPI } from "./interface/pokemon"; const 앱 = () => { const [포켓몬, setPokemon] = useState<ResponseAPI | null>({} as ResponseAPI); const [isLoading, setIsLoading] = useState(거짓) const [값, setValue] = useState(''); const onChange = (e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value); const [debouncedValue, setDebouncedValue] = useState(값); useEffect(() => { const 타이머 = setTimeout(() => setDebouncedValue(값), 지연 || 500) return () => clearTimeout(타이머) }, [값, 지연]); useEffect(() => { const 컨트롤러 = new AbortController(); if (debouncedValue) { setIsLoading(참) searchPokemon(debouncedValue, controller.signal) .then(데이터 => { setPokemon(데이터); setIsLoading(거짓); }) } return () => controller.abort(); }, [디바운스된 값]) 반품 ( <사업부 클래스 이름="컨테이너"> <h1> <span>검색 엔진</span> 및 <span>디바운스 효과</span> </h1> <입력 {...{ 값, onChange }} /> { isLoading ? <span>결과 로드 중...</span> : <포켓몬 포켓몬={포켓몬}/> } </div> ) } 기본 앱 내보내기 Es mucha lógica en un solo componente cierto? 😱 지금 당장 refactorizar! 🎈 Limpiando la lógica de nuestro componente.
Tenemos mucha lógica en nuestro componente por lo que es necesario separarla en diversos archivos: Logica para controlar el 입력. 디바운스 로직. Logica para hacer la llamada a la API y manejar el pokemon. 후크를 사용하여 사용 상태 및 사용 효과를 사용하면 사용자 지정 후크에서 데베모스 colocarlos를 유도할 수 있습니다. Lo primero sera crear una nueva carpeta src/hooks 1. Manejando la logica para controlar el 입력.
Dentro de la carpeta src/hooks creamos el siguiente archivo useInput.ts Y colocamos la lógica는 입력에 대응합니다. import { useState } from 'react'; export const useInput = (): [string, (e: React.ChangeEvent<HTMLInputElement>) => 무효] => { const [값, setValue] = useState(''); const onChange = (e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value); 반환 [값, onChange] } Luego llamamos al useInput en el archivo App.tsx import { useEffect, useState } from 'react'; "./components/Input"에서 가져오기 { 입력 } "./components/Pokemon"에서 { 포켓몬 } 가져오기; import { useInput } from "./hooks/useInput"; "./utils/searchPokemon"에서 가져오기 { searchPokemon }; import { ResponseAPI } from "./interface/pokemon"; const 앱 = () => { const [값, onChange] = useInput(); const [포켓몬, setPokemon] = useState<ResponseAPI | null>({} as ResponseAPI); const [isLoading, setIsLoading] = useState(거짓) const [debouncedValue, setDebouncedValue] = useState(값); useEffect(() => { const 타이머 = setTimeout(() => setDebouncedValue(값), 지연 || 500) return () => clearTimeout(타이머) }, [값, 지연]); useEffect(() => { const 컨트롤러 = new AbortController(); if (debouncedValue) { setIsLoading(참) searchPokemon(debouncedValue, controller.signal) .then(데이터 => { setPokemon(데이터); setIsLoading(거짓); }) } return () => controller.abort(); }, [디바운스된 값]) 반품 ( <사업부 클래스 이름="컨테이너"> <h1> <span>검색 엔진</span> 및 <span>디바운스 효과</span> </h1> <입력 {...{ 값, onChange }} /> { isLoading ? <span>결과 로드 중...</span> : <포켓몬 포켓몬={포켓몬}/> } </div> ) } 기본 앱 내보내기 2. Manejando la lógica para la llamada a la API.
useSearchPokemon.ts . Colocamos la lógica relacionada con hacer la petición a la API y mostrar el el pokemon. Este custom hook recibe como parametro un string llamado search, que es el nombre del pokemon o el ID. Y ese parametro se lo enviamos a la función que hace la llamada a la API searchPokemon 🚨 참고 사항: 관찰 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'; 내보내기 const useSearchPokemon = (검색: 문자열) => { const [포켓몬, setPokemon] = useState<ResponseAPI | null>({} as ResponseAPI); const [isLoading, setIsLoading] = useState(거짓) useEffect(() => { const 컨트롤러 = new AbortController(); if (검색) { setIsLoading(참); searchPokemon(검색, 컨트롤러.신호) .then(데이터 => { setPokemon(데이터); setIsLoading(거짓); }); }else { setPokemon({}을 ResponseAPI로) } return () => controller.abort(); }, [검색]) 반품 { 포켓몬, isLoading } } Luego llamamos al useSearchPokemon en el archivo App.tsx import { useEffect, useState } from 'react'; "./components/Input"에서 가져오기 { 입력 } "./components/Pokemon"에서 { 포켓몬 } 가져오기; import { useInput } from "./hooks/useInput"; import { useSearchPokemon } from "./hooks/useSearchPokemon"; "./utils/searchPokemon"에서 가져오기 { searchPokemon }; import { ResponseAPI } from "./interface/pokemon"; const 앱 = () => { const [값, onChange] = useInput(); const [debouncedValue, setDebouncedValue] = useState(값); const { isLoading, 포켓몬 } = useSearchPokemon(debouncedValue) useEffect(() => { const 타이머 = setTimeout(() => setDebouncedValue(값), 지연 || 500) return () => clearTimeout(타이머) }, [값, 지연]); 반품 ( <사업부 클래스 이름="컨테이너"> <h1> <span>검색 엔진</span> 및 <span>디바운스 효과</span> </h1> <입력 {...{ 값, onChange }} /> { isLoading ? <span>결과 로드 중...</span> : <포켓몬 포켓몬={포켓몬}/> } </div> ) } 기본 앱 내보내기 3. 디바운스를 없애기 위한 논리.
Dentro de la carpeta src/hooks creamos el siguiente archivo useDebounce.ts y colocamos toda la lógica para manejar el efecto debounce. 에스테 커스텀 훅, 2개의 매개변수: 값: es el valor del estado del 입력. 지연: es la cantidad de tiempo que quieres retrasar la ejecución del debounce y es opcional. 🚨 참고: 지연 시간은 setTimeout의 기능 매개 변수 매개변수로 사용되며 정의되지 않은 경우에는 지연 시간이 500ms로 지연됩니다. Y también, agregamos la propiedad delay al arreglo de dependsencias del efecto. import { useState, useEffect } from 'react'; 내보내기 const useDebounce = (값:문자열, 지연?:숫자) => { const [debouncedValue, setDebouncedValue] = useState(값); useEffect(() => { const 타이머 = setTimeout(() => setDebouncedValue(값), 지연 || 500) return () => clearTimeout(타이머) }, [값, 지연]); 디바운스 값 반환 } Luego llamamos al useDebounce en el archivo App.tsx import { useEffect, useState } from 'react'; "./components/Input"에서 가져오기 { 입력 } "./components/Pokemon"에서 { 포켓몬 } 가져오기; import { useInput } from "./hooks/useInput"; import { useSearchPokemon } from "./hooks/useSearchPokemon"; import { useDebounce } from "./hooks/useDebounce"; "./utils/searchPokemon"에서 가져오기 { searchPokemon }; import { ResponseAPI } from "./interface/pokemon"; const 앱 = () => { const [값, onChange] = useInput(); const debouncedValue = useDebounce(값, 1000); const { isLoading, 포켓몬 } = useSearchPokemon(debouncedValue) 반품 ( <사업부 클래스 이름="컨테이너"> <h1> <span>검색 엔진</span> 및 <span>디바운스 효과</span> </h1> <입력 {...{ 값, onChange }} /> { isLoading ? <span>결과 로드 중...</span> : <포켓몬 포켓몬={포켓몬}/> } </div> ) } 기본 앱 내보내기 Y asi nuestro componente App.tsx quedo mas limpio y fácil de leer. 🥳 🎈결론.
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. 🙌 🎈코디고 푸엔테.
/
React JS로 디바운스 효과가 있는 검색 엔진 만들기 🚀
검색 엔진 - 디바운스 효과 🔍
React JS 및 Pokemon API로 디바운스 효과가 있는 검색 엔진 만들기 🚀
Franklin361
기술 🧪
반응 JS 타자기 Vite JS
설치. 🚀
1. 리포지토리 복제
git clone https://github.com/Franklin361/journal-app
2. 이 명령을 실행하여 종속성을 설치합니다.
npm install
3. 이 명령을 실행하여 개발 서버를 올립니다.
npm run dev
연결. ⛓️
앱 데모 🔗 search-engine-debounce-effect
보고 싶은 경우를 대비하여 튜토리얼에 대한 링크가 있습니다! 👀
🔗 영어 기사 🇺🇸
🔗 스페인어 기사 🇲🇽
Reference
이 문제에 관하여(¿Cómo crear un buscador con efecto "debounce"? 🔎), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/franklin030601/como-crear-un-buscador-con-efecto-debounce-4jcp텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)