Async option list loading
To load option lists asynchronously for a value selector or editor, use the useAsyncOptionList hook.
This opt-in feature enables dynamic loading of options based on rule/group context, with intelligent caching for performance optimization.
Basic usage
- Create a component that accepts
ValueSelectorPropsorValueEditorProps. - Pass the props directly to
useAsyncOptionListalong with the async configuration options. - After any custom logic, pass the object returned from
useAsyncOptionListas the props to a standard selector/editor component. - Assign the component in the
controlElementsprop.
import { type UseAsyncOptionListParams, useAsyncOptionList } from 'react-querybuilder';
const useAsyncOptionListParams: UseAsyncOptionListParams = {
getCacheKey: 'field',
loadOptionList: async (value, { ruleOrGroup }) => {
const response = await fetch(`/api/operators?field=${ruleOrGroup.field}`);
return response.json();
},
};
// Step 1
const AsyncOperatorSelector = (props: ValueSelectorProps) => {
// Step 2
const asyncProps = useAsyncOptionList(props, useAsyncOptionListParams);
// Step 3
return <props.schema.controls.valueSelector {...asyncProps} />;
};
const App = () => (
<QueryBuilder
controlElements={{
// Step 4
operatorSelector: AsyncOperatorSelector,
}}
/>
);
While you can explicitly render any selector or editor component...
// For example:
return <AntDValueSelector {...asyncProps} />;
// or
return <MaterialValueEditor {...asyncProps} />;
...rendering the configured value selector/editor makes your component more versatile as it will automatically adapt to configuration changes at the context and query builder level.
return <props.schema.controls.valueSelector {...asyncProps} />;
// or
return <props.schema.controls.valueEditor {...asyncProps} />;
This method can also help avoid some issues with certain compatibility packages.
Configuration options
loadOptionList
Function that returns a Promise for the option list. This function is called when a valid cached list is unavailable. It should ultimately call your API, if and when necessary.
- As with option list-style props on
QueryBuilder, the resolved value fromloadOptionListcan bestring[],Option[], orOptionGroup[]. - The resolved list will be processed through the
prepareOptionListfunction, guaranteeing each option is aFullOptionwithname,value, andlabelproperties. - The processed list will be
optionsin the returned object if aValueSelectorPropsobject is passed in, orvaluesifValueEditorPropsis passed in.
Example:
const loadFieldOptions = async (value, { ruleOrGroup }) => {
// Current selector value is available
console.log('Current value:', value);
// Rule or group context is available
if (ruleOrGroup?.field === 'user') {
return await fetch('/api/user-fields').then(r => r.json());
}
return await fetch('/api/default-fields').then(r => r.json());
};
const ValueSelectorAsync = (props: ValueSelectorProps) => {
const asyncProps = useAsyncOptionList(props, { loadOptionList: loadFieldOptions });
return <props.schema.controls.valueSelector {...asyncProps} />;
};
getCacheKey
Controls cache key generation. Can be a string, array of strings, or a function returning a string.
Cache by property name (string)
// Cache by field value only
const getCacheKey = 'field';
// Or cache by operator value only
const getCacheKey = 'operator';
const ValueSelectorAsync = (props: ValueSelectorProps) => {
const asyncProps = useAsyncOptionList(props, { getCacheKey, loadOptionList });
return <props.schema.controls.valueSelector {...asyncProps} />;
};
Cache by multiple property names (array of strings)
// Cache by combination of field and operator
const getCacheKey = ['field', 'operator'];
const ValueSelectorAsync = (props: ValueSelectorProps) => {
const asyncProps = useAsyncOptionList(props, { getCacheKey, loadOptionList });
return <props.schema.controls.valueSelector {...asyncProps} />;
};
Cache by custom function
// `getCacheKey` receives the entire props object as its only parameter
const getCacheKey = (props: ValueSelectorProps) => {
const {
rule,
ruleGroup,
schema: { qbId },
} = props;
// Using `qbId` will cache each query builder separately
return `${qbid}-${rule?.field}-${rule?.operator}-${ruleGroup?.id}`;
};
const ValueSelectorAsync = (props: ValueSelectorProps) => {
const asyncProps = useAsyncOptionList(props, { getCacheKey, loadOptionList });
return <props.schema.controls.valueSelector {...asyncProps} />;
};
cacheTTL
Cache time-to-live in milliseconds. Defaults to 1_800_000 (30 minutes).
// 30 minutes (default)
const cacheTTL = 1_800_000;
// 5 minutes: m s ms
const cacheTTL = 5 * 60 * 1000;
// Disable caching (cache will be populated but immediately outdated)
const cacheTTL = 0;
const ValueSelectorAsync = (props: ValueSelectorProps) => {
const asyncProps = useAsyncOptionList(props, { cacheTTL, loadOptionList });
return <props.schema.controls.valueSelector {...asyncProps} />;
};
Loading states
useAsyncOptionList adds the "queryBuilder-loading" class while the promise from loadOptionList is pending (if suppressStandardClassnames is not true). No styles are applied by the default stylesheet for this class.
To add custom classes during pending loadOptionList promises, use controlClassnames#loading or override the className prop on the rendered value selector.
In this example, my-async-loading-class will be added to the specific component ValueSelectorAsync when loading, and common-async-loading-class will be added to all "loading" selectors.
const ValueSelectorAsync = (props: ValueSelectorProps) => {
const asyncProps = useAsyncOptionList(props, { ...otherParams, isLoading });
return (
<props.schema.controls.valueSelector
{...asyncProps}
className={`${asyncProp.className}${asyncProps.isLoading ? ' my-async-loading-class' : ''}`}
/>
);
};
const App = () => (
<QueryBuilder
controlElements={{ valueSelector: ValueSelectorAsync }}
controlClassnames={{ loading: 'common-async-loading-class' }}
/>
);
To force a "loading" state, set the isLoading parameter to true:
const ValueSelectorAsync = (props: ValueSelectorProps) => {
// Assume this hook determines whether to force a "loading" state and returns a `boolean`:
const isLoading = useIsLoading(props);
const asyncProps = useAsyncOptionList(props, { ...otherParams, isLoading });
return <props.schema.controls.valueSelector {...asyncProps} />;
};
Real-world examples
Dependent values
Load options in the value editor that depend on the selected field and operator. The value editor must
const ValueSelectorAsync = (props: ValueSelectorProps) => {
const asyncProps = useAsyncOptionList(props, {
loadOptionList: async (value, { ruleOrGroup }) => {
const { field, operator } = ruleOrGroup as RuleType;
return myValuesAPI({ field, operator });
},
getCacheKey: ['field', 'operator'],
});
return <props.schema.controls.valueSelector {...asyncProps} />;
};
// Assign the async value selector as `selectorComponent` to an otherwise
// "pass-through" value editor component.
const ValueEditorAsync = (props: ValueEditorProps) => (
<ValueEditor {...props} selectorComponent={ValueSelectorAsync} />
);
// Assign the custom value editor in `controlElements`
const App = () => <QueryBuilder controlElements={{ valueEditor: ValueEditorAsync }} />;
Dependent operators
Load operators that depend on the selected field type:
const ValueSelectorAsync = (props: ValueSelectorProps) => {
const asyncProps = useAsyncOptionList(props, {
loadOptionList: async (value, { ruleOrGroup }) => {
const fieldType = props.fieldData.datatype; // custom field property
return getOperatorsForType(fieldType);
},
getCacheKey: props => `operators-${props.fieldData.datatype}`,
});
return <props.schema.controls.valueSelector {...asyncProps} />;
};
Auto-complete value editor
Create an auto-complete component by including the current value in the cache key:
const AutoCompleteValueSelector = (props: ValueSelectorProps) => {
const asyncProps = useAsyncOptionList(props, {
loadOptionList: async (value, { ruleOrGroup }) => {
if (!value || value.length < 2) return [];
return fetch(`/api/autocomplete?q=${value}&field=${ruleOrGroup?.field}`).then(r => r.json());
},
getCacheKey: props => `autocomplete-${props.rule?.field}-${props.value}`,
});
// Rendering of the input and option list is left to this component
// (see below for example usage of third-party auto-complete components)
return <MyAutocompleteSelector {...asyncProps} />;
};
// Use the autocomplete selector as the selector for the value editor
const ValueEditorWithAutocomplete = (props: ValueEditorProps) => (
<ValueEditor {...props} selectorComponent={AutoCompleteValueSelector} />
);
// Assign the new value editor
const App = () => <QueryBuilder controlElements={{ valueEditor: ValueEditorWithAutocomplete }} />;
Some of the compatibility packages provide themed auto-complete components that integrate well with useAsyncOptionList.
- MUI/Material
- Mantine
- Ant Design
import { Autocomplete, TextField } from '@mui/material';
export const ValueEditorAutocompleteAsync = (props: ValueEditorProps) => {
const { value, handleOnChange, values } = useAsyncOptionList(props, {
getCacheKey,
loadOptionList,
});
return (
<Autocomplete
inputValue={value}
options={values ?? []}
onInputChange={(_e, v) => handleOnChange(v)}
disabled={props.disabled}
renderInput={params => (
<TextField {...params} label="Framework" placeholder="Start typing to load options..." />
)}
/>
);
};
import { Autocomplete } from '@mantine/core';
export const ValueEditorAutocompleteAsync = (props: ValueEditorProps) => {
const { value, handleOnChange, values } = useAsyncOptionList(props, {
getCacheKey,
loadOptionList,
});
return (
<Autocomplete
value={value ?? ''}
data={values}
onChange={handleOnChange}
clearable
disabled={props.disabled}
placeholder="Start typing to load options..."
/>
);
};
import { AutoComplete } from 'antd';
export const ValueEditorAutocompleteAsync = (props: ValueEditorProps) => {
const { value, handleOnChange, values } = useAsyncOptionList(props, {
getCacheKey,
loadOptionList,
});
return (
<AutoComplete
value={value ?? ''}
style={style}
options={values}
onSearch={handleOnChange}
onChange={handleOnChange}
disabled={props.disabled}
placeholder="Start typing to load options..."
/>
);
};
Mock loader setup
This code can be used to mock an API call for the compatibility examples above.
// prettier-ignore
const words = [ "React", "Angular", "Vue", "Svelte", "Next.js", "Nuxt.js", "Gatsby", "TypeScript", "JavaScript", "Python", "Java", "C#", "Go", "Rust", "Node.js", "Express", "Fastify", "Koa", "Hapi", "NestJS", "MongoDB", "PostgreSQL", "MySQL", "Redis", "SQLite", "Docker", "Kubernetes", "AWS", "Azure", "GCP"];
// Simulate async data loading
const loadOptionList = async (value: string | undefined): Promise<string[]> => {
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 500));
// Filter based on input value if provided
if (value && value.length > 0) {
return words.filter(word => word.toLowerCase().includes(value.toLowerCase()));
}
// Otherwise return no results
return [];
};
const getCacheKey = ({ value }: ValueEditorProps) => value;
Error handling
Async loading errors can be managed within your loadOptionList function or by checking the errors property on the object returned from useAsyncOptionList, which will contain an error message when the promise is rejected.
Internal error handling:
const loadOptionList = async (value, { ruleOrGroup }) => {
try {
const response = await fetch('/api/options');
if (!response.ok) throw new Error('Failed to load options');
return response.json();
} catch (error) {
// Log the error and return fallback options
console.error('Failed to load options:', error);
return [{ name: 'error', value: 'error', label: 'Error loading options' }];
}
};
Promise rejection detection:
const AsyncOperatorSelector = (props: ValueSelectorProps) => {
const asyncProps = useAsyncOptionList(props, useAsyncOptionListParams);
// If `errors` is truthy, the promise was rejected
if (asyncProps.errors) {
const fallbackOptions = [{ name: 'error', value: 'error', label: 'Error loading options' }];
return <props.schema.controls.valueSelector {...asyncProps} options={fallbackOptions} />;
}
return <props.schema.controls.valueSelector {...asyncProps} />;
};
Best practices
Cache key design
- Don't include the selector's own value unless building auto-complete
- Use specific, meaningful keys to avoid cache conflicts
- Consider rule/group hierarchy for context-dependent options
// ❌ Bad: includes own value (unless auto-complete)
getCacheKey: props => `${props.rule?.field}-${props.value}`;
// ✅ Good: context-dependent without own value
getCacheKey: props => `operators-${props.rule?.field}`;
Performance optimization
- Set appropriate cache TTL based on data freshness requirements
- Use specific cache keys to maximize cache hits
- Consider debouncing for auto-complete scenarios
Error resilience
- Provide fallback options when loading fails
- Handle network timeouts gracefully
- Show meaningful error states to users