Option Sources
Use optionSource when options come from somewhere other than local <option> children. The source might call an API,
read IndexedDB, query a cache, or compute options from application state.
Creating an Option Source
An API-backed option source translates the request into the format expected by the endpoint:
import { SuperSelect, useOptionSource } from "super-select-react";
function CitySelect() {
const citySource = useOptionSource(async ({ values, search, offset, limit, after, signal }) => {
const response = await fetch("/api/cities/options", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ values, search, offset, limit, after: after?.value }),
signal,
});
if (!response.ok) {
throw new Error(`Unable to load cities: ${response.status}`);
}
return response.json();
});
return <SuperSelect name="city" optionSource={citySource} />;
}
An option source can also work directly with something like a local array:
import { SuperSelect, useOptionSource } from "super-select-react";
const cities = [
{ value: "austin", label: "Austin" },
{ value: "chicago", label: "Chicago" },
{ value: "seattle", label: "Seattle" },
];
function CitySelect() {
const citySource = useOptionSource(async ({ values, search = "", offset = 0, limit = 100 }) => {
const normalizedSearch = search.trim().toLowerCase();
const matchingCities = cities.filter((city) => {
if (values) {
return values.includes(city.value);
}
return city.label.toLowerCase().includes(normalizedSearch);
});
return {
options: matchingCities.slice(offset, offset + limit),
hasMore: offset + limit < matchingCities.length,
};
});
return <SuperSelect name="city" optionSource={citySource} />;
}
useOptionSource keeps the same source until its dependencies change. When the fetch logic reads a prop or state value, pass a
dependency list as the second argument, like useMemo:
import { SuperSelect, useOptionSource } from "super-select-react";
function CitySelect({ countryCode }: { countryCode: string }) {
const citySource = useOptionSource(
async ({ values, search, offset, limit, signal }) => fetchCityOptions({ countryCode, values, search, offset, limit, signal }),
[countryCode],
);
return <SuperSelect name="city" optionSource={citySource} />;
}
Skip the hook and call createOptionSource directly when the source lives outside a component or when you want to control
memoization yourself with useMemo:
import { useMemo } from "react";
import { createOptionSource, SuperSelect } from "super-select-react";
function CitySelect({ countryCode }: { countryCode: string }) {
const citySource = useMemo(() => createOptionSource(fetchCityOptions), []);
return <SuperSelect name="city" optionSource={citySource} />;
}
Display Mode
Single Select
Multi Select
Never Resolves
Limited Pagination
Starts in Pending State
Requests and Responses
The fetch function you pass to useOptionSource handles two kinds of requests:
- Load a list of options, optionally filtered by a search term and paginated.
- Resolve options for a set of values so their labels can be displayed when they are not in the default list of options.
The function receives a request object that may contain:
values: The saved option values to resolve.search: The current search text.limit: The maximum number of options to return.offset: The number of matching options to skip, for offset-based pagination.after: The last option from the previous page, for cursor-based pagination.signal: AnAbortSignalthat indicates when the request is no longer needed.
The function returns a promise for an object containing:
options: An array of matching options. Each option requiresvalueandlabel.hasMore(optional): Whether another page of matching options is available.
When a response has hasMore: true, Super Select can request another page. The next request advances offset by the
number of options already returned and sets after to the last option from the previous page.
For each query, the modal and option-list modes display the first page and allow up to two additional pages to be
loaded. If more options are still available after that, the load-more action is replaced by an overflow indicator and
the user can narrow the results with a search. Change the limit with customization.maxAdditionalPages.
Option Shape
Source options use the same shape everywhere. Every option needs a submitted value and a user-facing label.
type Option = {
value: string;
label: string;
children?: React.ReactNode;
groupLabel?: string;
disabled?: boolean;
hidden?: boolean;
data?: unknown;
};
Use disabled for options that should be visible but not selectable. Use hidden for options that should not appear in
the rendered list. data is available when pagination, a custom search, or custom rendering needs extra metadata.
Use children when the option should render richer content than the plain label. The label remains the plain-text
representation used for search and anywhere the rich content cannot be rendered.
Grouped Options
Use groupLabel when an option belongs under a group header. If your application stores options in groups, flatten them
and copy the group label onto each option:
const peopleByGroup = [
{
label: "Operations",
options: [
{ value: "apollo-creed", label: "Apollo Creed" },
{ value: "james-lang", label: "James Lang" },
],
},
{
label: "Training",
options: [
{ value: "michael-goldmill", label: "Michael Goldmill" },
{ value: "tony-evers", label: "Tony Evers" },
],
},
{
label: "Personal",
options: [
{ value: "adrian-pennino", label: "Adrian Pennino" },
{ value: "robert-balboa", label: "Robert Balboa" },
],
},
];
const source = useOptionSource(async ({ values, search = "", offset = 0, limit = 100 }) => {
const normalizedSearch = search.trim().toLowerCase();
const matchingOptions = peopleByGroup
.flatMap((group) => group.options.map((option) => ({ ...option, groupLabel: group.label })))
.filter((option) =>
values ? values.includes(option.value) : option.label.toLowerCase().includes(normalizedSearch),
);
return {
options: matchingOptions.slice(offset, offset + limit),
hasMore: offset + limit < matchingOptions.length,
};
});
Display Mode
Caching
Option sources cache the first unfiltered page and options returned while resolving selected values. This avoids repeating common requests and allows selected values to reuse labels that have already been loaded. Search results and additional pages are still requested from the source when needed.
useOptionSource keeps the source instance stable so its cache survives React renders.
Disable the built-in cache when the request layer already handles caching or the data must always be refreshed:
const citySource = useOptionSource({
fetch: fetchCities,
noCache: true,
});
Call clearCache after the underlying data changes and previously loaded options should be discarded:
citySource.clearCache?.();
Error Handling
You can throw any kind of error from an option source. Super Select catches it and displays an error state with a retry button when the request can be repeated.
Use OptionSourceError when the error needs a user-facing message or additional metadata:
import { OptionSourceError } from "super-select-react";
throw new OptionSourceError("403 Forbidden Internal Error Message", {
code: "forbidden",
httpStatus: 403,
userMessage: "You do not have permission to load cities.",
});
The additional metadata properties available in an error are:
userMessageis displayed by the default error indicator.codeprovides a consistent, machine-readable category for the failure.httpStatusrecords an associated HTTP response and is used to derive a code whencodeis omitted.causepreserves the original error for code that catches the option-source request directly.
The optional code classifies the failure:
| Code | Use when |
|---|---|
network | The request could not reach its source. |
timeout | The request did not complete in time. |
unauthorized | Authentication is required or no longer valid. |
forbidden | The user is authenticated but does not have access. |
not-found | The requested endpoint or resource does not exist. |
rate-limited | The source rejected the request because too many requests were made. |
server | The source failed while processing the request. |
The default error indicator displays userMessage but does not otherwise behave differently based on code or
httpStatus. A custom error indicator component receives the complete error through its error prop and can use that
metadata to choose its content or behavior. See Customization for information about replacing rendered
components.