descriptive text Hammerbot
descriptive text
react
typescript

An async search hook for React

The Concept

So I’ve been having fun rewriting little concepts I used a lot on the Vue side.

Like this abstraction that allows you to list items and perform an asynchronous search on them. Super useful in the case of a combobox with a very large list of options, or for querying a third-party service.

I called it useAsyncSearch(). Here’s basically how it works:

tsx
CopyEdit
export function Main () {
	const {
	  fetching,
	  refresh,
	  setSearch,
	  data
	} = useAsyncSearch({
	  async fetch ({ search }) {
	    // => Return whatever items you want
	    // asynchronously, so you can even
	    // hit a server here
		  return ["blue", "green", "red"].filter(item => {
		    if (!search) {
		      return true
		    }
		    return item.includes(search)
		  })
	  }
	})

	// The rest of your component goes here.
}

typescript

As you can see, this hook provides you with four things:

With those four elements, you can build a neat little component:

tsx
CopyEdit
<div>
  <input onInput={(e) => setSearch(e.target.value)} />
  {fetching && "Fetching..."}
  {data && (
	  <ul>
		  {data.map(item => <li>{item}</li>)}
	  </ul>
  )}
  <button onClick={refresh} disabled={fetching}>Refresh</button>
</div>

typescript

How does it work?

The hook itself includes a few handy features, like:

tsx
CopyEdit
import { useEffect, useRef, useState } from "react";

export type AsyncSearchProps<T> = {
  fetch: ({ search }: { search: string }) => Promise<T>;
};

export function useAsyncSearch<T>(props: AsyncSearchProps<T>) {
  // We setup our state; refs are for internal logic,
  // states are for rendering.
  const [fetching, setFetching] = useState(false);
  const [resultState, setResultState] = useState<T | null>(null);
  const search = useRef("");
  const fetchCount = useRef(0);

  // Refresh uses a simple counter to avoid
  // flickering and race conditions
  const refresh = async () => {
    fetchCount.current++;
    setFetching(true);
    const currentCount = fetchCount.current;
    try {
      const result = await props.fetch({
        search: search.current,
      });
      if (currentCount === fetchCount.current) {
        setResultState(result);
        setFetching(false);
      }
    } catch (error) {
      console.error(error);
      if (currentCount === fetchCount.current) {
        setFetching(false);
      }
    }
  };

  // Run an initial fetch on mount
  useEffect(() => {
    refresh();
  }, []);

  // Return everything the consumer needs
  return {
    fetching,
    refresh,
    setSearch: (searchValue: string) => {
      search.current = searchValue;
      refresh();
    },
    data: resultState,
  };
}
typescript

Chargement des commentaires