1

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:

  • Search something, eg: Budget
  • After the results are shown, hold backspace, when the field is cleared, search results are shown for b
  • Issue is not there if I select all the text in the field and delete it using backspace.
Shoaib Ahmed
  • 101
  • 8

1 Answers1

2

Issue

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".

Solution

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
/>
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • 1
    btw useMemo is only for optimization right? – Shoaib Ahmed Aug 13 '21 at 07:47
  • 1
    @ShoaibAhmed Yes. Since this is a function component you don't generally want to redeclare the debounced callback each render. I did try this out in a codesandbox without the `useMemo` hook and it still appeared to work, but I suspect that was only because the input is an uncontrolled input. – Drew Reese Aug 13 '21 at 07:50
  • in this case, the handler is a function, therefore we'd better `useCallback` instead of `useMemo`, the latter being mostly used to memoize __values__ that are computationally expensive. `useCallback` will memoize the function itself. – Madrus Mar 29 '23 at 09:51
  • 1
    @Madrus I wouldn't necessarily agree that it's ***objectively better*** to use the `useCallback` hook here. `useCallback` and `useMemo` are *nearly* identical. See [useCallback](https://legacy.reactjs.org/docs/hooks-reference.html#usecallback) and note that "`useCallback(fn, deps)` is equivalent to `useMemo(() => fn, deps)`". They are both used to memoize *a value*, `useCallback` is a "special case" where the value is specifically *a function*. – Drew Reese Mar 29 '23 at 14:58
  • @Madrus To your point though, `useCallback(debounce(onChange, 300), []);` *should* work about the same, but I think `debounce(onChange, 300)` will be called each time the component is rendered, which incurs a slight cost. Using the `useMemo` hook it's only ever called when the hook's callback is invoked, i.e. when a dependency changes. – Drew Reese Mar 29 '23 at 15:03