Want to skip to the implementation? Check out these React examples:
import { useTable, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/react-table'
const features = tableFeatures({ columnFilteringFeature })
const table = useTable({
features,
rowModels: {
filteredRowModel: createFilteredRowModel(filterFns),
},
columns,
data,
})import { useTable, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFns } from '@tanstack/react-table'
const features = tableFeatures({ columnFilteringFeature })
const table = useTable({
features,
rowModels: {
filteredRowModel: createFilteredRowModel(filterFns),
},
columns,
data,
})Filtering comes in 2 flavors: Column Filtering and Global Filtering.
This guide will focus on column filtering, which is a filter that is applied to a single column's accessor value.
TanStack table supports both client-side and manual server-side filtering. This guide will go over how to implement and customize both, and help you decide which one is best for your use-case.
If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc.
However, as also discussed in the Pagination Guide, a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides.
TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first.
Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are:
If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows.
If you have decided that you need to implement server-side filtering instead of using the built-in client-side filtering, here's how you do that.
No filteredRowModel is needed for manual server-side filtering. Instead, the data that you pass to the table should already be filtered. However, if you have added a filteredRowModel to rowModels, you can tell the table to skip it by setting the manualFiltering option to true.
const features = tableFeatures({ columnFilteringFeature })
const table = useTable({
features,
rowModels: {}, // no filteredRowModel needed for manual server-side filtering
data,
columns,
manualFiltering: true,
})const features = tableFeatures({ columnFilteringFeature })
const table = useTable({
features,
rowModels: {}, // no filteredRowModel needed for manual server-side filtering
data,
columns,
manualFiltering: true,
})Note: When using manual filtering, many of the options that are discussed in the rest of this guide will have no effect. When manualFiltering is set to true, the table instance will not apply any filtering logic to the rows that are passed to it. Instead, it will assume that the rows are already filtered and will use the data that you pass to it as-is.
If you are using the built-in client-side filtering features, add the columnFilteringFeature to your features and the filteredRowModel to your row models. Import createFilteredRowModel and filterFns from TanStack Table:
import {
useTable,
tableFeatures,
columnFilteringFeature,
createFilteredRowModel,
filterFns,
} from '@tanstack/react-table'
const features = tableFeatures({ columnFilteringFeature })
const table = useTable({
features,
rowModels: {
filteredRowModel: createFilteredRowModel(filterFns),
},
data,
columns,
})import {
useTable,
tableFeatures,
columnFilteringFeature,
createFilteredRowModel,
filterFns,
} from '@tanstack/react-table'
const features = tableFeatures({ columnFilteringFeature })
const table = useTable({
features,
rowModels: {
filteredRowModel: createFilteredRowModel(filterFns),
},
data,
columns,
})Whether or not you use client-side or server-side filtering, you can take advantage of the built-in column filter state management that TanStack Table provides. There are many table and column APIs to mutate and interact with the filter state and retrieving the column filter state.
The column filtering state is defined as an array of objects with the following shape:
interface ColumnFilter {
id: string
value: unknown
}
type ColumnFiltersState = ColumnFilter[]interface ColumnFilter {
id: string
value: unknown
}
type ColumnFiltersState = ColumnFilter[]Since the column filter state is an array of objects, you can have multiple column filters applied at once.
For reactive reads that should re-render your UI, use table.state.columnFilters (selected by the useTable selector) or table.Subscribe. In event handlers or other non-render code, you can read the current snapshot with table.atoms.columnFilters.get(), but this read does not subscribe the component to future changes.
const table = useTable({
features,
rowModels: { filteredRowModel: createFilteredRowModel(filterFns) },
columns,
data,
//...
})
table.state.columnFilters // reactive read in render
table.atoms.columnFilters.get() // snapshot read in event handlersconst table = useTable({
features,
rowModels: { filteredRowModel: createFilteredRowModel(filterFns) },
columns,
data,
//...
})
table.state.columnFilters // reactive read in render
table.atoms.columnFilters.get() // snapshot read in event handlersHowever, if you need access to the column filter state outside of the table, you can "control" the column filter state like down below.
If you need easy access to the column filter state in other parts of your application, you can own the column filter state slice yourself. The recommended way in v9 is an external atom passed through the atoms table option. Atoms preserve fine-grained subscriptions, and the filter values can be used elsewhere (such as in a query key for server-side filtering) without forcing the component that owns the table to re-render.
import { useCreateAtom, useSelector } from '@tanstack/react-store'
const columnFiltersAtom = useCreateAtom<ColumnFiltersState>([]) // can set initial column filter state here
// subscribe to the atom wherever you need the value (e.g. for a query key)
const columnFilters = useSelector(columnFiltersAtom)
const table = useTable({
features,
rowModels: { filteredRowModel: createFilteredRowModel(filterFns) },
columns,
data,
//...
atoms: {
columnFilters: columnFiltersAtom, // table filter APIs now update columnFiltersAtom
},
})import { useCreateAtom, useSelector } from '@tanstack/react-store'
const columnFiltersAtom = useCreateAtom<ColumnFiltersState>([]) // can set initial column filter state here
// subscribe to the atom wherever you need the value (e.g. for a query key)
const columnFilters = useSelector(columnFiltersAtom)
const table = useTable({
features,
rowModels: { filteredRowModel: createFilteredRowModel(filterFns) },
columns,
data,
//...
atoms: {
columnFilters: columnFiltersAtom, // table filter APIs now update columnFiltersAtom
},
})Alternatively, the v8-style state.columnFilters plus onColumnFiltersChange pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the Table State Guide for a deeper comparison.
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
//...
const table = useTable({
features,
rowModels: { filteredRowModel: createFilteredRowModel(filterFns) },
columns,
data,
//...
state: {
columnFilters,
},
onColumnFiltersChange: setColumnFilters,
})const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
//...
const table = useTable({
features,
rowModels: { filteredRowModel: createFilteredRowModel(filterFns) },
columns,
data,
//...
state: {
columnFilters,
},
onColumnFiltersChange: setColumnFilters,
})If you do not need to control the column filter state in your own state management or scope, but you still want to set an initial column filter state, you can use the initialState table option instead of state.
const table = useTable({
features,
rowModels: { filteredRowModel: createFilteredRowModel(filterFns) },
columns,
data,
//...
initialState: {
columnFilters: [
{
id: 'name',
value: 'John', // filter the name column by 'John' by default
},
],
},
})const table = useTable({
features,
rowModels: { filteredRowModel: createFilteredRowModel(filterFns) },
columns,
data,
//...
initialState: {
columnFilters: [
{
id: 'name',
value: 'John', // filter the name column by 'John' by default
},
],
},
})NOTE: Do not use both initialState.columnFilters and state.columnFilters at the same time, as the controlled state.columnFilters value will override the initialState.columnFilters.
Each column can have its own unique filtering logic. Choose from any of the filter functions that are provided by TanStack Table, or create your own.
By default there are 12 built-in filter functions to choose from:
You can also define your own custom filter functions, either inline as the filterFn column option, or by name in the filter function registry that you pass to createFilteredRowModel.
Note: These filter functions only run during client-side filtering.
Whether you register a custom filter function in the registry passed to createFilteredRowModel or pass it directly as a filterFn column option, it should have the following signature:
const myCustomFilterFn: FilterFn<typeof features, MyData> = (
row, // Row<typeof features, MyData>
columnId: string,
filterValue: any,
addMeta?: (meta: FilterMeta) => void,
): boolean => ...const myCustomFilterFn: FilterFn<typeof features, MyData> = (
row, // Row<typeof features, MyData>
columnId: string,
filterValue: any,
addMeta?: (meta: FilterMeta) => void,
): boolean => ...Every filter function receives:
and should return true if the row should be included in the filtered rows, and false if it should be removed.
const columns = [
{
header: () => 'Name',
accessorKey: 'name',
filterFn: 'includesString', // use built-in filter function
},
{
header: () => 'Age',
accessorKey: 'age',
filterFn: 'inNumberRange',
},
{
header: () => 'Birthday',
accessorKey: 'birthday',
filterFn: 'myCustomFilterFn', // reference a custom filter function registered with createFilteredRowModel
},
{
header: () => 'Profile',
accessorKey: 'profile',
// use custom filter function directly
filterFn: (row, columnId, filterValue) => {
return // true or false based on your custom logic
},
}
]
//...
const table = useTable({
features,
rowModels: {
filteredRowModel: createFilteredRowModel({
...filterFns,
myCustomFilterFn: (row, columnId, filterValue) => {
return // true or false based on your custom logic
},
startsWith: startsWithFilterFn, // defined elsewhere
}),
},
columns,
data,
})const columns = [
{
header: () => 'Name',
accessorKey: 'name',
filterFn: 'includesString', // use built-in filter function
},
{
header: () => 'Age',
accessorKey: 'age',
filterFn: 'inNumberRange',
},
{
header: () => 'Birthday',
accessorKey: 'birthday',
filterFn: 'myCustomFilterFn', // reference a custom filter function registered with createFilteredRowModel
},
{
header: () => 'Profile',
accessorKey: 'profile',
// use custom filter function directly
filterFn: (row, columnId, filterValue) => {
return // true or false based on your custom logic
},
}
]
//...
const table = useTable({
features,
rowModels: {
filteredRowModel: createFilteredRowModel({
...filterFns,
myCustomFilterFn: (row, columnId, filterValue) => {
return // true or false based on your custom logic
},
startsWith: startsWithFilterFn, // defined elsewhere
}),
},
columns,
data,
})TypeScript Note: For filterFn: 'myCustomFilterFn' string references to typecheck, augment the FilterFns interface with a declare module block:
tsxdeclare module '@tanstack/react-table' { interface FilterFns { myCustomFilterFn: FilterFn<typeof features, MyData> } }declare module '@tanstack/react-table' { interface FilterFns { myCustomFilterFn: FilterFn<typeof features, MyData> } }Alternatively, skip the registry and the augmentation entirely by passing the function directly to the filterFn column option. See the Fuzzy Search example for a complete registration with module augmentation.
You can attach a few other properties to filter functions to customize their behavior:
filterFn.resolveFilterValue - This optional "hanging" method on any given filterFn allows the filter function to transform/sanitize/format the filter value before it is passed to the filter function.
filterFn.autoRemove - This optional "hanging" method on any given filterFn is passed a filter value and expected to return true if the filter value should be removed from the filter state. eg. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to false.
const startsWithFilterFn = <TFeatures extends TableFeatures, TData extends RowData>(
row: Row<TFeatures, TData>,
columnId: string,
filterValue: string, // resolveFilterValue below transforms the raw value to a string
) =>
row
.getValue<number | string>(columnId)
.toString()
.toLowerCase()
.trim()
.startsWith(filterValue); // toString, toLowerCase, and trim the filter value in `resolveFilterValue`
// remove the filter value from filter state if it is falsy (empty string in this case)
startsWithFilterFn.autoRemove = (val: any) => !val;
// transform/sanitize/format the filter value before it is passed to the filter function
startsWithFilterFn.resolveFilterValue = (val: any) => val.toString().toLowerCase().trim();const startsWithFilterFn = <TFeatures extends TableFeatures, TData extends RowData>(
row: Row<TFeatures, TData>,
columnId: string,
filterValue: string, // resolveFilterValue below transforms the raw value to a string
) =>
row
.getValue<number | string>(columnId)
.toString()
.toLowerCase()
.trim()
.startsWith(filterValue); // toString, toLowerCase, and trim the filter value in `resolveFilterValue`
// remove the filter value from filter state if it is falsy (empty string in this case)
startsWithFilterFn.autoRemove = (val: any) => !val;
// transform/sanitize/format the filter value before it is passed to the filter function
startsWithFilterFn.resolveFilterValue = (val: any) => val.toString().toLowerCase().trim();There are a lot of table and column options that you can use to further customize the column filtering behavior.
By default, column filtering is enabled for all columns. You can disable the column filtering for all columns or for specific columns by using the enableColumnFilters table option or the enableColumnFilter column option. You can also turn off both column and global filtering by setting the enableFilters table option to false.
Disabling column filtering for a column will cause the column.getCanFilter API to return false for that column.
const columns = [
{
header: () => 'Id',
accessorKey: 'id',
enableColumnFilter: false, // disable column filtering for this column
},
//...
]
//...
const table = useTable({
features,
rowModels: { filteredRowModel: createFilteredRowModel(filterFns) },
columns,
data,
enableColumnFilters: false, // disable column filtering for all columns
})const columns = [
{
header: () => 'Id',
accessorKey: 'id',
enableColumnFilter: false, // disable column filtering for this column
},
//...
]
//...
const table = useTable({
features,
rowModels: { filteredRowModel: createFilteredRowModel(filterFns) },
columns,
data,
enableColumnFilters: false, // disable column filtering for all columns
})There are a few additional table options to customize the behavior of column filtering when using features like expanding, grouping, and aggregation.
By default, filtering is done from parent rows down, so if a parent row is filtered out, all of its child sub-rows will be filtered out as well. Depending on your use-case, this may be the desired behavior if you only want the user to be searching through the top-level rows, and not the sub-rows. This is also the most performant option.
However, if you want to allow sub-rows to be filtered and searched through, regardless of whether the parent row is filtered out, you can set the filterFromLeafRows table option to true. Setting this option to true will cause filtering to be done from leaf rows up, which means parent rows will be included so long as one of their child or grand-child rows is also included.
const features = tableFeatures({ columnFilteringFeature, rowExpandingFeature })
const table = useTable({
features,
rowModels: {
filteredRowModel: createFilteredRowModel(filterFns),
expandedRowModel: createExpandedRowModel(),
},
columns,
data,
filterFromLeafRows: true, // filter and search through sub-rows
})const features = tableFeatures({ columnFilteringFeature, rowExpandingFeature })
const table = useTable({
features,
rowModels: {
filteredRowModel: createFilteredRowModel(filterFns),
expandedRowModel: createExpandedRowModel(),
},
columns,
data,
filterFromLeafRows: true, // filter and search through sub-rows
})By default, filtering is done for all rows in a tree, no matter if they are root level parent rows or the child leaf rows of a parent row. Setting the maxLeafRowFilterDepth table option to 0 will cause filtering to only be applied to the root level parent rows, with all sub-rows remaining unfiltered. Similarly, setting this option to 1 will cause filtering to only be applied to child leaf rows 1 level deep, and so on.
Use maxLeafRowFilterDepth: 0 if you want to preserve a parent row's sub-rows from being filtered out while the parent row is passing the filter.
const features = tableFeatures({ columnFilteringFeature, rowExpandingFeature })
const table = useTable({
features,
rowModels: {
filteredRowModel: createFilteredRowModel(filterFns),
expandedRowModel: createExpandedRowModel(),
},
columns,
data,
maxLeafRowFilterDepth: 0, // only filter root level parent rows out
})const features = tableFeatures({ columnFilteringFeature, rowExpandingFeature })
const table = useTable({
features,
rowModels: {
filteredRowModel: createFilteredRowModel(filterFns),
expandedRowModel: createExpandedRowModel(),
},
columns,
data,
maxLeafRowFilterDepth: 0, // only filter root level parent rows out
})There are a lot of Column and Table APIs that you can use to interact with the column filter state and hook up to your UI components. Here is a list of the available APIs and their most common use-cases:
table.setColumnFilters - Overwrite the entire column filter state with a new state.
table.resetColumnFilters - Useful for a "clear all/reset filters" button.
column.getFilterValue - Useful for getting the default initial filter value for an input, or even directly providing the filter value to a filter input.
column.setFilterValue - Useful for connecting filter inputs to their onChange or onBlur handlers.
column.getCanFilter - Useful for disabling/enabling filter inputs.
column.getIsFiltered - Useful for displaying a visual indicator that a column is currently being filtered.
column.getFilterIndex - Useful for displaying in what order the current filter is being applied.
column.getAutoFilterFn - Used internally to find the default filter function for a column if none is specified.
column.getFilterFn - Useful for displaying which filter mode or function is currently being used.