DataTable
Sort, filter, select, paginate.
The DataTable rendered by defineResource() handles sort, filter, pagination, search, row selection, and per-column cell rendering — all from the column config. This lesson tours the knobs.
The default surface
From the previous lesson's minimal config you got:
- Sortable column headers (click to sort)
- Pagination at the bottom (20 rows / page default)
- Search bar (searches columns with
searchable: true) - Row action menu (Edit / Delete / View)
- Bulk selection checkboxes
- Loading + empty + error states out of the box
Adding filters
filters: [{type: 'select',name: 'status',label: 'Status',options: [{ value: 'active', label: 'Active' },{ value: 'archived', label: 'Archived' },],},{type: 'date-range',name: 'created_between',label: 'Created',},{type: 'multi-select',name: 'category_id',label: 'Category',optionsFrom: '/api/categories', // populates from your API},]
Each filter appears in the toolbar. Selected values become URL query params, so a filtered view is shareable / bookmarkable / back-button-friendly.
Per-column custom render
columns: [{ key: 'name', label: 'Name' },{key: 'status',label: 'Status',format: 'badge',badgeColor: (v) => v === 'active' ? 'green' : 'gray',},{key: 'image_url',label: 'Image',format: 'image',width: 60,},{key: 'price',label: 'Price',cell: (row) => <strong className="text-green-600">{formatCurrency(row.price)}</strong>,},]
format is the quick-and-clean shortcut. cell is the escape hatch for custom JSX.
Bulk actions
bulkActions: [{label: 'Archive',icon: Archive,onClick: async (selectedIds, refresh) => {await fetch('/api/products/bulk-archive', {method: 'POST',body: JSON.stringify({ ids: selectedIds }),})refresh() // re-fetches the table},},{label: 'Export as CSV',onClick: (ids) => window.open(`/api/products/export?ids=${ids.join(',')}`),},]
Bulk actions appear in a toolbar that slides up when rows are selected. The handler gets the array of selected IDs + refresh().
Sorting + URL state
Every filter, sort, page number, and search query syncs to the URL:
/resources/products?page=2&sort=price&order=desc&q=widget&status=active
A user can bookmark a filtered view, send the URL to a teammate, or hit the back button to undo a filter. No work on your part.
searchable: true issues a WHERE col ILIKE '%q%' on every keystroke. On a 100K- row table without an index, this hangs the UI. Add a GIN trigram index for search columns.Per-row actions vs. bulk actions
- Row actions live in the "…" menu at the end of each row. Default: Edit, Delete, View.
- Bulk actions appear above the table when rows are selected. Use for "archive 50 things" type workflows.
Quick check
Try it
Extend your products admin with two improvements:
- Make the
namecolumn searchable. - Add a select filter for an
is_active: booleancolumn (you may need to add this field to the Product model + grit migrate + grit sync). - Add a bulk action "Mark as inactive" that flips
is_activeto false on selected rows. - Test that the URL changes when you filter/search.
Paste the URL of a filtered+searched+sorted state in notes.md.
What's next
Last lesson of this chapter — FormBuilder. The forms behind New / Edit, with all 8 field types in action.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled — suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub