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} /><AgGridReactrowData={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.