Async Select

Async Select component built with React & shadcn/ui

Async select component with search functionality
Async select component with preloaded options, with local filtering.

Installation

The Async Select Component is built through the composition of <Popover /> and the <Command /> components from shadcn/ui.

See installation instructions for the Popover and the Command components.

Basic Usage

import { AsyncSelect } from "@/components/async-select";
 
function MyComponent() {
  const [value, setValue] = useState("");
 
  return (
    <AsyncSelect<DataType>
      fetcher={fetchData}
      renderOption={(item) => <div>{item.name}</div>}
      getOptionValue={(item) => item.id}
      getDisplayValue={(item) => item.name}
      label="Select"
      value={value}
      onChange={setValue}
    />
  );
}

Props

Required Props

PropTypeDescription
fetcher(query?: string) => Promise<T[]>Async function to fetch options
renderOption(option: T) => React.ReactNodeFunction to render each option in the dropdown
getOptionValue(option: T) => stringFunction to get unique value from option
getDisplayValue(option: T) => React.ReactNodeFunction to render selected value
valuestringCurrently selected value
onChange(value: string) => voidCallback when selection changes
labelstringLabel for the select field

Optional Props

PropTypeDefaultDescription
preloadbooleanfalseEnable preloading all options
filterFn(option: T, query: string) => boolean-Custom filter function for preload mode
notFoundReact.ReactNode-Custom not found message/component
loadingSkeletonReact.ReactNode-Custom loading state component
placeholderstring"Select..."Placeholder text
disabledbooleanfalseDisable the select
widthstring | number"200px"Custom width
classNamestring-Custom class for popover
triggerClassNamestring-Custom class for trigger button
noResultsMessagestring-Custom no results message
clearablebooleantrueAllow clearing selection

Examples

Async Mode

<AsyncSelect<User>
  fetcher={searchUsers}
  renderOption={(user) => (
    <div className="flex items-center gap-2">
      <Image
        src={user.avatar}
        alt={user.name}
        width={24}
        height={24}
        className="rounded-full"
      />
      <div className="flex flex-col">
        <div className="font-medium">{user.name}</div>
        <div className="text-xs text-muted-foreground">{user.role}</div>
      </div>
    </div>
  )}
  getOptionValue={(user) => user.id}
  getDisplayValue={(user) => (
    <div className="flex items-center gap-2">
      <Image
        src={user.avatar}
        alt={user.name}
        width={24}
        height={24}
        className="rounded-full"
      />
      <div className="flex flex-col">
        <div className="font-medium">{user.name}</div>
        <div className="text-xs text-muted-foreground">{user.role}</div>
      </div>
    </div>
  )}
  notFound={<div className="py-6 text-center text-sm">No users found</div>}
  label="User"
  placeholder="Search users..."
  value={selectedUser}
  onChange={setSelectedUser}
  width="375px"
/>

Preload Mode

<AsyncSelect<User>
  fetcher={searchAllUsers}
  preload
  filterFn={(user, query) => user.name.toLowerCase().includes(query.toLowerCase())}
  renderOption={(user) => (
    <div className="flex items-center gap-2">
      <Image
        src={user.avatar}
        alt={user.name}
        width={24}
        height={24}
        className="rounded-full"
      />
      <div className="flex flex-col">
        <div className="font-medium">{user.name}</div>
        <div className="text-xs text-muted-foreground">{user.role}</div>
      </div>
    </div>
  )}
  getOptionValue={(user) => user.id}
  getDisplayValue={(user) => user.name}
  label="User"
  value={selectedUser}
  onChange={setSelectedUser}
/>

TypeScript Interface

interface AsyncSelectProps<T> {
  fetcher: (query?: string) => Promise<T[]>;
  preload?: boolean;
  filterFn?: (option: T, query: string) => boolean;
  renderOption: (option: T) => React.ReactNode;
  getOptionValue: (option: T) => string;
  getDisplayValue: (option: T) => React.ReactNode;
  notFound?: React.ReactNode;
  loadingSkeleton?: React.ReactNode;
  value: string;
  onChange: (value: string) => void;
  label: string;
  placeholder?: string;
  disabled?: boolean;
  width?: string | number;
  className?: string;
  triggerClassName?: string;
  noResultsMessage?: string;
  clearable?: boolean;
}