Search Input with useTimeout hook using TypeScript
Tags: react, beginners, tutorial, typescript
Published: 2023-01-11 18:23:43

Almost every project has some type of search. No matter if it's a local search in a list or you're sending server requests to get new results. In any case, it can be not really good to do searching on every user typing. So, let's create a search input with a special hook that helps us to solve this issue.

---

* [Setting up](#setting-up)
* [Search component](#input)
* [Timeout](#timeout)
* [useTimeout hook](#hook)
* [Full code](#full-code)

---
## Setting up 💡 <a name="setting-up"></a>
<br>
We won't install any additional packages, so you can start writing code for any of your projects. Although if you don't have TypeScript don't worry, just remove the typings. But I strongly recommend using it.

So, let's just use Create React App with TypeScript:

`npx create-react-app search-input --template typescript`
or
`yarn create react-app search-input --template typescript`
<br>
##Search component 🔍 <a name="input"></a>
<br>
Let's begin with our base component called `SearchInput.tsx`. For now, it's a simple input with a name list.

**SearchInput.tsx**

```typescript
const names = ['Stone, John', 'Priya, Ponnappa', 'Wong, Mia', 'Stanbrige, Peter', 'Lee-Walsh, Natalie']

function SearchInput() {
    return (
        <div>
            <input/>
            
            <ul>
                {names.map(name => <li key={name}>{name}</li>)}
            </ul>
        </div>
    );
}
```

The next step is to add an ability to search throw our list. We need a function and a state to store our filtered list. Let's add some code.


```ts
const [filteredList, setFilteredList] = useState<string[]>(names);
```
```ts
function handleFilter(event: React.ChangeEvent<HTMLInputElement>) {
    const { value } = event.target;
    const newList = names.filter(name => name.toLowerCase().includes(value.toLowerCase()));
    setFilteredList(newList);
}
```

Don't forget to add a prop function and change `names` variable.

```ts
<input onChange={handleFilter}/>

<ul>
    {filteredList.map(name => <li key={name}>{name}</li>)}
</ul>
```

Now, it works fine. It filters our list as expected. 


![filtering the list](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yns5uf6f373fmq3sv8mk.gif)


But if you have a larger table or you need to ask a server for results it can bring some issues with performance. Let's upgrade our list to have some delay before searching.

We'll use a technique called **debounce**. It means that our filter function will be postponed until the time has passed since the last `onChange` call.
<br>
## Timeout ⏱️ <a name="timeout"></a>
<br>
First, add a ref to save timeoutId. It's needed to clear a timeout if we've already had one.

```ts
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
```

Second, upgrading our filter function. First, as I've said, we need to clear the timeout, and the next step we create a new timeout with a filter inside.

```ts
function handleFilter(event: React.ChangeEvent<HTMLInputElement>) {
    const { value } = event.target;

    clearTimeout(timeoutRef.current);
    timeoutRef.current = setTimeout(() => {
        const newList = names.filter(name => name.toLowerCase().includes(value.toLowerCase()));

        setFilteredList(newList);
    }, 500);
}
``` 

**One more important thing.** Don't forget to add `clearTimeout` in `useEffect` callback in case if the component will be unmounted before `setTimeout` triggers.

```ts
useEffect(() => {
    return () => {
        clearTimeout(timeoutRef.current);
    }
}, []);
```

And now all the things work as we want them to! Let's have a look.


![filtering with timeout](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ih6jc5afl4eyk1zj4jdi.gif)

Great, but let's separate `setTimeout` in a hook in case we want to reuse it anywhere else.
<br>
## useTimeout hook 🤛 <a name="hook"></a>
<br>
No matter how you'll call it. In our example let's create a file `useTimeout.ts` and place all **debounce** code there.

**useTimeout.ts**

```ts
function useTimeout(ms = 500) {
    const timeoutRef = useRef<ReturnType<typeof setTimeout>>();

    useEffect(() => {
        return () => {
            clearTimeout(timeoutRef.current);
        }
    }, []);

    return (fnc: (...args: any[]) => void) => {
        if (timeoutRef.current) clearTimeout(timeoutRef.current);
        timeoutRef.current = setTimeout(fnc, ms);
    };
}
```

Also, as you can see, the hook can have any time to wait. By default, it'll be 500ms. This hook returns a `higher-order function` which takes a function as an argument. Inside this `higher-order function` is a code that we've already written before. And it's time to use this hook in our `SearchInput.tsx`.

First, we get a `higher-order function`.

```ts
const timeout = useTimeout();
```

When rewriting our filter function and removing all unnecessary code.

```ts
function handleFilter(event: React.ChangeEvent<HTMLInputElement>) {
    const { value } = event.target;

    timeout(() => {
        const newList = names.filter(name => name.toLowerCase().includes(value.toLowerCase()));
        setFilteredList(newList);
    });
}
```

Full code here: <a name="full-code"></a>

{% details **useTimeout.ts** %} 

```ts
import { useEffect, useRef } from 'react';

export default function useTimeout(ms = 500) {
    const timeoutRef = useRef<ReturnType<typeof setTimeout>>();

    useEffect(() => {
        return () => {
            clearTimeout(timeoutRef.current);
        }
    }, []);

    return (fnc: (...args: any[]) => void) => {
        if (timeoutRef.current) clearTimeout(timeoutRef.current);
        timeoutRef.current = setTimeout(fnc, ms);
    };
}
```
{% enddetails %}

{% details **SearchInput.tsx** %} 
```ts
import React, { useState } from 'react';
import useTimeout          from './useTimeout';

const names = ['Stone, John', 'Priya, Ponnappa', 'Wong, Mia', 'Stanbrige, Peter', 'Lee-Walsh, Natalie']

export default function SearchInput() {
    const [filteredList, setFilteredList] = useState<string[]>(names);
    const timeout = useTimeout();

    function handleFilter(event: React.ChangeEvent<HTMLInputElement>) {
        const { value } = event.target;

        timeout(() => {
            const newList = names.filter(name => name.toLowerCase().includes(value.toLowerCase()));
            setFilteredList(newList);
        });
    }

    return (
        <div>
            <input onChange={handleFilter}/>

            <ul>
                {filteredList.map(name => <li key={name}>{name}</li>)}
            </ul>
        </div>
    );
}
```
{% enddetails %}

---

So, that's it. If you haven't already used such a debounce hack, I hope you'll find the place for it. If you're familiar with it, I ask you to share in which cases you've already used it.