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>
);
}
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);
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.
<input onChange={handleFilter}/>
<ul>
{filteredList.map(name => <li key={name}>{name}</li>)}
</ul>
Now, it works fine. It filters our list as expected.
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>>();
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);
}
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);
}
}, []);
And now all the things work as we want them to! Let's have a look.
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);
};
}
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();
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);
});
}
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);
};
}
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>
);
}
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)
It was helpful for me. Used this in my project.