Dynamically Load Options from an API using react-select
When you need to provide a select component with tons of options, it's preferable to load it via an API. For instance, if you want to provide a select component to select a city, then it would be wise to load it from a remote source, since loading the entire city list can be network-intensive. A great tool to do this is via react-select
. It provides an asynchronous method for loading the options, which we can leverage to load options as the user types.
Requirement
This is what is expected of our select component:
- Load options from an API.
- If the user types to search for an option, the component must call the API with the text as a search parameter (like so
<api-endpoint>/todos?search=react
). - As the user types, the API call must be debounced to limit the number of API calls to the server.
Implementation
With above requirement in mind, let's implement this:
- Install the
react-select
with a package manager of your choice:npm install react-select
. react-select
expects a callback function as a parameter that returns a promise. The promise must resolve to provide the list of options. We can define it as such:
const repoSearchApiCall = (
input: string,
callback: (options: TRepository[]) => void
) => {
//Returns a promise with callback function pointing to the resolved data
return fetch(`https://api.github.com/search/repositories?q=${input}`)
.then((res) => res.json())
.then((data) => callback(data.items));
};
- We need a debounce function that limits the number of API calls we make as the user types the option they are looking for.
const debounceApiCall = (
func: (
...args: [
string,
(options: OptionsOrGroups<unknown, GroupBase<unknown>>) => void
]
) => void,
wait: number
) => {
let timeout: ReturnType<typeof setInterval> | null;
return function executedFunction(
...args: [
string,
(options: OptionsOrGroups<unknown, GroupBase<unknown>>) => void
]
) {
const later = () => {
timeout = null;
func(...args);
};
if (timeout) clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
Don’t get intimidated by the function above (except the type definition). The debounceApiCall
takes a function as a parameter where the invocation of that function is debounced with the given wait
time.
- Now we can put all these together to get our async select component.
import { useState } from "react";
import "./App.css";
import AsyncSelect from "react-select/async";
import { TRepository, TUser } from "./types";
import { GroupBase, OptionsOrGroups } from "react-select";
const repoSearchApiCall = (
input: string,
callback: (options: TRepository[]) => void
) => {
return fetch(
`https://api.github.com/search/repositories?per_page=10&page=2&q=${input}`
)
.then((res) => res.json())
.then((data) => callback(data.items));
};
const userSearchApiCall = (
input: string,
callback: (options: TUser[]) => void
) => {
//fetches something else
return fetch(`https://api.github.com/search/users?q=${input}`)
.then((res) => res.json())
.then((data) => callback(data.items));
};
function App() {
const [repo, setRepo] = useState<null | TRepository>(null);
return (
<div>
<h4>Select a repositories</h4>
<AsyncSelect
value={repo}
onChange={(newValue) => setRepo(newValue as TRepository | null)}
loadOptions={debounceApiCall(repoSearchApiCall, 700)}
getOptionLabel={(data) => (data as TRepository)?.full_name}
getOptionValue={(data) => (data as TRepository)?.id.toString()}
cacheOptions
isClearable
placeholder="Search repositories"
// defaultOptions
/>
<p>Selected repository: {repo?.full_name}</p>
<div className="divider" />
<h4>Select a user</h4>
<AsyncSelect
getOptionLabel={(data) => (data as TUser)?.login}
getOptionValue={(data) => (data as TUser)?.id.toString()}
loadOptions={debounceApiCall(userSearchApiCall, 700)}
cacheOptions
isClearable
placeholder="Search user"
// defaultOptions
/>
</div>
);
}
export default App;
const debounceApiCall = (
func: (
...args: [
string,
(options: OptionsOrGroups<unknown, GroupBase<unknown>>) => void
]
) => void,
wait: number
) => {
let timeout: ReturnType<typeof setInterval> | null;
return function executedFunction(
...args: [
string,
(options: OptionsOrGroups<unknown, GroupBase<unknown>>) => void
]
) {
const later = () => {
timeout = null;
func(...args);
};
if (timeout) clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
Preview
This meets all our requirement that we defined above. You can preview the output below.
I highly encourage you to check the network logs on this page as you search for the options to gain a better understanding of the component's behavior.
Enhancing user experience with your select component
- To indicate to the user that they can search through the list of options, it is crucial that you add a placeholder indicating the same. Something like this
placeholder='Search repo'
. - The search functionality should ideally be added when there are a lot of options, and you want to prevent expensive API calls to fetch that list. In saying that, make sure that options are already loaded when the user opens the select menu for the first time. You can do this by passing
defaultOptions
as a prop to react-select. This will ensure the data is loaded with an empty search param. - Continuing on the previous point, you can configure the API to return the most selected or relevant options when the search param is empty. For example, if you are providing a select component for your user to select a city preference for work, you can configure your API to return cities like Bangalore, Mumbai, Delhi, Pune when an empty string is passed to the search param. This will ensure that when the user opens the select menu, it is already loaded with most likely options to be selected.
- You can also cache the options for each keyword search by passing
cacheOptions
as prop to react-select. It will ensure that the API is not called twice for the same keyword search. You need not do this if you are already using a caching mechanism likereact-query
orrtk-query
.
The above changes are optional, but will greatly enhance user experience.