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.