This is a generic, modular, reusable table block built on TanStack Table + shadcn/ui. It powers any list: students, teachers, classes, subjects, invoices, etc. It supports client UX with URL-synced state and server-driven pagination/filtering/sorting for scale.
DataTable renderer + useDataTable hook + filter/sort/pagination UI.nuqs for sharable deep links.@/components/table/config/data-table.Key files:
@/components/table/data-table/data-table.tsx — renders headers, rows, pinned columns, pagination, action bar.@/components/table/hooks/use-data-table.ts — owns table state, URL sync, debounced filters, manual modes.@/components/table/data-table/* — toolbar, filter list/menu, sort list, date/range/select filters, view options.@/components/table/lib/prisma-filter-columns.ts — maps filters to Prisma where conditions (example for Task).@/components/table/lib/data-table.ts — pinning CSS, operator helpers, valid filters guard.@/components/table/types/data-table.ts — shared types, column meta (label, variant, options, etc.).meta.variant, meta.options, meta.range, meta.icon, etc.actionBar API.import type { ColumnDef } from "@tanstack/react-table"
import { DataTableColumnHeader } from "@/components/table/data-table/data-table-column-header"
import type { Student } from "@prisma/client"
export function getStudentColumns(): ColumnDef<Student>[] {
return [
{ id: "select", /* checkbox column like tasks */ },
{
id: "name",
accessorKey: "name",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Name" />
),
meta: { label: "Name", placeholder: "Search names…", variant: "text" },
enableColumnFilter: true,
},
{
id: "grade",
accessorKey: "grade",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Grade" />
),
meta: {
label: "Grade",
variant: "multiSelect",
options: [
{ label: "Grade 1", value: "1" },
{ label: "Grade 2", value: "2" },
],
},
enableColumnFilter: true,
},
{
id: "createdAt",
accessorKey: "createdAt",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Enrolled" />
),
meta: { label: "Enrolled", variant: "dateRange" },
enableColumnFilter: true,
},
{ id: "actions", /* domain actions */ },
]
}Use the URL-parsed filters/sorts to build a DB query. For Prisma, use filterColumns or a generalized version per model.
import { filterColumns } from "@/components/table/lib/prisma-filter-columns"
export async function getStudents({ page, perPage, sorting, filters }: Params) {
const where = filterColumns({ filters, joinOperator: "and" }) // customize for Student model
const orderBy = sorting.map(s => ({ [s.id]: s.desc ? "desc" : "asc" }))
const [data, total] = await Promise.all([
prisma.student.findMany({
where,
orderBy,
skip: (page - 1) * perPage,
take: perPage,
}),
prisma.student.count({ where }),
])
return { data, pageCount: Math.ceil(total / perPage) }
}import { useDataTable } from "@/components/table/hooks/use-data-table"
import { DataTable } from "@/components/table/data-table/data-table"
export default function StudentsTable({ data, pageCount }: { data: Student[], pageCount: number }) {
const columns = React.useMemo(() => getStudentColumns(), [])
const { table, shallow, debounceMs, throttleMs } = useDataTable({
data,
columns,
pageCount,
initialState: { sorting: [{ id: "createdAt", desc: true }] },
getRowId: (row) => row.id,
clearOnDefault: true,
})
return (
<DataTable table={table}>
{/* choose Advanced or Basic toolbar */}
{/* <DataTableAdvancedToolbar table={table}>...filters...</DataTableAdvancedToolbar> */}
{/* <DataTableToolbar table={table}>...filters...</DataTableToolbar> */}
</DataTable>
)
}text, number, range, date, dateRange, boolean, select, multiSelect.config/data-table.ts (e.g., text: iLike, notILike, etc.).enableColumnFilter and meta.variant (and meta.options for select types).getValidFilters drops empty values to avoid noisy queries.orderBy.getCommonPinningStyles applies sticky + shadows.Advanced filter UX:
DataTableAdvancedToolbar for power users: combined sort list + filter list/menu.DataTableToolbar for simpler setups.useDataTable props.schoolId; unique constraints scoped by schoolId.perPage; paginate on server; consider virtualization for wide tables.schoolId, filter payloads for troubleshooting.This table block is designed for a shared, central database and logic layer while safely serving many schools (tenants).
schoolId. See /docs/database.@/components/table/lib/*.schoolId from subdomain/session and include it in every query.End-to-end flow:
/students?page=2&sort=name:asc).schoolId from subdomain (e.g., hogwarts.hogwarts.app).where/orderBy from URL and enforces { schoolId }.pageCount returned to the table; UI renders with consistent toolbar/filters.Example (Prisma, tenant-scoped):
export async function getStudents({ schoolId, page, perPage, sorting, filters }: Params) {
const whereBase = filterColumns({ filters, joinOperator: "and" })
const where = { ...whereBase, schoolId }
const orderBy = sorting.map((s) => ({ [s.id]: s.desc ? "desc" : "asc" }))
const [data, total] = await Promise.all([
prisma.student.findMany({ where, orderBy, skip: (page - 1) * perPage, take: perPage }),
prisma.student.count({ where }),
])
return { data, pageCount: Math.ceil(total / perPage) }
}How it serves all schools:
schoolId mapping; queries always include { schoolId }.Tie-ins with other docs:
/docs/architecture, /docs/pattern./docs/database (multi-tenant, indexes, constraints)./docs/add-school, /docs/arrangements./docs/internationalization.id/meta.schoolId from context.TablePage shell that wires: fetch → table → toolbar → action bar.This block is designed to be copied and configured per list with minimal code. Follow the pattern above, keep tenant scoping first, and reuse the shared operators/filters for a consistent UX across all tables.
On This Page
Generic Data Table (Reusable List Block)README (What this is)Current Progress (Working Now)Open Issues / NextHow to Use Everywhere (Pattern)Filtering & Sorting (Deep Dive)Production Readiness ChecklistMulti-tenant SaaS Integration (Central DB + Logic)Handle All Tables For All SchoolsExample Issues To File