Query management
Refer to the TypeScript reference page for information about the types and interfaces referenced below.
Utilities for building and modifying query objects programmatically, without the
<QueryBuilder /> component.
Query tools
Several methods are available to assist with programmatic manipulation of query objects. These methods are used by the <QueryBuilder /> component itself, so they're guaranteed to achieve the same result as a corresponding UI-based update. Each method returns the modified query.
Check out the "External controls" Tips & Tricks page to see these methods used outside the <QueryBuilder /> component context.
For a stateful, chainable wrapper around these methods that holds the query for you, see QueryManager below.
add
(query: RuleGroupTypeAny, ruleOrGroup: RuleGroupTypeAny | RuleType, parentPathOrID: Path | string, options: AddOptions) => RuleGroupTypeAny
Adds a rule or group (and an independent combinator if necessary to keep the query valid) to the group at the specified path or with the given id.
AddOptions
export interface AddOptions extends AbortOptions {
/**
* If the query extends `RuleGroupTypeIC` (i.e. the query has independent
* combinators), then the first combinator in this list will be inserted
* before the new rule/group if the parent group is not empty. This option
* is overridden by `combinatorPreceding`.
*/
combinators?: OptionList;
/**
* If the query extends `RuleGroupTypeIC` (i.e. the query has independent
* combinators), then this combinator will be inserted before the new rule/group
* if the parent group is not empty. This option will supersede `combinators`.
*/
combinatorPreceding?: string;
/**
* ID generator.
*/
idGenerator?: () => string;
}
Source: /packages/core/src/utils/queryTools.ts#L173-L191
remove
(query: RuleGroupTypeAny, pathOrID: Path | string, options: RemoveOptions) => RuleGroupTypeAny
Removes a rule or group (and the preceding independent combinator if one exists) at the specified path or with the given id.
update
(query: RuleGroupTypeAny, prop: string, value: any, pathOrID: Path | string, options: UpdateOptions) => RuleGroupTypeAny
Updates a property of a rule or group, or an independent combinator, at the specified path or with the given id.
Multiple properties can be updated at once by passing either a property-to-value map or parallel arrays of property names and values (note that pathOrID shifts to the third argument for the map form):
(query: RuleGroupTypeAny, props: UpdateValueMap, pathOrID: Path | string, options: UpdateOptions) => RuleGroupTypeAny
(query: RuleGroupTypeAny, props: string[], values: any[], pathOrID: Path | string, options: UpdateOptions) => RuleGroupTypeAny
// Equivalent ways to set `valueSource` and `value` in a single call:
update(query, { valueSource: 'field', value: 'otherField' }, rulePath);
update(query, ['valueSource', 'value'], ['field', 'otherField'], rulePath);
Regardless of the order in which properties are listed, field, operator, and valueSource are applied before value. This ensures an explicitly-provided value is never reset by a change to one of those properties (e.g. updating field normally resets value).
UpdateOptions
export interface UpdateOptions extends AbortOptions {
/**
* When updating the `field` of a rule, the rule's `operator`, `value`, and `valueSource`
* will be reset to their respective defaults. Defaults to `true`.
*/
resetOnFieldChange?: boolean;
/**
* When updating the `operator` of a rule, the rule's `value` and `valueSource`
* will be reset to their respective defaults. Defaults to `false`.
*/
resetOnOperatorChange?: boolean;
/**
* Determines the default operator name for a given field.
*/
getRuleDefaultOperator?: (field: string) => string;
/**
* Determines the valid value sources for a given field and operator.
*/
getValueSources?: (field: string, operator: string) => ValueSources | ValueSourceFlexibleOptions;
/**
* Gets the default value for a given rule, in case the value needs to be reset.
*/
// oxlint-disable-next-line typescript/no-explicit-any
getRuleDefaultValue?: (rule: RuleType) => any;
/**
* Determines the valid match modes for a given field.
*/
getMatchModes?: (field: string) => MatchModeOptions;
}
Source: /packages/core/src/utils/queryTools.ts#L286-L314
move
(query: RuleGroupTypeAny, oldPathOrID: Path | string, newPath: Path | 'up' | 'down', options: MoveOptions) => RuleGroupTypeAny
Moves (or clones with a new id) a rule or group at the specified path or with the given id to a new location in the query tree. Using "up" or "down" as the newPath will "shift" the rule or group higher or lower, respectively.
MoveOptions
export interface MoveOptions extends AbortOptions {
/**
* When `true`, the source rule/group will not be removed from its original path.
*/
clone?: boolean;
/**
* If the query extends `RuleGroupTypeIC` (i.e. the query is using independent
* combinators), then the first combinator in this list will be inserted before
* the rule/group if necessary.
*/
combinators?: OptionList;
/**
* ID generator.
*/
idGenerator?: () => string;
}
Source: /packages/core/src/utils/queryTools.ts#L763-L778
insert
(query: RuleGroupTypeAny, ruleOrGroup: RuleGroupTypeAny | RuleType, path: Path, options: InsertOptions) => RuleGroupTypeAny
Inserts a rule or group (and an independent combinator if necessary to keep the query valid) at the specified path.
InsertOptions
export interface InsertOptions extends AbortOptions {
/**
* If the query extends `RuleGroupTypeIC` (i.e. the query has independent
* combinators), then the first combinator in this list will be inserted
* before the new rule/group if the parent group is not empty. This option
* is overridden by `combinatorPreceding`.
*/
combinators?: OptionList;
/**
* If the query extends `RuleGroupTypeIC` (i.e. the query has independent
* combinators), then this combinator will be inserted before the new rule/group
* if the parent group is not empty and the new rule/group is not the first in the
* group (`path.at(-1) > 0`). This option will supersede `combinators`.
*/
combinatorPreceding?: string;
/**
* If the query extends `RuleGroupTypeIC` (i.e. the query has independent
* combinators), then this combinator will be inserted after the new rule/group
* if the parent group is not empty and the new rule/group is the first in the
* group (`path.at(-1) === 0`). This option will supersede `combinators`.
*/
combinatorSucceeding?: string;
/**
* ID generator.
*
* @default generateID
*/
idGenerator?: () => string;
/**
* When `true`, the new rule/group will replace the rule/group at `path`.
*/
replace?: boolean;
}
Source: /packages/core/src/utils/queryTools.ts#L971-L1003
group
(query: RuleGroupTypeAny, sourcePathOrID: Path | string, targetPathOrID: Path | string, options: GroupOptions) => RuleGroupTypeAny
Creates a new group at the target path (based on the specified path or given id) containing the existing rule/group at that path and the rule/group at the source path (or with the source id), in that order, in its rules array.
GroupOptions
export interface GroupOptions extends AbortOptions {
/**
* When `true`, the source rule/group will not be removed from its original path.
*/
clone?: boolean;
/**
* If the query extends `RuleGroupTypeIC` (i.e. the query is using independent
* combinators), then the first combinator in this list will be inserted between
* the two rules/groups.
*/
combinators?: OptionList;
/**
* ID generator.
*/
idGenerator?: () => string;
}
Source: /packages/core/src/utils/queryTools.ts#L1121-L1136
Aborted operations
Query tools never throw. When an operation can't be carried out—the path/id doesn't resolve, the parent is a rule instead of a group, the target is the root group, and so on—the original query is returned unmodified. Since the return value is the same object either way, that outcome is indistinguishable from a successful call that happened to change nothing.
Pass onAbort (accepted by every query tool) to find out which happened and why:
const newQuery = remove(query, 'some-id', {
onAbort: ({ reason, operation, pathOrID }) => {
console.warn(`${operation} aborted: ${reason}`, pathOrID);
},
});
AbortOptions
export interface AbortOptions extends GuardOptions {
/**
* Called when the operation returns the query unmodified, with the reason why. Query tools
* never throw, so this is the only way to distinguish "the target was invalid" from
* "the operation had nothing to do".
*/
onAbort?: (info: AbortInfo) => void;
}
Source: /packages/core/src/utils/queryTools.ts#L122-L129
reason is one of:
AbortReason | Meaning |
|---|---|
"target-not-found" | The rule/group identified by the given path or id does not exist. |
"parent-not-found" | The parent group identified by the given path or id does not exist. |
"parent-not-a-group" | The given parent path or id refers to a rule rather than a group. |
"destination-not-found" | The destination's parent group does not exist. |
"root-not-allowed" | The root group cannot be removed, moved, or grouped. |
"not-a-combinator-slot" | In an independent combinators query, the target index holds a rule. |
"same-location" | The rule/group is already at the destination. Not an error. |
"no-change" | The property already has the given value. Not an error. |
"target-disabled" | The target is disabled, or descends from a disabled group. |
"parent-disabled" | The parent group is disabled, or descends from a disabled group. |
"max-levels-exceeded" | Adding the group would nest it deeper than maxLevels allows. |
QueryManager's strict mode builds on this channel.
Guards
The last three reasons above are produced by opt-in guards, accepted by every query tool alongside onAbort:
respectDisabled— abort when the target (or, foradd/insert, the parent) is disabled, either directly or by descending from a disabled group. Defaults tofalse.queryDisabled— abort every mutation, as though the whole query were disabled. Defaults tofalse.maxLevels— the maximum depth at which a group may be added byaddorinsert. Rules are unaffected. Defaults toInfinity.
disabled is a property of the query itself, so honoring it is a matter of data integrity rather than presentation—a query saved with a locked rule should still be locked when it is loaded again. It is opt-in here only to preserve the existing behavior of the standalone tools; QueryManager enables it by default.
Updating a rule or group's own disabled property is always permitted under respectDisabled, since that is the only way to re-enable it. queryDisabled blocks even that.
// Aborts: the rule at [0] is disabled
update(query, 'value', 'x', [0], { respectDisabled: true });
// Succeeds: re-enabling is always allowed
update(query, 'disabled', false, [0], { respectDisabled: true });
getGuardAbortReason(query, pathOrID, guards, { asParent }) and exceedsMaxLevels(parentPath, guards) are exported so a UI layer that runs its own logic before mutating—invoking a confirmation callback, for example—can apply the same rules without duplicating them.
Query manager
QueryManager is a stateful wrapper around the query tools above. It holds the query internally, so each method takes the same arguments as its query tool counterpart minus the leading query parameter, and returns the manager itself for chaining. It has no React dependency, so it can be used on the server, in a CLI, or anywhere else a query needs to be built or modified programmatically.
import { QueryManager } from 'react-querybuilder';
// or: import { QueryManager } from '@react-querybuilder/core';
const q = new QueryManager(undefined, { fields });
q.add({ field: 'firstName', operator: '=', value: 'Steve' })
.add({ field: 'lastName', operator: '=', value: 'Vai' })
.update('operator', 'contains', 'rule-id-1');
q.validate(); // boolean | ValidationMap
q.format('sql'); // string
q.getQuery(); // RuleGroupType
Constructor
new QueryManager(query?: RuleGroupTypeAny, options?: QueryManagerOptions)
Without a query, an empty group is created using the configured combinators and addRuleToNewGroups options. With one, every rule and group is assigned an id if it doesn't already have one. Pass a RuleGroupTypeIC to manage a query with independent combinators.
QueryManagerOptions
export interface QueryManagerOptions<
F extends FullField = FullField,
O extends FullOperator = FullOperator,
C extends FullCombinator = FullCombinator,
> {
/** The fields available for rules. Accepts the same shapes as the `fields` prop. */
fields?: FlexibleOptionListProp<F> | BaseOptionMap<F>;
/** The operators available for rules. Accepts the same shapes as the `operators` prop. */
operators?: FlexibleOptionListProp<O> | BaseOptionMap<O>;
/** The combinators available for groups. Defaults to `defaultCombinators`. */
combinators?: FlexibleOptionListProp<C> | BaseOptionMap<C>;
/** Properties applied to every field in `fields`. */
baseField?: Record<string, unknown>;
/** Properties applied to every operator in `operators`. */
baseOperator?: Record<string, unknown>;
/** Properties applied to every combinator in `combinators`. */
baseCombinator?: Record<string, unknown>;
/** When `false`, an empty placeholder option is prepended to the field list. */
autoSelectField?: boolean;
/** When `false`, an empty placeholder option is prepended to each operator list. */
autoSelectOperator?: boolean;
/** When `false`, an empty placeholder option is prepended to each value list. */
autoSelectValue?: boolean;
/** The default `field` for rules created by {@link QueryManager.createRule}. */
getDefaultField?: string | ((fieldsData: FullOptionList<F>) => string);
/** The default `operator` for a given field. */
getDefaultOperator?: string | ((field: string, misc: { fieldData: F }) => string);
/** Overrides the computed default `value` for a new rule. */
getDefaultValue?: (rule: RuleType, misc: { fieldData: F }) => unknown;
/** The operators available for a given field. */
getOperators?: (field: string, misc: { fieldData: F }) => FlexibleOptionList<O> | null;
/** The value editor type for a given field/operator, which informs the default value. */
getValueEditorType?: (field: string, operator: string, misc: { fieldData: F }) => ValueEditorType;
/** The value list for a given field/operator, which informs the default value. */
getValues?: (
field: string,
operator: string,
misc: { fieldData: F }
) => FlexibleOptionList<Option>;
/** The valid value sources for a given field/operator. */
getValueSources?: (
field: string,
operator: string,
misc: { fieldData: F }
) => ValueSources | ValueSourceFlexibleOptions;
/** The valid match modes for a given field. */
getMatchModes?: (
field: string,
misc: { fieldData: F }
) => boolean | MatchMode[] | FlexibleOption<MatchMode>[];
/** The named parameters available for a given field/operator. */
getParameters?: (
field: string,
operator: string,
misc: { fieldData: F }
) => FlexibleOptionList<Option> | null;
/** When `true`, multi-value defaults are arrays instead of comma-joined strings. */
listsAsArrays?: boolean;
/** When `true`, groups created by {@link QueryManager.createRuleGroup} contain one new rule. */
addRuleToNewGroups?: boolean;
/**
* When updating a rule's `field`, reset its `operator`, `value`, and `valueSource` to their
* defaults. Defaults to `true`, matching the `QueryBuilder` prop of the same name.
*/
resetOnFieldChange?: boolean;
/**
* When updating a rule's `operator`, reset its `value` to the default. Defaults to `false`,
* matching the `QueryBuilder` prop of the same name.
*/
resetOnOperatorChange?: boolean;
/**
* The maximum depth at which groups may be added. As with the `QueryBuilder` prop of the same
* name, a non-positive value means unlimited. Defaults to `Infinity`.
*/
maxLevels?: number;
/**
* Honor `disabled` properties within the query, so mutations targeting a disabled rule or
* group (or a descendant of a disabled group) are aborted. Updating a node's own `disabled`
* property is always permitted. Defaults to `true`, matching the `QueryBuilder` component;
* pass `false` to mutate freely regardless of the property.
*/
respectDisabled?: boolean;
/**
* Paths that are disabled without the corresponding rule or group carrying a `disabled`
* property. This mirrors the array form of the `QueryBuilder` `disabled` prop
* (e.g. `disabled={[[2]]}`), which disables nodes by position rather than by data.
*
* A path is treated as disabled if it appears here or descends from a path that does. Honored
* only when `respectDisabled` is `true`; as with the `disabled` property, a node's own
* `disabled` can always be changed so it is never permanently locked.
*/
disabledPaths?: Path[];
/** Abort every mutation, as though the entire query were disabled. Defaults to `false`. */
queryDisabled?: boolean;
/** The input type for a given field/operator, surfaced by {@link QueryManager.getRuleContext}. */
getInputType?: (field: string, operator: string, misc: { fieldData: F }) => InputType | null;
/** Extra props for a subquery builder, surfaced by {@link QueryManager.getRuleContext}. */
getSubQueryBuilderProps?: (field: string, misc: { fieldData: F }) => Record<string, unknown>;
/**
* Enables undo/redo recording. Pass `true` for the defaults, or an object to configure
* `maxHistory` and/or `coalesceMs`. Disabled by default, so instances that never undo
* retain no extra references.
*/
history?: boolean | QueryHistoryOptions;
/**
* Throw a {@link QueryManagerError} when a mutation is aborted because its target could not
* be used. Disabled by default, in which case such mutations are silent no-ops. Can be
* overridden per call.
*/
strict?: boolean;
/**
* Called whenever a mutation is aborted, including for the non-error reasons
* `"same-location"` and `"no-change"`. Can be overridden per call.
*/
onInvalidTarget?: (info: AbortInfo) => void;
/** Validates the query. Defaults to {@link defaultValidator}. */
validator?: QueryValidator;
/** Generates `id` properties for new rules and groups. Defaults to {@link generateID}. */
idGenerator?: () => string;
/**
* Clock used to time history coalescing. Defaults to `Date.now`.
*
* @internal Test seam. Exists so that history recording can be compared against the
* `react-querybuilder/history` implementation without depending on wall-clock timing.
*/
now?: () => number;
}
Source: /packages/core/src/utils/QueryManager.ts#L166-L292
The constructor also accepts the guard options respectDisabled (defaulting to true here, matching the QueryBuilder component), queryDisabled, and maxLevels, plus resetOnFieldChange (default true) and resetOnOperatorChange (default false), which mirror the props of the same names.
State access
getQuery(): RuleGroupTypeAny— The current query. The returned object is frozen and structurally shared, so it's safe to retain and compare by reference to detect changes.setQuery(query: RuleGroupTypeAny)— Replaces the current query, assigningids as needed.
Factories
createRule(): RuleType— Creates a rule using the configured fields, operators, and defaults, applying the same precedence rules as the<QueryBuilder />component.createRuleGroup(independentCombinators?: boolean): RuleGroupTypeAny— Creates a group.
Neither method adds anything to the query; pass the result to add or insert.
Mutation
Each method below delegates to the query tool of the same name, using the manager's configuration (combinators, idGenerator, and the default resolvers) as the default options. Per-call options take precedence.
add(ruleOrGroup, parentPathOrID?, options?)— Unlike theaddquery tool,parentPathOrIDis optional and defaults to the root group.remove(pathOrID, options?)update(prop, value, pathOrID, options?),update(props, values, pathOrID, options?), orupdate(propsMap, pathOrID, options?)— All three argument forms of theupdatequery tool are supported.move(oldPathOrID, newPath, options?)insert(ruleOrGroup, path, options?)— Like theinsertquery tool, this accepts a path only, not anid.group(sourcePathOrID, targetPathOrID, options?)
As with the query tools themselves, these methods are a no-op when the target path or id can't be resolved—including attempts to remove the root group. Nothing is thrown by default, and mutators always return the manager, so compare getQuery() by reference if you need to know whether a call had any effect. See strict mode to turn these into errors instead.
Validation and export
validate(): boolean | ValidationMap— Validates the current query with the configuredvalidator, defaulting todefaultValidator.format(options?)— Passes the current query toformatQuery. Accepts everythingformatQueryaccepts as its second parameter, including the format-name shorthand (q.format('sql')), and has the same return types.
Cloning
clone(options?: { regenerateIDs?: boolean }) returns an independent manager with the same configuration and the current query. Subscribers and history are not carried over. Because every mutation produces a new query object, the two managers share the initial query safely and diverge from the first change. Pass { regenerateIDs: true } to give every rule and group in the clone a new id, which is useful when both queries will be used together.
Subscriptions
subscribe(listener: () => void) registers a listener called after every change to the query, and returns a function that unregisters it. Mutations that resolve to a no-op do not notify, and a batch notifies once no matter how many changes it contains.
The method is bound to the instance, so it is a stable reference across renders and can be passed directly to React's useSyncExternalStore. getQuery is bound as well, so it can serve as the snapshot getter without a wrapper:
const query = useSyncExternalStore(q.subscribe, q.getQuery);
In React, prefer the useQueryManager hook, which wraps this and handles creating the manager exactly once.
Batching
batch(fn: () => void) runs fn, deferring history recording and subscriber notification until it returns. The whole batch becomes a single undo step and triggers a single notification, or neither if the query ends up unchanged. Batches may be nested; only the outermost one commits.
If fn throws, the query and its history are restored to their pre-batch state and the error propagates, so a batch either applies completely or not at all.
undo(), redo(), and clearHistory() may be called inside a batch, and their notifications are deferred like everything else. Because they manage the history stacks themselves, a batch containing one of them records no entry of its own, leaving the stacks exactly as those methods left them.
q.batch(() => {
q.add(rule1);
q.add(rule2);
q.add(rule3);
}); // one notification, one undo step
Strict mode
By default, a mutation whose target can't be used is a silent no-op (see aborted operations). Set strict: true to raise a QueryManagerError instead:
const q = new QueryManager(query, { fields, strict: true });
q.remove('no-such-id'); // throws QueryManagerError
The error carries a code (the AbortReason) and the full info object, so failures can be handled by kind rather than by message:
try {
q.remove(id);
} catch (error) {
if (error instanceof QueryManagerError && error.code === 'target-not-found') {
// ...
}
}
The two non-error reasons—"same-location" and "no-change"—never throw, so idempotent code (setting a value to what it already is, or dropping a rule where it started) keeps working under strict.
To observe aborted operations without changing control flow, use onInvalidTarget. It is called for every abort, including the non-error reasons, and runs before any strict throw, so an operation can be both observed and enforced:
const q = new QueryManager(query, {
fields,
onInvalidTarget: ({ reason, operation }) => logger.warn(`${operation}: ${reason}`),
});
Both options can be overridden per call, which takes precedence over the manager's own configuration:
q.remove(id, { strict: true }); // just this call
q.remove(id, { strict: false }); // opt out of a manager-wide strict setting
A strict throw inside batch rolls the whole batch back.
Undo/redo
Recording is opt-in through the history option, so instances that never undo retain no extra references. Pass true for the defaults, or an object to configure maxHistory (default 50) and/or coalesceMs (default 500).
const q = new QueryManager(undefined, { fields, history: true });
undo()/redo()— Restore the previous or most recently undone query. No-ops when the correspondingcan*method returnsfalse. Both notify subscribers.canUndo(): boolean/canRedo(): booleanclearHistory()— Discards all history without changing the current query.getHistory(): { past, future }— Copies of the recorded queries,pastoldest first andfuturenewest first.
undo(), redo(), and clearHistory() may be called inside a batch; see Batching for how that interacts with the batch's own history entry.
Recording follows the same semantics as the react-querybuilder/history entry point: consecutive changes to the same property of the same rule within coalesceMs are merged into a single undo step (so typing a value produces one entry rather than one per keystroke), structural changes never coalesce, and changes that alter no observable property are not recorded at all.
Traversal
walk(options?) is a generator yielding every rule and group in the query, depth-first in pre-order, starting with the root group itself. Combinator strings in independent-combinator queries are skipped.
Each entry is a QueryNode:
interface QueryNode {
node: RuleGroupTypeAny | RuleType;
path: Path;
parent: RuleGroupTypeAny | null; // `null` for the root group
}
for (const { node, path } of q.walk({ rulesOnly: true })) {
console.log(path, node.field);
}
WalkOptions:
| Option | Description |
|---|---|
from | Traverse only the subtree at this Path or id, visiting that node first. Yields nothing if unresolvable. |
rulesOnly | Visit only rules. Groups are still traversed, just not yielded (except as parent). |
groupsOnly | Visit only groups. |
Derived methods:
rules(options?)/groups(options?)— Shorthand forwalkwithrulesOnly/groupsOnly.find(predicate, options?): QueryNode | null— The first matching node.filter(predicate, options?): QueryNode[]— All matching nodes.[Symbol.iterator]()— Equivalent towalk()with no options, so a manager can be spread ([...q]) or used directly infor...of.
Traversal operates on the query as it was when iteration began, so mutating the manager mid-iteration does not affect a walk already in progress. Because generators are lazy, that snapshot is taken on the first iteration step rather than when walk() is called.
Lookups
These mirror the standalone findPath and findID utilities, minus the trailing query parameter.
findPath(path: Path)— The rule or group atpath.findID(id: string)— The rule or group with the givenid.getPathOfID(id: string): Path | null— The path of the rule or group with the givenid.pathIsDisabled(path: Path): boolean— Whether the node atpathis disabled, either itself or by an ancestor group.
Methods that accept either a path or an id, and narrow the result:
getNode(pathOrID)— The rule or group.getRule(pathOrID): RuleType | null—nullif the target is a group.getGroup(pathOrID): RuleGroupTypeAny | null—nullif the target is a rule.getParent(pathOrID): RuleGroupTypeAny | null— The containing group, ornullfor the root group.
Unlike the standalone findPath, which can return undefined for an out-of-range index, all of these normalize an unresolvable target to null.
findID, getPathOfID, and the pathOrID forms above are backed by an id-to-Path index built once per query, so repeated lookups are constant time rather than a tree walk. The index is rebuilt automatically whenever the query changes, including via undo, redo, and a rolled-back batch.
validate() results are cached against the current query for the same reason. A custom validator with side effects, or one that depends on anything other than the query, may therefore run fewer times than expected.
Rule configuration
These methods resolve the same field/operator configuration the QueryBuilder component uses, applying the same precedence rules (field-level properties first, then the corresponding get* option, then the manager-level defaults).
getFields(): FullOptionList<FullField>— The normalized field list, for populating a field selector.getCombinators(): FullOptionList<FullCombinator>— The normalized combinator list, for populating a combinator selector.getFieldData(field): FullField— The configured field. For an unconfigured field, returns the same minimal fallback ({ name, value, label }, all set to the field name) thatgetRuleContextreports asfieldData.
getFields and getCombinators return frozen arrays, so they are safe to hand to rendering code without defensive copying.
getOperators(field): FullOptionList<FullOperator>— The operator list for a field.getValueSources(field, operator): ValueSourceFullOptions— The available value sources.getMatchModes(field): MatchModeOptions— The available match modes.getValues(field, operator): FullOptionList<Option>— The value option list.getValueEditorType(field, operator): ValueEditorType— The value editor type.
getRuleContext(pathOrID) resolves all of the above for a specific rule at once, plus its validation result, returning null when the target can't be resolved or isn't a rule.
interface RuleContext {
fieldData: FullField;
hideValueControls: boolean;
inputType: InputType | null;
matchModes: MatchModeOptions;
operatorObject: FullOperator | undefined;
operators: OptionList<FullOperator>;
parameters: FlexibleOptionList<Option> | null;
validationResult: boolean | ValidationResult;
valueEditorType: ValueEditorType;
values: FlexibleOptionList<Option>;
valueSourceOptions: ValueSourceFullOptions;
valueSources: ValueSources;
}
This is the same derivation the useRule hook performs—both call the shared deriveRuleContext utility—so a non-React implementation can render a rule without reimplementing the precedence rules.
const ctx = q.getRuleContext([0]);
// => { fieldData: { name: 'firstName', ... }, valueEditorType: 'text', valueSources: ['value'], ... }
QueryManager has no getInputType option, so inputType reflects only a field's own inputType property and is otherwise null.
getRuleGroupContext(pathOrID) is the equivalent for groups, defaulting to the root group. It returns null when the target can't be resolved or isn't a group.
interface RuleGroupContext {
combinator: string;
combinatorObject: FullCombinator | undefined;
combinators: FullOptionList<FullCombinator>;
/** The selected combinator's `className`, or `null` for independent combinators. */
combinatorBasedClassName: Classname | null;
independentCombinators: boolean;
validationResult: boolean | ValidationResult;
}
Unlike a rule, a group's validationResult comes only from the query-level validation map—there is no field-level validator fallback.
Classnames
@react-querybuilder/core also exports the classname derivations used by useRule and useRuleGroup, so an implementation in any framework can produce a byte-identical class attribute for every element:
deriveRuleClassNames({ classNames, suppressStandardClassnames })— the per-element classnames for a rule (fields,operators,value,removeRule, and so on).deriveRuleOuterClassName({ classNames, suppressStandardClassnames, ...state })— the rule's wrapper classname, including conditional state classes fordisabled,muted, drag-and-drop, subqueries, and validation.deriveRuleGroupClassNames({ classNames, suppressStandardClassnames, ...dndState })— the per-element classnames for a rule group, including itsheader.deriveRuleGroupOuterClassName({ ... })— the group's wrapper classname.deriveRuleClassName(key, { ... })— a single rule classname, for cases where only one is needed.
A group's wrapper reflects fewer states than a rule's: dndOver, dndCopy, dndDropNotAllowed, and hasSubQuery apply to rules only. The two condition sets are declared separately for exactly this reason, so use the matching function rather than assuming symmetry.
Each takes the merged controlClassnames object and returns plain strings.
Composition is declared as data rather than code: every derived classname names the controlClassnames keys that contribute to it (in application order, after the standard classname) plus any state-dependent classes. Conditional elements—the rule wrapper and the group header—use the same declaration, so there is no element a port can handle differently by accident.
interface ClassnameSpec {
sources: readonly (keyof Classnames)[];
conditions?: readonly { key; when; standardOnly? }[];
}
For each entry the result is the standard classname, then each sources entry, then each active condition's custom class, then a single object of active standard classes.
deriveRuleClassNames({ classNames: { valueSelector: 'vs', fields: 'f' } });
// => { fields: 'rule-fields vs f', operators: 'rule-operators vs', ... }
Option resolvers and factories
The precedence rules the QueryBuilder component applies when resolving a rule's configuration are exported as standalone functions, so useQueryBuilderSetup and QueryManager share one implementation rather than each keeping its own:
resolveOperatorList({ field, fieldData, getOperators, operators, ... })— the field's ownoperators, thengetOperators, then the query-level list.resolveDefaultOperator({ field, fieldData, getDefaultOperator, getOperators })— the field'sdefaultOperator, thengetDefaultOperator(string or function), then the first available operator.resolveValueEditorType({ field, operator, fieldData, getValueEditorType })— the field'svalueEditorType(string or function of the operator), thengetValueEditorType, then"text".resolveValueList({ field, operator, fieldData, getValues, ... })— the field's ownvalues, thengetValues, then an empty list.createRule(options)/createRuleGroup(options, independentCombinators?)— build a new rule or group from that configuration.
Each accepts an optional placeholder so a UI layer can supply translated placeholder options; omit it where translations don't apply.
createRule computes value in a second pass, once field, operator, and valueSource are known, since the default value depends on all three. createRuleGroup generates the group's own id before any contained rule's — observable when idGenerator is deterministic.
Value editors
The logic behind the useValueEditor hook is exported separately, since a value editor is the control an implementation is most likely to rewrite:
getValueEditorReset({ skipHook, type, operator, value, inputType })— returns{ reset, value }. A rule'svaluemust collapse to a single element when it's an array (or a comma-containing string in anumberinput) but the operator is no longer one ofbetween/notBetween/in/notInand the editor isn't a multiselect. React applies this in an effect; apply it wherever is idiomatic.getMultiValueUpdate({ value, index, valueAsArray, operator, values, listsAsArrays, parseNumberMethod })— the next value when the editor atindexin a series changes. Forbetween/notBetween, editing the first bound guarantees an array of at least two elements, seeding the second from the first available option.coerceBigIntValue(value, parseNumberMethod)— abigint, falling back to the parsed number when the value can't be represented as one.coerceInputType(inputType, operator)— thetypean<input>should use.bigintvalues and thein/notInoperators both require a text input.isBetweenOperator(operator)— whether an operator's value is a pair of bounds.getValueSelectorUpdate(value, { multiple, listsAsArrays })/normalizeValueSelectorValue(value, multiple)— the equivalents foruseValueSelector. The latter stringifies multiselect values so they match option names, which are always strings.
Query actions
createQueryActions(config) builds the six mutations a query builder performs as pure functions of the current query. Each returns the next query, or undefined when the mutation was aborted—because the target is disabled, a confirmation callback declined, or maxLevels was reached.
const actions = createQueryActions({
qbId,
combinators,
idGenerator,
maxLevels,
queryDisabled,
respectDisabled,
resetOnFieldChange,
resetOnOperatorChange,
getRuleDefaultOperator,
getValueSources,
getRuleDefaultValue,
getMatchModes,
onAddRule,
onAddGroup,
onRemove,
onMoveRule,
onMoveGroup,
onGroupRule,
onGroupGroup,
onLog,
});
// { addRule, addGroup, propChange, removeRuleOrGroup, moveRule, groupRule }
This is the policy that surrounds the query tools—disabled gating, the confirmation callback protocol, depth limits, and debug logging. An implementation supplies only its own storage: read the current query, call the action, apply a non-undefined result.
const newQuery = actions.addRule(currentQuery, rule, parentPath);
if (newQuery) applyQuery(newQuery);
The confirmation callbacks have three distinct return contracts:
| Callback | Return value |
|---|---|
onAddRule, onAddGroup | true to proceed, falsy to cancel, or a replacement rule/group to add instead |
onMoveRule, onMoveGroup, onGroupRule, onGroupGroup | true to proceed, falsy to cancel, or a replacement query to apply instead |
onRemove | boolean only |
The move and group actions compute the prospective query before invoking their callback, so it can inspect the result it is being asked to approve.
Controlled and uncontrolled queries
resolveCandidateQuery({ query, storeQuery, defaultQuery, fallbackQuery }, { idGenerator }) applies the precedence a query builder uses to decide what to render: the controlled query, then whatever is already in its store, then the uncontrolled defaultQuery, then a freshly created empty group. The result is passed through prepareRuleGroup unless it already has an id, which is taken to mean it has been prepared before—most often because the caller is passing back the object it received from onQueryChange.
Paths
derivePathInfo(path, childCount, { disabled, disabledPaths }) returns { path, disabled } for each child of a group, applying the rule that a child is disabled if its parent is disabled or its own path appears in disabledPaths. The usePathsMemo hook wraps it to keep the array referentially stable across renders.
Inspection
isIC(): boolean— Whether the current query uses independent combinators.signatureOf(other: RuleGroupTypeAny): string— How the current query differs fromother, using the same signature scheme as undo/redo coalescing.diagnostics(): DiagnosticsResult— Shorthand forformat('diagnostics').toJSON(): RuleGroupTypeAny— Returns the current query, soJSON.stringify(q)matchesJSON.stringify(q.getQuery()).
Conversion
toIC() and fromIC() return a new manager with the same configuration and the query converted to or from the independent combinators structure (see convertQuery). Both are idempotent and never modify the original. As with clone, subscribers and history are not carried over.
const ic = q.toIC(); // QueryManager<RuleGroupTypeIC>
q.getQuery(); // unchanged
transform(options?) runs transformQuery against the current query and returns its result directly, rather than a new manager — transformQuery can produce arbitrary shapes that are no longer valid queries. The manager is never modified.
q.transform({ propertyMap: { combinator: 'AndOr' } });