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.
}
As you can see, this hook provides you with four things:
fetching
: a variable to help display when a search is in progress
refresh
: a method that manually triggers a search
setSearch
: a method to update the search value
data
: the return value of your fetch
function passed as a parameter
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>
The hook itself includes a few handy features, like:
data
variable is automatically typed based on what you return in the fetch
function. That lets you confidently write the rest of your app logic. This is done by using a generic component.
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,
};
}