TanStack Table Filtering with React Query Builder
Use nested Builder rules to decide which TanStack Table rows to show.
What this recipe builds
- React state keeps the Builder and table filters in sync.
- One table filter checks nested AND/OR conditions across several columns.
- Filter data that can be evaluated in the browser or sent to a server.
Loading the interactive recipe demo...
Install and imports
Install / imports
npm install @vojtechportes/react-query-builder @tanstack/react-table
Fields and initial query
Typed field configuration
const fields: IBuilderFieldProps[] = [{ field: 'name', label: 'Name', type: 'TEXT' },{ field: 'role', label: 'Role', type: 'LIST', value: roles },{ field: 'lastActive', label: 'Last active', 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 {createColumnHelper,getCoreRowModel,getFilteredRowModel,useReactTable,} from '@tanstack/react-table';import {Builder,type DenormalizedQuery,type IBuilderFieldProps,} from '@vojtechportes/react-query-builder';interface IUser {name: string;role: string;}const data: IUser[] = [{ name: 'Ada', role: 'ADMIN' },{ name: 'Grace', role: 'VIEWER' },];const helper = createColumnHelper<IUser>();const columns = [helper.accessor('name', { header: 'Name' }),helper.accessor('role', { header: 'Role' }),];const fields: IBuilderFieldProps[] = [{field: 'name',label: 'Name',type: 'TEXT',operators: ['EQUAL', 'CONTAINS'],},{ field: 'role', label: 'Role', type: 'TEXT' },];const initialQuery: DenormalizedQuery = [{type: 'GROUP',value: 'AND',isNegated: false,children: [{ field: 'role', operator: 'EQUAL', value: 'ADMIN' }],},];export const TanstackFilter = () => {const [query, setQuery] = React.useState(initialQuery);const table = useReactTable({data,columns,state: { globalFilter: query },onGlobalFilterChange: setQuery,globalFilterFn: (row) => {const rule = 'type' in query[0] ? query[0].children[0] : query[0];return !rule || 'type' in rule || row.original.role === rule.value;},getCoreRowModel: getCoreRowModel(),getFilteredRowModel: getFilteredRowModel(),});return (<><Builder fields={fields} data={query} onChange={setQuery} /><p>{table.getFilteredRowModel().rows.length} matching users</p></>);};
Check whether each row matches
Check whether each row matches
const matchesUser = (user: User, query: DenormalizedQuery) =>compileQuery<User>(query, fields)(user);
Validation and safety
- Client filtering only controls display and cannot protect records.
- Filter protected rows on the server before sending them to the browser.
Production notes
- Use the useMemo hook so the row-matching function is not rebuilt on every render.
- For server-side pagination, enable manualFiltering in TanStack Table and send the filters with each request.
Related guides
Frequently asked questions
Why does the table not update when the Builder filter changes?
Pass the Builder data to TanStack Table as its globalFilter value and provide a globalFilterFn that checks whether each row matches.
Does this work with server pagination?
Yes; enable manual filtering and send the validated builder data with each request.