So I'm using Next.js and built a basic search page with input and storing the results after the query on a state array. The problem is when I clear the input field fast using backspace, it shows the result from the last keyword.
I believe I'm using the React state in the wrong manner.
Here is how I'm querying the search using meilisearch:
const [search, setSearch] = useState([]);
const [showSearch, setShowSearch] = useState(false);
async function onChange(e) {
if (e.length > 0) {
await client.index('sections')
.search(e)
.then((res) => {
const list = res.hits.map((elm) => ({
title: elm.Title,
content: elm.formattedContent
}));
setSearch(list);
setShowSearch(true);
});
} else {
setSearch([]);
setShowSearch(false);
}
}
and here is the input field and the search results:
<div className="searchPage wrapper">
<input
type="text"
aria-label="Search through site content"
placeholder="Search your keywords"
onChange={(e) => onChange(e.target.value);}
/>
{showSearch && (
<div className="searchPageResults">
<p className="suggested">Top Results</p>
{search.length > 0 ? (
<ul>
{search.map((item, index) => (
<li key={`search-${index}`}>
<Link href={`/${item.slug}`}>
<a role="link">
{item.title}
</a>
</Link>
<p>{item.content}</p>
</li>
))}
</ul>
) : (
<p className="noResults">No results found</p>
)}
</div>
)}
</div>
What are the best practices to prevent this kind of situation?
You can check the live implementation here: https://budgetbasics.openbudgetsindia.org/search
To reproduce the issue:
Budget
b
I suspect that when you quickly backspace that the last request made with "b" resolves asynchronously after the last onChange
call is made where e.length > 0
is false. The search
state is updated to an empty array and once the final asynchronous request resolves the search
state is updated with the result of "b".
One possible solution would be to debounce the onChange
handler so useless requests aren't made for fast typers. debounce from lodash is a common utility. I used a delay of 300ms
, but this is obviously tunable to suit your needs and what feels best for you or your general users.
import debounce from 'lodash/debounce';
async function onChange(e) {
if (e.length > 0) {
await client.index('sections')
.search(e)
.then((res) => {
const list = res.hits.map((elm) => ({
title: elm.Title,
content: elm.formattedContent
}));
setSearch(list);
setShowSearch(true);
});
} else {
setSearch([]);
setShowSearch(false);
}
}
const debouncedOnChange = useMemo(() => debounce(onChange, 300), []);
...
<input
type="text"
aria-label="Search through site content"
placeholder="Search your keywords"
onChange={(e) => debouncedOnChange(e.target.value)} // <-- use debounced handler
/>