Local Filter, Sort, Group
A filter dropdown, a sort dropdown, and a grouping dropdown share one localModel pipeline. The pipeline runs filter → sort → group; each control publishes one action and only the affected stage (and the stages after it) rerun.
APIs used
Section titled “APIs used”localModel()pipelineto declare transformation orderstageto attach an action to a pipeline stepmodel.send()to publish filter, sort, and group changesGroupHeaderCell
import { useEffect, useState } from 'react'
import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader, GroupHeaderCell } from '@/components/ui/data-table'
import { localModel } from '@virtuoso.dev/data-table'
import { FilterBar } from './FilterBar'
import { groupProducts, products } from './data'
import type { Product, ProductGroup } from './data'
function isProduct(item: Product | ProductGroup): item is Product {
return 'id' in item
}
export default function App() {
const [category, setCategory] = useState('all')
const [sortBy, setSortBy] = useState<'name' | 'price' | 'revenue'>('name')
const [groupBy, setGroupBy] = useState<'none' | 'category' | 'status'>('category')
const [model] = useState(() =>
localModel<Product, ProductGroup>({
data: products,
pipeline: ['filter', 'sort', 'group'],
actions: {
filter: {
stage: 'filter',
handler: ({ data, payload }) => {
const nextCategory = payload as string
const rows = data.filter(isProduct)
return nextCategory === 'all' ? rows : rows.filter((product) => product.category === nextCategory)
},
},
sort: {
stage: 'sort',
handler: ({ data, payload }) => {
const field = payload as 'name' | 'price' | 'revenue'
return data.filter(isProduct).toSorted((a, b) => (a[field] < b[field] ? -1 : a[field] > b[field] ? 1 : 0))
},
},
group: {
stage: 'group',
handler: ({ data, payload }) => groupProducts(data.filter(isProduct), payload as 'none' | 'category' | 'status'),
},
},
})
)
useEffect(() => {
model.send({ action: 'filter', payload: category })
}, [category, model])
useEffect(() => {
model.send({ action: 'sort', payload: sortBy })
}, [sortBy, model])
useEffect(() => {
model.send({ action: 'group', payload: groupBy })
}, [groupBy, model])
return (
<div className="space-y-4">
<FilterBar
category={category}
groupBy={groupBy}
onCategoryChange={setCategory}
onGroupByChange={setGroupBy}
onSortByChange={setSortBy}
sortBy={sortBy}
/>
<DataTable className="rounded-xl" model={model} style={{ height: 420 }}>
<GroupHeaderCell className="bg-muted px-3 py-2 font-medium">{({ row }) => row.data.label}</GroupHeaderCell>
<DataTableColumn field="name">
<DataTableColumnHeader>Product</DataTableColumnHeader>
<DataTableCell className="font-medium">{({ row }) => row.data.name}</DataTableCell>
</DataTableColumn>
<DataTableColumn field="category">
<DataTableColumnHeader>Category</DataTableColumnHeader>
<DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
<DataTableColumn field="status">
<DataTableColumnHeader>Status</DataTableColumnHeader>
<DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
</DataTable>
</div>
)
}When to outgrow this shape
Section titled “When to outgrow this shape”- The full dataset doesn’t fit in memory — switch to
remoteModeland move filter/sort/group into request params. - Filtering rules need server-side evaluation (permissions, complex SQL, full-text search) — keep the table local for display but fetch filtered rows.