Skip to main content
Version: Next

Migrating from jQuery QueryBuilder

Refer to the TypeScript reference page for information about the types and interfaces referenced below.

This guide covers moving an existing jQuery QueryBuilder (jQQB) implementation to React Query Builder (RQB). For a broader overview, see Comparison with other libraries.

RQB's original API was modeled on jQQB, so most concepts map directly. Both libraries store queries as plain JSON with nearly the same structure. Most of the work is renaming properties and operators.

tip

Not moving to React? Official Solid, Svelte, and Vue ports use the same query format, fields, operators, and import/export utilities, so everything below except the component examples applies to them as well.

Concept mapping​

jQuery QueryBuilderReact Query Builder
$('#builder').queryBuilder({ ... })A <QueryBuilder /> element
filtersfields
operators (global)operators / getOperators
conditionscombinators
default_conditioncombinator of the initial query
rules option / setRules()query / defaultQuery
getRules()The query object itself (onQueryChange)
getSQL() / getMongo()formatQuery(query, 'sql') / formatQuery(query, 'mongodb_query')
setRulesFromSQL() / setRulesFromMongo()parseSQL / parseMongoDB
validate()validator prop, per-field validator, defaultValidator
reset()Set the query to { combinator: 'and', rules: [] }
templates, iconscontrolElements, controlClassnames, translations
lang_code / lang / i18n filestranslations
*.queryBuilder eventsonQueryChange, onAddRule, onAddGroup, onRemove
Rule/group dataArbitrary extra properties on rules and groups (preserved as-is)

Settings​

jQQB settingRQB equivalent
allow_groups: falsemaxLevels={0} (see also Limiting groups)
allow_groups: <n>maxLevels={n}
allow_emptyReturn true from a query-level validator for empty queries (RQB doesn't fail validation on empty queries by default)
default_filtergetDefaultField
display_empty_filter: falseautoSelectField (default true)
select_placeholdertranslations.fields.placeholderLabel
sort_filtersSort the fields array before passing it in
optgroups / filter optgroupOption groups in fields, operators, or values
inputs_separatorCustom valueEditor

Plugins​

jQQB pluginRQB equivalent
sortable@react-querybuilder/dnd with enableDragAndDrop
not-groupshowNotToggle
sql-supportformatQuery (sql, parameterized, parameterized_named) and parseSQL
mongodb-supportformatQuery (mongodb_query) and parseMongoDB
invertNo built-in action. Toggle not on the group, or write a custom action using query tools
unique-filterFilter the field list in a custom fieldSelector
change-filtersPass a new fields array, since props are reactive
filter-descriptionCustom fieldSelector or field label
bt-tooltip-errorsValidation results are applied as queryBuilder-valid/queryBuilder-invalid classes and passed to every component as validation
bt-checkbox, bt-selectpicker, chosen-selectpickerA compatibility package (e.g. Bootstrap) or a custom valueEditor
Filter plugin / plugin_config (datepickers, sliders, etc.)A custom valueEditor, or @react-querybuilder/datetime for dates

Rendering the component​

Before (jQQB)
<div id="builder"></div>
<button id="btn-sql">Get SQL</button>

<script>
$('#builder').queryBuilder({
plugins: ['sortable', 'not-group'],
allow_groups: 2,
filters: [
{ id: 'name', label: 'Name', type: 'string' },
{
id: 'category',
label: 'Category',
type: 'integer',
input: 'select',
values: { 1: 'Books', 2: 'Movies' },
operators: ['equal', 'not_equal', 'in', 'not_in'],
},
{ id: 'price', label: 'Price', type: 'double', validation: { min: 0, step: 0.01 } },
],
rules: {
condition: 'AND',
rules: [{ id: 'price', operator: 'less', value: 10.25 }],
},
});

$('#btn-sql').on('click', () => {
console.log($('#builder').queryBuilder('getSQL', 'question_mark'));
});
</script>
After (RQB)
import { useState } from 'react';
import type { Field, RuleGroupType } from 'react-querybuilder';
import { formatQuery, QueryBuilder } from 'react-querybuilder';
import { QueryBuilderDnD } from '@react-querybuilder/dnd';
import * as ReactDnD from 'react-dnd';
import * as ReactDndHtml5Backend from 'react-dnd-html5-backend';
import 'react-querybuilder/dist/query-builder.css';

const fields: Field[] = [
{ name: 'name', label: 'Name' },
{
name: 'category',
label: 'Category',
valueEditorType: 'select',
values: [
{ name: '1', label: 'Books' },
{ name: '2', label: 'Movies' },
],
operators: [
{ name: '=', label: '=' },
{ name: '!=', label: '!=' },
{ name: 'in', label: 'in' },
{ name: 'notIn', label: 'not in' },
],
},
{
name: 'price',
label: 'Price',
inputType: 'number',
validator: ({ value }) => Number(value) >= 0,
},
];

const initialQuery: RuleGroupType = {
combinator: 'and',
rules: [{ field: 'price', operator: '<', value: 10.25 }],
};

export const App = () => {
const [query, setQuery] = useState(initialQuery);

return (
<>
<QueryBuilderDnD dnd={{ ...ReactDnD, ...ReactDndHtml5Backend }}>
<QueryBuilder
fields={fields}
query={query}
onQueryChange={setQuery}
showNotToggle
maxLevels={2}
parseNumbers="strict-limited"
/>
</QueryBuilderDnD>
<pre>{JSON.stringify(formatQuery(query, 'parameterized'), null, 2)}</pre>
</>
);
};

Notable differences:

  • There's no imperative instance. The query is React state, so "get rules" means reading the state and "set rules" means updating it.
  • formatQuery(query, 'parameterized') returns { sql, params }, like getSQL('question_mark'). Use 'parameterized_named' for named parameters (like getSQL('named')) and 'sql' for inline values (like getSQL(false)).
  • jQQB's type: 'integer' | 'double' makes values numeric. RQB value editors produce strings by default; use parseNumbers to get numbers.

Operator mapping​

jQQBRQB
equal / not_equal= / !=
less / less_or_equal< / <=
greater / greater_or_equal> / >=
in / not_inin / notIn
between / not_betweenbetween / notBetween
begins_with / not_begins_withbeginsWith / doesNotBeginWith
contains / not_containscontains / doesNotContain
ends_with / not_ends_withendsWith / doesNotEndWith
is_null / is_not_nullnull / notNull
is_empty / is_not_empty= / != with an empty string

Custom jQQB operators ({ type, nb_inputs, multiple, apply_to }) become RQB operators with an arity ('unary' for nb_inputs: 0, 'binary' for 1, 'ternary' for 2). You'll also need a custom rule processor for each export format the operator should support.

Converting filters and rules​

There's no migration package for jQQB, since the conversion is small enough to include in your own code. The utilities below convert jQQB filters to RQB fields and getRules() output to an RQB query. Copy them into your project and adjust as needed.

convertJQQB.ts
import type { Field, Option, RuleGroupType, RuleType } from 'react-querybuilder';

type JQQBValues =
| (string | number)[]
| Record<string, string>
| Record<string, string>[]
| { value: string | number; label: string; optgroup?: string }[];

export interface JQQBFilter {
id: string;
field?: string;
label?: string | Record<string, string>;
type: 'string' | 'integer' | 'double' | 'date' | 'time' | 'datetime' | 'boolean';
input?:
| 'text'
| 'number'
| 'textarea'
| 'radio'
| 'checkbox'
| 'select'
| ((...args: unknown[]) => string);
values?: JQQBValues;
multiple?: boolean;
operators?: string[];
default_operator?: string;
default_value?: unknown;
placeholder?: string;
}

export interface JQQBRule {
id?: string;
field?: string;
operator?: string;
value?: unknown;
flags?: Record<string, boolean>;
data?: unknown;
}

export interface JQQBGroup {
condition?: string;
not?: boolean;
rules: (JQQBRule | JQQBGroup)[];
flags?: Record<string, boolean>;
data?: unknown;
}

export const operatorMap: Record<string, string> = {
equal: '=',
not_equal: '!=',
less: '<',
less_or_equal: '<=',
greater: '>',
greater_or_equal: '>=',
in: 'in',
not_in: 'notIn',
between: 'between',
not_between: 'notBetween',
begins_with: 'beginsWith',
not_begins_with: 'doesNotBeginWith',
contains: 'contains',
not_contains: 'doesNotContain',
ends_with: 'endsWith',
not_ends_with: 'doesNotEndWith',
is_null: 'null',
is_not_null: 'notNull',
is_empty: '=',
is_not_empty: '!=',
};

const inputTypeMap: Partial<Record<JQQBFilter['type'], string>> = {
integer: 'number',
double: 'number',
date: 'date',
time: 'time',
datetime: 'datetime-local',
};

const convertValues = (values: JQQBValues): Option[] => {
if (Array.isArray(values)) {
return values.flatMap((v): Option[] => {
if (typeof v !== 'object') return [{ name: `${v}`, label: `${v}` }];
if ('value' in v && 'label' in v) return [{ name: `${v.value}`, label: `${v.label}` }];
return Object.entries(v).map(([name, label]) => ({ name, label: `${label}` }));
});
}
return Object.entries(values).map(([name, label]) => ({ name, label }));
};

export const convertJQQBFilters = (filters: JQQBFilter[], lang = 'en'): Field[] =>
filters.map(f => {
const field: Field = {
// jQQB allows several filters per field; RQB field names must be unique
name: f.id,
label:
typeof f.label === 'object' ? (f.label[lang] ?? f.label.en) : (f.label ?? f.field ?? f.id),
};
const inputType = inputTypeMap[f.type];
if (inputType) field.inputType = inputType;
if (f.input === 'select') field.valueEditorType = f.multiple ? 'multiselect' : 'select';
else if (f.input === 'radio') field.valueEditorType = 'radio';
else if (f.input === 'textarea') field.valueEditorType = 'textarea';
else if (f.input === 'checkbox') field.valueEditorType = f.values ? 'multiselect' : 'checkbox';
else if (f.type === 'boolean') field.valueEditorType = 'checkbox';
if (f.values) field.values = convertValues(f.values);
if (f.operators) {
field.operators = f.operators.map(op => ({
name: operatorMap[op] ?? op,
label: op.replaceAll('_', ' '),
}));
}
if (f.default_operator)
field.defaultOperator = operatorMap[f.default_operator] ?? f.default_operator;
if (f.default_value !== undefined) field.defaultValue = f.default_value;
if (f.placeholder) field.placeholder = f.placeholder;
return field;
});

const isGroup = (r: JQQBRule | JQQBGroup): r is JQQBGroup => 'rules' in r && Array.isArray(r.rules);

const isReadonly = (flags?: Record<string, boolean>) =>
!!flags && (flags.readonly || flags.filter_readonly || flags.condition_readonly);

export const convertJQQBRules = (group: JQQBGroup): RuleGroupType => ({
combinator: (group.condition ?? 'AND').toLowerCase(),
...(group.not ? { not: true } : {}),
...(isReadonly(group.flags) ? { disabled: true } : {}),
...(group.data === undefined ? {} : { data: group.data }),
rules: group.rules.map(r => (isGroup(r) ? convertJQQBRules(r) : convertJQQBRule(r))),
});

const convertJQQBRule = (r: JQQBRule): RuleType => {
const operator = r.operator ?? 'equal';
const value = operator === 'is_empty' || operator === 'is_not_empty' ? '' : (r.value ?? '');
return {
field: r.id ?? r.field ?? '',
operator: operatorMap[operator] ?? operator,
value,
...(isReadonly(r.flags) ? { disabled: true } : {}),
...(r.data === undefined ? {} : { data: r.data }),
};
};
Usage
import { convertJQQBFilters, convertJQQBRules } from './convertJQQB';

const fields = convertJQQBFilters(jqqbFilters);
const query = convertJQQBRules($('#builder').queryBuilder('getRules', { get_flags: true }));

Translation notes:

  • Field identity. jQQB rules reference a filter by id, and several filters can share one field. RQB fields are identified by name, so the converter uses the jQQB id. If your filters use id and field differently, set each RQB field's name to the id and rename it in exports with a custom ruleProcessor.
  • Values. Multi-value operators (between, in, multiple selects) produce arrays in both libraries, and RQB accepts arrays as is. Pass listsAsArrays so RQB's value editors also emit arrays instead of comma-separated strings.
  • Flags. RQB has a single disabled state. The converter maps readonly, filter_readonly, and condition_readonly to disabled. Finer-grained flags (operator_readonly, value_readonly, no_delete, no_add_rule, no_add_group) need custom controlElements that read the rule's properties. Keep the flags object on the rule if you need them.
  • is_empty / is_not_empty. These become = / != with an empty string. If you need a true "empty" check (e.g. NULL OR ''), define custom operators.
  • Validation. jQQB's declarative validation (min, max, step, format, allow_empty_value, callback) becomes a per-field validator function. Number bounds and step can also be passed to the value editor via inputType and custom value editor props.
  • SQL and MongoDB round-trips. If your queries are stored as SQL or MongoDB rather than jQQB JSON, skip convertJQQBRules and use parseSQL or parseMongoDB directly.