How to create a search engine with "debounce effect"? 🔎
Tags: react, javascript, typescript, webdev
Published: 2022-06-23 21:12:04

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.🤗

&nbsp;

Table of contents.
> 📌 [Technologies to be used.](#2)   
> 📌 [What is the "Debounce" effect?](#3)  
> 📌 [Creating the project.](#4)   
> 📌 [First steps.](#5)   
> 📌 [Creating the input.](#6)   
> 📌 [Handling the input status.](#7)   
> 📌 [Creating the function for the API request.](#8)   
> 📌 [Creating the Debounce effect.](#9)   
> 📌 [Making the API call.](#10)   
> 📌 [Creating the Pokemon.tsx component.](#11)   
> 📌 [Using our Pokemon component.](#12)   
> 📌 [Cleaning the logic of our component.](#13)   
>> 📌 [1. Handling the logic to control the input.](#14)   
>> 📌 [2. Handling the logic for the API call.](#15)   
>> 📌 [3. Handling the logic for the Debounce effect.](#16)      
>
> 📌 [Conclusion.](#17)   
>> 📌 [Source code.](#18)   

&nbsp;

## 🎈 Technologies to be used. <a id="2"></a>

- ▶️ React JS (version 18)
- ▶️ Vite JS
- ▶️ TypeScript
- ▶️ [Pokemon API](https://pokeapi.co)
- ▶️ Vanilla CSS (You can find the styles in the repository at the end of this post) 

&nbsp;


## 🎈 What is the "Debounce" effect? <a id="3"></a>

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.

&nbsp;

## 🎈 Creating the project. <a id="4"></a>

We will name the project: **`search-debounce`** (optional, you can name it whatever you like).
```bash
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.

```bash
cd search-debounce
```
Then we install the dependencies.
```bash
npm install
```
Then we open the project in a code editor (in my case VS code).
```bash
code .
```

&nbsp;

## 🎈 First steps. <a id="5"></a>

Inside the folder **`src/App.tsx`** we delete all the contents of the file and place a functional component that displays a title.
```javascript
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 👀:

![Title](https://res.cloudinary.com/dnxchppfm/image/upload/v1655994217/posts/search-debounce/01_sp9sa9.png)

&nbsp;


## 🎈 Creating the input. <a id="6"></a>

Now we create the folder **`src/components`** and inside the folder we create the file **`Input.tsx`** and inside we add the following: 

```jsx
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.

```jsx
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 👀:

![Input](https://res.cloudinary.com/dnxchppfm/image/upload/v1655994217/posts/search-debounce/02_avk6ld.png)

&nbsp;


## 🎈 Handling the input status. <a id="7"></a>

> 🚨 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. 
```js
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.
```js
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.
```jsx
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.
```js
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**.
```ts
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. 🥳

&nbsp;

## 🎈 Creating the function for the API request. <a id="8"></a>
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.


```ts
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
    }
}
```


&nbsp;


## 🎈 Creating the Debounce effect. <a id="9"></a>
In the **`App.tsx`** file we create a state, which will be used to store the value of the input.

```jsx
const [debouncedValue, setDebouncedValue] = useState();
```
As initial state we send the value of the input state (**value**).
```jsx
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`**.

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

    return () => clearTimeout(timer)
}, [value]);
```
So for the moment our App.tsx file would look like this: 
```jsx
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
```

&nbsp;


## 🎈 Making the API call. <a id="10"></a>
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.

```jsx
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.
```js
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.
```js
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**.

```js
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.
```js
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. 👀
```js
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
```

&nbsp;


## 🎈 Creating the Pokemon.tsx component. <a id="11"></a>

1 - First we create the empty functional component.

```js
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.
```js
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. 
```js
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 👀:
![Loading](https://res.cloudinary.com/dnxchppfm/image/upload/v1655994217/posts/search-debounce/03_cugi7i.png)

It should look like this when there are no results 👀:
![No results](https://res.cloudinary.com/dnxchppfm/image/upload/v1655994217/posts/search-debounce/05_olggt6.png)

It should look like this there is a pokemon 👀:
![Pokemon](https://res.cloudinary.com/dnxchppfm/image/upload/v1655994217/posts/search-debounce/04_zomwzc.png)


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.
>

```js
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. 😌

&nbsp;

## 🎈 Using our Pokemon component. <a id="12"></a>
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.

```js
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.

```js
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.

```js
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 👀:
```js
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**!

&nbsp;

## 🎈 Cleaning the logic of our component. <a id="13"></a>
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.** <a id="14"></a>
Inside the folder **`src/hooks`** we create the following file **`useInput.ts`****.
And we place the logic corresponding to the handling of the input.

```js
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.
```js
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
```

&nbsp;

### **2. Handling the logic for the API call.** <a id="15"></a>
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**.

```js
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.
```js
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
```

&nbsp;

### **3. Handling the logic for the Debounce effect.** <a id="16"></a>
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.

```js
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.
```js
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.** 🥳

&nbsp;

## 🎈 Conclusion. <a id="17"></a>
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. 🙌

&nbsp;

### 🎈 Source code. <a id="18"></a>
{%embed https://github.com/Franklin361/search-engine-debounce-effect %}