Recipes

Build an AG Grid Query Builder Filter Panel

Use an external panel to build nested filters for AG Grid.

What this recipe builds

  • An external panel for building advanced AG Grid filters.
  • A small conversion function applies nested AND/OR rules to each row.
  • The same filter data can be sent to your server when rows are not loaded in the browser.
Loading the interactive recipe demo...

Install and imports

Install / imports
npm install @vojtechportes/react-query-builder ag-grid-react ag-grid-community

Fields and initial query

Typed field configuration
const fields: IBuilderFieldProps[] = [
{ field: 'athlete', label: 'Athlete', type: 'TEXT' },
{ field: 'age', label: 'Age', type: 'NUMBER' },
{ field: 'country', label: 'Country', type: 'TEXT' },
];
const initialQuery: DenormalizedQuery = [
{
type: 'GROUP',
value: 'AND',
isNegated: false,
children: [{ field: 'age', operator: 'LARGER_EQUAL', value: 18 }],
},
];

Builder implementation

React implementation
import '@vojtechportes/react-query-builder/styles.css';
import React from 'react';
import {
AllCommunityModule,
ModuleRegistry,
type GridApi,
} from 'ag-grid-community';
import { AgGridReact } from 'ag-grid-react';
import {
Builder,
type DenormalizedQuery,
type IBuilderFieldProps,
} from '@vojtechportes/react-query-builder';
ModuleRegistry.registerModules([AllCommunityModule]);
const fields: IBuilderFieldProps[] = [
{
field: 'athlete',
label: 'Athlete',
type: 'TEXT',
operators: ['EQUAL', 'CONTAINS'],
},
{
field: 'age',
label: 'Age',
type: 'NUMBER',
operators: ['EQUAL', 'LARGER_EQUAL', 'SMALLER_EQUAL'],
},
];
const initialQuery: DenormalizedQuery = [
{
type: 'GROUP',
value: 'AND',
isNegated: false,
children: [{ field: 'age', operator: 'LARGER_EQUAL', value: 18 }],
},
];
const rows = [
{ athlete: 'Ada', age: 36 },
{ athlete: 'Grace', age: 17 },
];
export const AgGridExternalFilter = () => {
const apiRef = React.useRef<GridApi | null>(null);
const [query, setQuery] = React.useState(initialQuery);
React.useEffect(() => apiRef.current?.onFilterChanged(), [query]);
const rule = 'type' in query[0] ? query[0].children[0] : query[0];
const matches = (row: (typeof rows)[number]) =>
!rule || 'type' in rule || Number(row.age) >= Number(rule.value);
return (
<>
<Builder fields={fields} data={query} onChange={setQuery} />
<AgGridReact
rowData={rows}
columnDefs={[{ field: 'athlete' }, { field: 'age' }]}
onGridReady={(event) => (apiRef.current = event.api)}
isExternalFilterPresent={() => Boolean(rule)}
doesExternalFilterPass={(node) =>
Boolean(node.data && matches(node.data))
}
/>
</>
);
};

Evaluate the external filter

Evaluate the external filter
const matchesQuery = (row: Athlete, query: DenormalizedQuery) =>
evaluateGroup(query[0], (rule) =>
compare(row[rule.field], rule.operator, rule.value)
);

Validation and safety

  • Browser-side filtering only hides rows; it does not control which rows a user is allowed to access.
  • For server-side row models, validate field names, operators, values, group depth, and page limits.

Production notes

  • Rebuild the row-matching function only when the filter changes.
  • For large datasets, validate and apply the query on the server instead of scanning rows in the browser.

Related guides

Frequently asked questions

Can I use Builder together with AG Grid column filters?

Yes. Decide whether rows must match both filters or only one of them, then apply that rule consistently.

When should filtering move to the server?

Use server filtering when the dataset is too large to load in the browser or contains records the current user should not receive.

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