The purpose of this post is to show a simple way on how to make a small search engine with a debounce effect.
Such a project can be extended in many ways, but I will try to make it something basic but efficient.
π¨ Note: This post requires you to know the basics of React with TypeScript (basic hooks and fetch requests).
Any kind of Feedback is welcome, thanks and I hope you enjoy the article.π€
Β
Table of contents.
π Technologies to be used.
π What is the "Debounce" effect?
π Creating the project.
π First steps.
π Creating the input.
π Handling the input status.
π Creating the function for the API request.
π Creating the Debounce effect.
π Making the API call.
π Creating the Pokemon.tsx component.
π Using our Pokemon component.
π Cleaning the logic of our component.π 1. Handling the logic to control the input.
π 2. Handling the logic for the API call.
π 3. Handling the logic for the Debounce effect.π Conclusion.
π Source code.
Β
π Technologies to be used.
- βΆοΈ React JS (version 18)
- βΆοΈ Vite JS
- βΆοΈ TypeScript
- βΆοΈ Pokemon API
- βΆοΈ Vanilla CSS (You can find the styles in the repository at the end of this post)
Β
π What is the "Debounce" effect?
The debounce effect is when they are not executed at the time of their invocation. Instead, their execution is delayed for a predetermined period of time. If the same function is invoked again, the previous execution is cancelled and the timeout is restarted.
Β
π Creating the project.
We will name the project: search-debounce
(optional, you can name it whatever you like).
npm init vite@latest
We create the project with Vite JS and select React with TypeScript.
Then we execute the following command to navigate to the directory just created.
cd search-debounce
Then we install the dependencies.
npm install
Then we open the project in a code editor (in my case VS code).
code .
Β
π First steps.
Inside the folder src/App.tsx
we delete all the contents of the file and place a functional component that displays a title.
const App = () => {
return (
<div className="container">
<h1> <span>Search Engine</span> whit <span>Debounce Effect</span> </h1>
</div>
)
}
export default App
It should look like this π:
Β
π Creating the input.
Now we create the folder src/components
and inside the folder we create the file Input.tsx
and inside we add the following:
export const Input = () => {
return (
<>
<label htmlFor="pokemon">Name or ID of a Pokemon</label>
<input type="text" id="pokemon" placeholder="Example: Pikachu" />
</>
)
}
Once done, we import it into the App.tsx
file.
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
It should look like this π:
Β
π Handling the input status.
π¨ Note: This is NOT the only way to perform this exercise, it is only one option! If you have a better way, I would like you to share it in the comments please. π
In this case I am going to handle the input status at a higher level, i.e. the App component of the App.tsx
file.
We will do this, because we need the value of the input available in App.tsx
, since the request to the API and the debounce effect will be made there.
1 - First we create the state to handle the value of the input.
const [value, setValue] = useState('');
2 - We create a function to update the state of the input when the input makes a change.
This function receives as parameter the event that emits the input, of this event we will obtain the property target and then the property value, which is the one that we will send to our state.
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value);
3 - Therefore, it is time to send the function and the value of the status to the 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 - In the Input component we add an interface to receive the properties by parameter in the Input.tsx
file.
interface Props {
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
5 - We unstructure the properties and add them to the input.
The onChange function, we place it in the onChange property of the input and the same with the value property 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}
/>
</>
)
}
And so we already have the status of our input under control. π₯³
Β
π Creating the function for the API request.
Now we create the src/utils
folder and inside we place a file called searchPokemon.ts
and add the following function to make the request, and search for a pokemon by its name or ID.
π¨ Note: The API response has more properties than what is represented in the ResponseAPI interface.
This function receives two parameters:
- pokemon: is the name or ID of the pokemon.
- signal**: allows to set event listeners. In other words, it will help us to cancel the HTTP request when the component is unmounted or makes a change in the state.
This function returns the pokemon data if everything goes well or null if something goes wrong.
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
}
}
Β
π Creating the Debounce effect.
In the App.tsx
file we create a state, which will be used to store the value of the input.
const [debouncedValue, setDebouncedValue] = useState();
As initial state we send the value of the input state (value).
const [value, setValue] = useState('');
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value);
const [debouncedValue, setDebouncedValue] = useState(value);
Now, we create an effect so that when the value of the input changes, we execute the setTimeout function that will update the state of the debouncedValue sending the new value of the input, after 1 second, and thus we will obtain the keyword or the pokemon, to make the request to the API.
At the end of the effect, we execute the cleaning method, which consists of cleaning the setTimeout function, that is why we store it in a constant called timer
.
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), 1000)
return () => clearTimeout(timer)
}, [value]);
So for the moment our App.tsx file would look like this:
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
Β
π Making the API call.
Once we have the value of the input already with the debounce effect, it is time to make the API call.
For that we will use the function that we created previously, searchPokemon.tsx
.
For it, we are going to use an effect.
First we create the controller
which is the one that will help us to cancel the HTTP request, as we mentioned before.
Inside the controller we have two properties that interest us:
- abort(): when executed, cancels the request.
- signal**: maintains the connection between the controller and the request to know which one to cancel.
The abort() is executed at the end, when the component is unmounted.
useEffect(() => {
const controller = new AbortController();
return () => controller.abort();
}, []);
The dependency of this effect will be the value of the debouncedValue, since every time this value changes, we must make a new request to search for the new pokemon.
useEffect(() => {
const controller = new AbortController();
return () => controller.abort();
}, [debouncedValue])
We make a condition, in which only if the debouncedValue exists and has some word or number, we will make the request.
useEffect(() => {
const controller = new AbortController();
if (debouncedValue) {
}
return () => controller.abort();
}, [debouncedValue])
Inside the if we call the searchPokemon function and send it the value of debouncedValue and also the signal property of the controller.
useEffect(() => {
const controller = new AbortController();
if (debouncedValue) {
searchPokemon(debouncedValue, controller.signal)
}
return () => controller.abort();
}, [debouncedValue])
And since the searchPokemon function returns a promise and within the effect it is not allowed to use async/await, we will use .then to resolve the promise and get the value it returns.
useEffect(() => {
const controller = new AbortController();
if (debouncedValue) {
searchPokemon(debouncedValue, controller.signal)
.then(data => {
console.log(data) //pokemon | null
})
}
return () => controller.abort();
}, [debouncedValue])
In the end it should look like this. π
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
Β
π Creating the Pokemon.tsx component.
1 - First we create the empty functional component.
export const Pokemon = () => {
return (
<></>
)
}
2 - We add the ResponseAPI
interface since we are going to receive by props the pokemon, which can contain the pokemon data or a null value.
import { ResponseAPI } from "../utils/searchPokemon"
export const Pokemon = ({ pokemon }: { pokemon: ResponseAPI | null }) => {
return (
<></>
)
}
3 - We make an evaluation where:
- If the pokemon property is null, we show the "No results" message.
- If the pokemon property contains the pokemon data, we show its name and an image.
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>
}
</>
)
}
It should look like this if it is loading π:
It should look like this when there are no results π:
It should look like this there is a pokemon π:
4 - And now finally, we add a last condition, where we evaluate if the pokemon exists (i.e. it is not null) and if it is an empty object we return a fragment.
This is because the initial state for storing pokemon will be an empty object. "{}".
If we don't put that condition, then at the start of our app, even without having typed anything in the input, the "No results" message will appear, and the idea is that it will appear after we have typed something in the input and the API call has been made.
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>
}
</>
)
}
This is how our pokemon component would look like, it's time to use it. π
Β
π Using our Pokemon component.
In the App.tsx file we will add 2 new states:
- To store the pokemon found, which will have an initial value of an empty object.
- To handle a loading on what the API call is made, which will have an initial value of false.
const [pokemon, setPokemon] = useState<ResponseAPI | null>({} as ResponseAPI);
const [isLoading, setIsLoading] = useState(false)
Now inside the effect where we make the call to the API through the function searchPokemon
, before making the call we send the value of true to the setIsLoading to activate the loading.
Then, once we get the data inside the .then we send the data to the setPokemon (which can be the pokemon or a null value).
And finally we send the value of false to setIsLoading to remove the loading.
useEffect(() => {
const controller = new AbortController();
if (debouncedValue) {
setIsLoading(true)
searchPokemon(debouncedValue, controller.signal)
.then(data => {
setPokemon(data);
setIsLoading(false);
})
}
return () => controller.abort();
}, [debouncedValue])
Once the pokemon is stored, in the JSX we place the following condition:
- If the value of the isLoading status is true, we display the "Loading Results... " message.
- If the value of the isLoading status is false, we show the Pokemon component, sending it the 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>
)
And everything together would look like this π:
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
That's a lot of logic in one component right? π±
Now it's our turn to refactor!
Β
π Cleaning the logic of our component.
We have a lot of logic in our component so it is necessary to separate it into several files:
- Logic to control the input.
- Debounce logic.
- Logic to make the API call and handle the pokemon. And as this logic makes use of hooks like useState and useEffect, then we must place them in a custom hook.
The first thing will be to create a new folder src/hooks
.
1. Handling the logic to control the input.
Inside the folder src/hooks
we create the following file useInput.ts
**.
And we place the logic corresponding to the handling of the 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]
}
Then we call useInput in the App.tsx
file.
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. Handling the logic for the API call.
Inside the folder src/hooks
we create the following file useSearchPokemon.ts
.
We place the logic related to make the request to the API and show the pokemon.
This custom hook receives as parameter a string called search, which is the name of the pokemon or the ID. And we send that parameter to the function that makes the API call searchPokemon.
π¨ Note: Observe the If part in the effect, at the end we place an else where if the debouncedValue is empty, we will not make a call to the API and we send the value of an empty object to 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
}
}
Then we call useSearchPokemon in the App.tsx
file.
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. Handling the logic for the Debounce effect.
Inside the folder src/hooks
we create the following file useDebounce.ts
and place all the logic to handle the debounce effect.
This custom hook, receives 2 parameters:
- value: is the value of the input status.
- delay**: is the amount of time you want to delay the debounce execution and is optional.
π¨ Note: the delay property is used as the second parameter of the setTimeout function, where in case delay is undefined, then the default time will be 500ms.
And also, we add the delay property to the effect dependencies array.
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
}
Then we call useDebounce in the App.tsx
file.
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
And so our App.tsx component was cleaner and easier to read. π₯³
Β
π Conclusion.
The whole process I just showed, is one of the ways you can make a search engine with debounce effect. π
I hope I helped you understand how to do this exercise,thank you very much for making it this far! π€
I invite you to comment if you know any other different or better way of how to make a debounce effect for a search engine. π
Β
π Source code.
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 π
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
Here's the link to the tutorial in case you'd like to take a look at it! π
-
π Article in English πΊπΈ https://dev.to/franklin030601/how-to-create-a-search-engine-with-debounce-effect-4hef
-
π Article in Spanish π²π½ https://dev.to/franklin030601/como-crear-un-buscador-con-efecto-debounce-4jcp
Top comments (2)
Thanks for this information!
It was really useful for me! β£οΈ
Thanks for writing this up with so much detail, super helpful!