|
| 1 | +--- |
| 2 | +title: "Nobody should hand-code a data table in 2026" |
| 3 | +description: "We have rebuilt the data-table-filters with a single schema, state management adapters, shadcn registry distribution, and an AI agent skill." |
| 4 | +author: "Maximilian Kaske" |
| 5 | +publishedAt: "2026-03-16" |
| 6 | +image: "/assets/posts/nobody-should-hand-code-a-data-table/nobody-should-hand-code-a-data-table.png" |
| 7 | +category: "engineering" |
| 8 | +--- |
| 9 | + |
| 10 | +The [data-table-filters](https://data-table.openstatus.dev?referrer=openstatus) project has been live for 1.5+ years. Filters, sorting, infinite scroll — the whole thing. It worked. People starred it. Bigger companies like [Supabase](https://supabase.com?referrer=openstatus) took inspiration for their logs dashboard. |
| 11 | + |
| 12 | +But adopting it in another project wasn't straightforward. You'd have to clone the repo, wire together multiple files just to add a column — columns in one file, filter config in another, sheet fields in a third, Zod schema somewhere else. Change one, forget the others, wonder why nothing works. |
| 13 | + |
| 14 | +> It was a good idea. It was a bad DX. |
| 15 | +
|
| 16 | +Then the **[shadcn](https://ui.shadcn.com?referrer=openstatus)** components registry and **agent skills** landed — and suddenly the pieces fit. We can ship code people own and an AI that knows how to set it up. No CLI installer needed. No npm package needed. And for AI coding tools, importing files via CLI keeps the context window lean — no need to overbloat it by copy-pasting entire repos worth of code. |
| 17 | + |
| 18 | +We rebuilt the entire developer experience — refactoring code, improving composability, and shipping modern distribution — without compromising the UI taste. Here's what we shipped. |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +<Image src="/assets/posts/nobody-should-hand-code-a-data-table/data-table-filters-homepage.png" alt="The data-table-filters homepage." /> |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## State management adapters |
| 27 | + |
| 28 | +Before, the table was married to [nuqs](https://nuqs.dev?referrer=openstatus) (URL search params). If you didn't want URL state, tough luck. |
| 29 | + |
| 30 | +We built a BYOS (Bring Your Own Store) architecture with three adapters that all implement the same interface: |
| 31 | + |
| 32 | +- **Memory** — state lives in React refs. No URL, no external store. The default when getting started. |
| 33 | +- **[nuqs](https://nuqs.dev?referrer=openstatus)** — state syncs to URL search params. Shareable links, back button works, SSR-friendly. The original behavior, now pluggable. |
| 34 | +- **[zustand](https://zustand.docs.pmnd.rs?referrer=openstatus)** — state lives in a zustand store via a `createFilterSlice()` helper. Drops into existing zustand setups. |
| 35 | + |
| 36 | +All three support pause/resume (for live-streaming mode) and work with React 18's `useSyncExternalStore`. Swapping adapters is one line — the table doesn't know or care which one you're using. |
| 37 | + |
| 38 | +Adding a new store to the mix is straightforward too. Just implement the `StoreAdapter<T>` interface and you're good to go. |
| 39 | + |
| 40 | +## Single source of truth: the table schema |
| 41 | + |
| 42 | +This was the big refactor. Before, defining a table meant keeping five separate files in sync: |
| 43 | + |
| 44 | +- `columns.tsx` — TanStack column definitions, cell renderers, sizing |
| 45 | +- `constants.tsx` — filter field configs, sheet field configs, UI properties |
| 46 | +- `schema.ts` — Zod validation for data rows AND a separate BYOS filter schema with serialization delimiters |
| 47 | +- `search-params.ts` — nuqs parser derived from the filter schema |
| 48 | +- `store.ts` — zustand slice derived from the filter schema |
| 49 | + |
| 50 | +Adding a column? Edit all five. Renaming a field? All five. Changing a filter type from checkbox to slider? Update the schema, the constants, and the columns file — and hope you didn't miss the filterFn. |
| 51 | + |
| 52 | +The solution: define a column once, derive everything else. |
| 53 | + |
| 54 | +```tsx |
| 55 | +export const tableSchema = createTableSchema({ |
| 56 | + level: col |
| 57 | + .enum(LEVELS) |
| 58 | + .label("Level") |
| 59 | + .filterable("checkbox", { |
| 60 | + options: LEVELS.map((l) => ({ label: l, value: l })), |
| 61 | + }) |
| 62 | + .defaultOpen() |
| 63 | + .size(27), |
| 64 | + |
| 65 | + date: col |
| 66 | + .timestamp() |
| 67 | + .label("Date") |
| 68 | + .display("timestamp") |
| 69 | + .sortable() |
| 70 | + .defaultOpen(), |
| 71 | + |
| 72 | + latency: col |
| 73 | + .number() |
| 74 | + .label("Latency") |
| 75 | + .display("number", { unit: "ms" }) |
| 76 | + .filterable("slider"), |
| 77 | +}); |
| 78 | +``` |
| 79 | + |
| 80 | +One definition. Four generators derive columns, filter fields, filter schema, and sheet fields automatically. Five files collapsed into one `table-schema.tsx`. |
| 81 | + |
| 82 | +This became the foundation for everything that followed — the builder, the Drizzle integration, the registry all depend on it. |
| 83 | + |
| 84 | +It was also only possible to build because of all the extra work done before. It's fine to copy-paste things and not extract too early — until you see the pattern and can actually optimize for it. |
| 85 | + |
| 86 | +## The schema builder |
| 87 | + |
| 88 | +If the schema can be generated from a definition, why not generate it from raw data? |
| 89 | + |
| 90 | +A [builder](https://data-table.openstatus.dev/builder?referrer=openstatus) where you paste JSON (or upload a CSV) and instantly get a working, filterable table. No column definitions. No config. Just data in, table out. |
| 91 | + |
| 92 | +<Image src="/assets/posts/nobody-should-hand-code-a-data-table/builder-data-table.png" alt="The schema builder: JSON on the left, live filterable table on the right." /> |
| 93 | + |
| 94 | +The components aren't fully customizable in the builder (serialization constraints), but it helps you get started and understand how schema changes affect the data table. |
| 95 | + |
| 96 | +The inference engine detects types and applies domain heuristics — keys with "latency" get millisecond units, "id" columns get monospace display, trace IDs are auto-hidden. We ship presets for common patterns like timestamps, durations, and log levels. Contributions welcome — the more opinionated, well-built presets we have, the better the out-of-the-box experience gets for specific use cases. |
| 97 | + |
| 98 | +The preview uses the same infinite-scroll architecture as the real thing — what you see in the builder is exactly what you'll ship. |
| 99 | + |
| 100 | +## Drizzle ORM: a real database with real data |
| 101 | + |
| 102 | +A client-side demo only takes you so far. People need to see this working with a real database, real data, growing over time. |
| 103 | + |
| 104 | +We built a full [Drizzle ORM](https://orm.drizzle.team?referrer=openstatus) integration connected to a [Supabase](https://supabase.com?referrer=openstatus) PostgreSQL database. |
| 105 | + |
| 106 | +A [Vercel](https://vercel.com?referrer=openstatus) cron job runs every 10 minutes, generating realistic HTTP request logs — randomized timing metrics, status codes, multiple regions with latency multipliers. The data accumulates over time, so the demo always has fresh, realistic data to filter through — and it supports live mode (just time it right, the cron runs every 10 minutes). |
| 107 | + |
| 108 | +The [`/drizzle`](https://data-table.openstatus.dev/drizzle?referrer=openstatus) route shows it all working together: infinite scroll with cursor-based pagination, faceted search with live mode, nuqs URL state so filters are shareable, and time-bucketed charts via PostgreSQL's `date_bin()`. |
| 109 | + |
| 110 | +<Image src="/assets/posts/nobody-should-hand-code-a-data-table/drizzle-data-table.png" alt="The /drizzle route with live data, faceted filters, and time-bucketed charts." /> |
| 111 | + |
| 112 | +This is the kind of example that actually helps people adopt a library. Not "here's a static demo" — here's a production-like setup with a real database, real data pipeline, and all the pieces wired together. |
| 113 | + |
| 114 | +## Tests. Lots of tests. |
| 115 | + |
| 116 | +> Writing good tests is cheap nowadays. Just do it. Thank me later. |
| 117 | +
|
| 118 | +39 test files covering the table schema, store adapters, builder, Drizzle ORM (including a dedicated SQL injection suite), and utilities. Everything from column builder validation to `'; DROP TABLE logs; --`. |
| 119 | + |
| 120 | +CI runs against a real PostgreSQL container — migrations, seed data, then tests. No mocks for the database layer. We want this production-ready, so making sure test coverage is solid and tests are green is non-negotiable. |
| 121 | + |
| 122 | +## Distribution killer: shadcn registry + agent skill |
| 123 | + |
| 124 | +A GitHub issue ([#39](https://github.com/openstatushq/data-table-filters/issues/39?referrer=openstatus)) put it plainly: "Can you please provide it as a package, so that it could easily be installed and managed?" Another ([#14](https://github.com/openstatushq/data-table-filters/issues/14?referrer=openstatus)) asked for a Vite example, which would've meant restructuring into a monorepo. |
| 125 | + |
| 126 | +The **[shadcn registry](https://ui.shadcn.com/docs/registry?referrer=openstatus)** solves the distribution problem. Adopting it required migrating to Tailwind v4 first (which was long overdue!) — the registry blocks declare CSS variables using `@theme inline` syntax, so v4 was a prerequisite. Then we created a `registry.json` with 9 installable blocks: |
| 127 | + |
| 128 | +```bash |
| 129 | +npx shadcn@latest add https://data-table.openstatus.dev/r/data-table.json |
| 130 | +``` |
| 131 | + |
| 132 | +One command. Dependencies resolved. CSS variables injected. Path aliases rewritten. Install just the core, or add Drizzle helpers, command palette, zustand adapter — whatever you need. |
| 133 | + |
| 134 | +Then there's the **agent skill**. |
| 135 | + |
| 136 | +Think about what a CLI installer does: it asks you questions ("TypeScript?", "Which state manager?", "Do you use Drizzle?"), then generates files based on your answers. It's a decision tree pretending to be a conversation. |
| 137 | + |
| 138 | +An agent skill is an actual conversation. Install it with: |
| 139 | + |
| 140 | +```bash |
| 141 | +npx skills add https://github.com/openstatushq/data-table-filters --skill data-table-filters |
| 142 | +``` |
| 143 | + |
| 144 | +When someone opens their AI coding tool and says "add a filterable data table", the skill activates. |
| 145 | + |
| 146 | +<Image src="/assets/posts/nobody-should-hand-code-a-data-table/agent-data-table.png" alt="asking 'add a data-table example' - no extra guidance (after setting up the skill)." /> |
| 147 | + |
| 148 | +It understands the project, installs the right blocks, generates a schema from the data model, wires up the state adapter, and configures the database integration — if you ask for it. Start minimal and expand to your use case. |
| 149 | + |
| 150 | +We also rewrote all the [docs](https://data-table.openstatus.dev/docs?referrer=openstatus) from scratch. Not glamorous work — but when an AI reads your docs to install your library, doc quality directly affects agent quality. **Better docs, better agent.** |
| 151 | + |
| 152 | +## Where this is going |
| 153 | + |
| 154 | +We keep improving data-table-filters to make it the fastest way to spin up large, production-ready dataset views for logs and beyond. |
| 155 | + |
| 156 | +> It's not a library. It's a playbook. |
| 157 | +
|
| 158 | +The combination of shadcn registries and agent skills is a really good distribution model for frontend libraries. You don't publish a package with a fixed API. You ship composable code blocks, matching your design system, that an AI understands how to assemble. |
| 159 | + |
| 160 | +Not "install this package and read the docs." More like "tell your AI what you need and it builds it from well-structured, composable pieces." |
| 161 | + |
| 162 | +> Nobody should hand-code a data table anymore. |
| 163 | +
|
| 164 | +The full project is open source at [data-table-filters](https://github.com/openstatushq/data-table-filters?referrer=openstatus). Try the builder at [data-table.openstatus.dev/builder](https://data-table.openstatus.dev/builder?referrer=openstatus). Or just open your favorite chat interface, install the agent skill, and say "add a filterable data table" — the skill will take it from there. |
0 commit comments