Recipes

React Hook Form Query Builder Integration

Treat the query as a form field with validation, change tracking, submission, and reset.

What this recipe builds

  • Query data participates in form validation and change tracking.
  • Submit and reset use the same default values.
  • Builder validation remains visible beside form errors.
Loading the interactive recipe demo...

Install and imports

Install / imports
npm install @vojtechportes/react-query-builder react-hook-form

Fields and initial query

Typed field configuration
type FilterForm = { name: string; query: DenormalizedQuery };
const fields: IBuilderFieldProps[] = [
{ field: 'email', label: 'Email', type: 'TEXT' },
{ field: 'createdAt', label: 'Created', type: 'DATE' },
];
const initialQuery: DenormalizedQuery = [
{
type: 'GROUP',
value: 'AND',
isNegated: false,
children: [],
},
];

Builder implementation

React implementation
import '@vojtechportes/react-query-builder/styles.css';
import React from 'react';
import { Controller, useForm } from 'react-hook-form';
import {
Builder,
type DenormalizedQuery,
type IBuilderFieldProps,
} from '@vojtechportes/react-query-builder';
interface IFilterForm {
name: string;
query: DenormalizedQuery;
}
const fields: IBuilderFieldProps[] = [
{
field: 'email',
label: 'Email',
type: 'TEXT',
operators: ['EQUAL', 'CONTAINS'],
},
{
field: 'createdAt',
label: 'Created',
type: 'DATE',
operators: ['EQUAL', 'LARGER_EQUAL', 'SMALLER_EQUAL'],
},
];
const initialQuery: DenormalizedQuery = [
{
type: 'GROUP',
value: 'AND',
isNegated: false,
children: [{ field: 'email', operator: 'CONTAINS', value: '@example.com' }],
},
];
export const HookFormFilter = () => {
const { control, handleSubmit, reset, formState } = useForm<IFilterForm>({
defaultValues: { name: 'Customer search', query: initialQuery },
});
return (
<form onSubmit={handleSubmit(console.log)}>
<Controller
name="query"
control={control}
rules={{ validate: (value) => value.length > 0 || 'Add a query root.' }}
render={({ field }) => (
<Builder
fields={fields}
data={field.value}
onChange={field.onChange}
showValidation
/>
)}
/>
<button type="submit" disabled={!formState.isDirty}>
Save
</button>
<button type="button" onClick={() => reset()}>
Reset
</button>
</form>
);
};

Submit the filter form

Submit the filter form
const saveFilter = ({ name, query }: FilterForm) =>
fetch('/api/filter-presets', {
method: 'POST',
body: JSON.stringify({ name, query }),
});

Validation and safety

  • Validate submitted query data again when it reaches your API.
  • Client-side validation only checks the form values. Your server must still decide what the user is allowed to access.

Production notes

  • Keep defaultValues stable and call reset after a successful save.
  • Do not copy the same form value into a separate useState. Let Controller manage it.

Related guides

Frequently asked questions

Why use Controller instead of register?

Builder does not behave like a native text input. Controller passes its value and changes to React Hook Form for you.

Will React Hook Form reset update Builder too?

Yes, as long as Builder receives its value from Controller. React Hook Form restores defaultValues and passes the restored filter back to Builder.

© Vojtěch Václav Porteš 2026 - All library contents are available under the MIT license.
Loading privacy preferences...