← Back to the blog

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:

  1. Install the react-select with a package manager of your choice: npm install react-select.
  2. 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));
};
  1. 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.

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

Open in stackblitzGithub

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 like react-query or rtk-query.

The above changes are optional, but will greatly enhance user experience.