DEV Community

Cover image for Search Input with useTimeout hook using TypeScript
Sviatoslav
Sviatoslav

Posted on

Search Input with useTimeout hook using TypeScript

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 ๐Ÿ’ก



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

Search component ๐Ÿ”



Let's begin with our base component called SearchInput.tsx. For now, it's a simple input with a name list.

SearchInput.tsx

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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

<input onChange={handleFilter}/>

<ul>
    {filteredList.map(name => <li key={name}>{name}</li>)}
</ul>
Enter fullscreen mode Exit fullscreen mode

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

filtering the list

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.

Timeout โฑ๏ธ



First, add a ref to save timeoutId. It's needed to clear a timeout if we've already had one.

const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
Enter fullscreen mode Exit fullscreen mode

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.

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);
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

filtering with timeout

Great, but let's separate setTimeout in a hook in case we want to reuse it anywhere else.

useTimeout hook ๐Ÿค›



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

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);
    };
}
Enter fullscreen mode Exit fullscreen mode

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.

const timeout = useTimeout();
Enter fullscreen mode Exit fullscreen mode

When rewriting our filter function and removing all unnecessary code.

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

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

Full code here:

useTimeout.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);
    };
}
Enter fullscreen mode Exit fullscreen mode

SearchInput.tsx
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>
    );
}
Enter fullscreen mode Exit fullscreen mode


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.

Top comments (1)

Collapse
 
shmihshmih profile image
Shmih Shmih

It was helpful for me. Used this in my project.