DEV Community

Cover image for ๐Ÿš€ I Built an Enterprise-Grade Data Table for Next.js - Now Available via Shadcn Registry!
Jackson Kasi
Jackson Kasi Subscriber

Posted on

๐Ÿš€ I Built an Enterprise-Grade Data Table for Next.js - Now Available via Shadcn Registry!

TL;DR (Too Long; Didn't Read) ๐Ÿ‘€

After months of building the same data table features over and over, I created the ultimate data table component that handles everything from server-side operations to Excel exports. Today, it's available with one-click installation via Shadcn registry! Plus, sub-row support is coming soon with 3 powerful modes.

npx shadcn@latest add https://tnks-data-table.vercel.app/r/data-table.json
Enter fullscreen mode Exit fullscreen mode

The Problem Every Developer Faces ๐Ÿ˜ค

Picture this: Your PM walks in and says, "We need a data table for our admin panel. Should be simple, right?"

Two weeks later:

  • "Can we add sorting?" โœ…
  • "We need server-side pagination" โœ…
  • "Users want to export to Excel" โœ…
  • "Can we add date range filters?" โœ…
  • "We need bulk operations" โœ…
  • "Oh, and make it work with our snake_case API" โœ…
  • "Row selection across pages would be nice" โœ…
  • "URL state persistence for sharing filters?" โœ…

Sound familiar? I've built this same "simple table" at least 20 times. So I decided to build it once more - but this time, build it right.


Introducing TNKS Data Table ๐ŸŽ‰

A production-ready data table that actually handles real-world requirements. Built on TanStack Table v8, fully typed with TypeScript, and now installable in seconds via Shadcn registry.

๐ŸŽฏ What Makes It Different?

This isn't just another table component. It's a complete data management solution:

<DataTable<User, any>
  getColumns={getColumns}
  exportConfig={useExportConfig()}
  fetchDataFn={useUsersData}
  fetchByIdsFn={fetchUsersByIds}
  idField="id"
  onRowClick={(user, rowIndex) => router.push(`/users/${user.id}`)}
  renderToolbarContent={({ selectedRows, totalSelectedCount }) => (
    <ToolbarActions 
      selectedCount={totalSelectedCount}
      onBulkDelete={() => handleBulkDelete(selectedRows)}
    />
  )}
  config={{
    enableRowSelection: true,
    enableSearch: true,
    enableDateFilter: true,
    enableColumnVisibility: true,
    enableUrlState: true,
    defaultSortBy: "created_at",
    searchPlaceholder: "Search by name, email, or ID...",
  }}
/>
Enter fullscreen mode Exit fullscreen mode

That's it. This single component handles EVERYTHING.


๐Ÿ”ฅ Features That Will Save Your Sanity

1. One-Command Installation

Forget copying 20 files and installing 15 dependencies:

# Install Shadcn UI components (if you haven't)
npx shadcn@latest init
npx shadcn@latest add button checkbox input select table # ...etc

# Install the complete data-table with ALL features
npx shadcn@latest add https://tnks-data-table.vercel.app/r/data-table.json

# Also grab the date picker
npx shadcn@latest add https://tnks-data-table.vercel.app/r/calendar-date-picker.json
Enter fullscreen mode Exit fullscreen mode

Done. You now have a production-ready data table.

2. Server-Side Everything

No more client-side filtering of 10,000 rows:

// Your API gets clean, typed parameters
{
  search: "john",
  from_date: "2024-01-01",
  to_date: "2024-12-31",
  sort_by: "created_at",  // or sortBy for camelCase APIs
  sort_order: "desc",
  page: 1,
  limit: 20
}
Enter fullscreen mode Exit fullscreen mode

3. Smart Row Selection Across Pages

Users can select rows across multiple pages. The table tracks both visible selections AND all selected IDs:

renderToolbarContent={({ 
  selectedRows,        // Currently visible selected rows
  allSelectedIds,      // ALL selected IDs across pages
  totalSelectedCount,  // Total count
  resetSelection      // Clear function
}) => (
  <Button onClick={() => deleteUsers(allSelectedIds)}>
    Delete {totalSelectedCount} users
  </Button>
)}
Enter fullscreen mode Exit fullscreen mode

4. Export with Data Transformation

Export to CSV/Excel with calculated columns that don't even exist in your table:

const transformFunction = (user) => ({
  ...user,
  // Format existing data
  created_at: formatDate(user.created_at),
  total_spent: formatCurrency(user.total_spent),

  // ADD COMPLETELY NEW COLUMNS for export only!
  account_age_days: daysSince(user.created_at),
  customer_tier: user.total_spent > 1000 ? "Premium" : "Standard",
  risk_score: calculateRiskScore(user),
});
Enter fullscreen mode Exit fullscreen mode

5. Works with ANY API Case Format

Your API uses snake_case? camelCase? PascalCase? Doesn't matter.

// Snake case API? Just use it directly!
columns = [
  { accessorKey: "user_name", header: "Name" },
  { accessorKey: "created_at", header: "Created" },
]

// Camel case API? Same thing!
columns = [
  { accessorKey: "userName", header: "Name" },
  { accessorKey: "createdAt", header: "Created" },
]
Enter fullscreen mode Exit fullscreen mode

No conversion layers. No transformers. It just works.

6. URL State Persistence

Share filtered views with a simple URL:

/users?search=john&sort_by=created_at&sort_order=desc&page=2
Enter fullscreen mode Exit fullscreen mode

Your users can bookmark filtered views, share them in Slack, whatever they want.

7. Built-in Row Click Navigation

Make entire rows clickable without conflicting with buttons or checkboxes:

onRowClick={(user, rowIndex) => {
  router.push(`/users/${user.id}`);
}}
Enter fullscreen mode Exit fullscreen mode

The table automatically prevents clicks on interactive elements from triggering row navigation. Smart!


๐ŸŽญ Coming Soon: Sub-Row Support (This is HUGE!)

We're adding three powerful sub-row modes that will handle complex hierarchical data:

Mode 1: Same Columns (Orders โ†’ Products)

โ–ผ ORD-001  โ”‚ John Doe    โ”‚ Laptop      โ”‚ 1 qty  โ”‚ $999.99
   โ””โ”€ ORD-001 โ”‚ John Doe โ”‚ Mouse       โ”‚ 2 qty  โ”‚ $59.98
   โ””โ”€ ORD-001 โ”‚ John Doe โ”‚ Keyboard    โ”‚ 1 qty  โ”‚ $79.99
Enter fullscreen mode Exit fullscreen mode

Mode 2: Custom Columns (Logistics โ†’ Stops)

โ–ผ Booking B-001 โ”‚ NYC โ†’ Boston โ”‚ 4 stops โ”‚ $450
  โ”œโ”€ ๐Ÿ“ Pickup  โ”‚ 123 Main St, NYC    โ”‚ 9:00 AM
  โ”œโ”€ ๐Ÿ“ Pickup  โ”‚ 456 Oak Ave, NYC    โ”‚ 10:30 AM
  โ”œโ”€ ๐Ÿ“ฆ Delivery โ”‚ 789 Elm St, Boston  โ”‚ 4:00 PM
  โ””โ”€ ๐Ÿ“ฆ Delivery โ”‚ 321 Pine Rd, Boston โ”‚ 5:30 PM
Enter fullscreen mode Exit fullscreen mode

Mode 3: Custom Component (Support Tickets โ†’ Timeline)

โ–ผ TKT-001 โ”‚ Cannot login โ”‚ High Priority โ”‚ Open
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚  ๐Ÿ‘ค John: Can't login, getting errors   โ”‚
  โ”‚  ๐Ÿ‘จโ€๐Ÿ’ผ Sarah: Looking into this now...    โ”‚
  โ”‚  ๐Ÿค– Status changed to "In Progress"     โ”‚
  โ”‚  ๐Ÿ‘จโ€๐Ÿ’ผ Sarah: Fixed! Try again.           โ”‚
  โ”‚  [Reply box] [Attach] [Close ticket]   โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ–ผ TKT-002 โ”‚ Cannot login โ”‚ High Priority โ”‚ Open
etc...
Enter fullscreen mode Exit fullscreen mode

This will make complex data relationships actually manageable!


๐Ÿ’ป Real-World Example

Here's a complete working example you can copy-paste:

// app/users/page.tsx
"use client";

import { DataTable } from "@/components/data-table/data-table";
import { useRouter } from "next/navigation";

export default function UsersPage() {
  const router = useRouter();

  // Column definitions
  const columns = [
    {
      id: "select",
      header: ({ table }) => (
        <Checkbox
          checked={table.getIsAllPageRowsSelected()}
          onCheckedChange={(value) => 
            table.toggleAllPageRowsSelected(!!value)
          }
        />
      ),
      cell: ({ row }) => (
        <Checkbox
          checked={row.getIsSelected()}
          onCheckedChange={(value) => row.toggleSelected(!!value)}
        />
      ),
    },
    {
      accessorKey: "name",
      header: ({ column }) => (
        <DataTableColumnHeader column={column} title="Name" />
      ),
    },
    {
      accessorKey: "email",
      header: "Email",
    },
    {
      accessorKey: "created_at",
      header: "Joined",
      cell: ({ row }) => formatDate(row.getValue("created_at")),
    },
    {
      id: "actions",
      cell: ({ row }) => <RowActions user={row.original} />,
    },
  ];

  // Data fetching hook
  const useUsersData = (page, limit, search, dateRange, sortBy, sortOrder) => {
    return useQuery({
      queryKey: ["users", page, limit, search, dateRange, sortBy, sortOrder],
      queryFn: () => fetchUsers({ page, limit, search, ...dateRange, sortBy, sortOrder }),
    });
  };

  return (
    <DataTable
      getColumns={() => columns}
      fetchDataFn={useUsersData}
      idField="id"
      onRowClick={(user) => router.push(`/users/${user.id}`)}
      config={{
        enableRowSelection: true,
        enableSearch: true,
        enableDateFilter: true,
        enableUrlState: true,
      }}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ—๏ธ Built with Modern Stack

  • Next.js 15 with App Router
  • TanStack Table v8 for table logic
  • TanStack Query for data fetching
  • Zod for runtime validation
  • TypeScript for type safety
  • Tailwind CSS for styling
  • Shadcn UI for base components

๐Ÿ“Š Performance Metrics

In production, this table handles:

  • 50,000+ rows with server-side pagination
  • 100+ concurrent users with optimistic updates
  • Sub-50ms response times for sort/filter operations
  • 2MB+ Excel exports without freezing the UI

๐ŸŽ Why I'm Sharing This

I've spent countless hours building and rebuilding data tables. Every project needed the same features, but slightly different. This component is my gift to developers who are tired of reinventing the wheel.

It's MIT licensed, production-tested, and actively maintained.


๐Ÿš€ Get Started in 2 Minutes

# 1. Install it
npx shadcn@latest add https://tnks-data-table.vercel.app/r/data-table.json

# 2. Create your table
import { DataTable } from "@/components/data-table/data-table";

# 3. Ship to production
npm run build
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“š Resources


๐Ÿค Contributing

Found a bug? Have a feature request? PRs are welcome! This is a community project, and I'd love your input.

Special shoutout to the Shadcn UI team for creating an amazing component system that makes sharing like this possible.


๐Ÿ’ญ Final Thoughts

Building this table taught me that the "simple" features are often the hardest to get right. URL state persistence? Took 3 refactors. Row selection across pages? 5 iterations. Snake_case/camelCase support? Don't ask. ๐Ÿ˜…

But now it's done, tested, and ready for you to use. No more building the same table features over and over.

Your turn: What's the most complex data table requirement you've had to implement? Drop a comment below!๐Ÿ‘‡


If this saved you some time, give it a โญ on GitHub or share it with your team. Happy coding! ๐Ÿš€

Top comments (2)

Collapse
 
jacksonkasi profile image
Jackson Kasi

Soon ๐Ÿคฉ

Some comments may only be visible to logged-in visitors. Sign in to view all comments.