Advanced Patterns

Global Configuration

Wrap your application with TableProvider in app/providers.tsx to set global defaults for all table instances. Any instance-level config will override these defaults.

tsx
// app/providers.tsx
import { TableProvider } from "@/components/table";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <TableProvider
      config={{
        pagination: {
          defaultPageSize: 25,
          pageSizeOptions: [10, 25, 50, 100],
        },
        features: {
          search: true,
          filters: true,
          columnVisibility: true,
          csvExport: true,
          cardView: false,
        },
        i18n: {
          locale: "en",
          overrides: {
            "pagination.of": "out of",
            "table.noResults": "Nothing to show here",
          },
        },
      }}
    >
      {children}
    </TableProvider>
  );
}

Per-Instance Overrides

Pass a config prop directly to ClientSideTable to override any global defaults for that specific instance.

tsx
// components/user-table.tsx
import { ClientSideTable } from "@/components/table";
import { columns } from "./columns";
import { data } from "./data";

export function UserTable() {
  return (
    <ClientSideTable
      columns={columns}
      data={data}
      config={{
        features: {
          csvExport: false,
          cardView: true,
        },
        pagination: {
          defaultPageSize: 10,
        },
      }}
    />
  );
}

Dynamic Column Generation

Generate columns programmatically from a schema definition using a buildColumns utility. This is useful when your column structure is determined at runtime or driven by an API response.

tsx
// lib/build-columns.ts
import { ColumnDef } from "@tanstack/react-table";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";

export interface FieldSchema {
  key: string;
  label: string;
  type: "string" | "date" | "badge";
  sortable?: boolean;
  hideable?: boolean;
}

export function buildColumns<T>(fields: FieldSchema[]): ColumnDef<T>[] {
  return fields.map((field) => ({
    accessorKey: field.key,
    header: field.label,
    cell: ({ getValue }) => {
      const value = getValue();

      if (field.type === "date") {
        return value ? format(new Date(value as string), "MMM d, yyyy") : "—";
      }

      if (field.type === "badge") {
        return value ? <Badge variant="outline">{String(value)}</Badge> : null;
      }

      return value != null ? String(value) : "—";
    },
    enableSorting: field.sortable ?? true,
    enableHiding: field.hideable ?? true,
  }));
}

Feature Toggling at Runtime

Use useState and useMemo to toggle table features dynamically based on user interactions or application state.

tsx
// components/toggleable-table.tsx
"use client";

import { useState, useMemo } from "react";
import { ClientSideTable } from "@/components/table";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { columns } from "./columns";
import { data } from "./data";

export function ToggleableTable() {
  const [filtersEnabled, setFiltersEnabled] = useState(true);
  const [exportEnabled, setExportEnabled] = useState(false);

  const config = useMemo(
    () => ({
      features: {
        filters: filtersEnabled,
        csvExport: exportEnabled,
      },
    }),
    [filtersEnabled, exportEnabled]
  );

  return (
    <div className="space-y-4">
      <div className="flex items-center gap-6">
        <div className="flex items-center gap-2">
          <Switch
            id="filters-toggle"
            checked={filtersEnabled}
            onCheckedChange={setFiltersEnabled}
          />
          <Label htmlFor="filters-toggle">Enable Filters</Label>
        </div>
        <div className="flex items-center gap-2">
          <Switch
            id="export-toggle"
            checked={exportEnabled}
            onCheckedChange={setExportEnabled}
          />
          <Label htmlFor="export-toggle">Enable CSV Export</Label>
        </div>
      </div>
      <ClientSideTable columns={columns} data={data} config={config} />
    </div>
  );
}

Multi-Table Setup

Multiple table instances can coexist on the same page. Wrap them in a shared TableProvider for global defaults, then override per-instance as needed.

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

import { TableProvider, ClientSideTable } from "@/components/table";
import { userColumns } from "./user-columns";
import { orderColumns } from "./order-columns";
import { users, orders } from "./data";

export default function DashboardPage() {
  return (
    <TableProvider
      config={{
        features: {
          search: true,
          filters: true,
          csvExport: false,
        },
      }}
    >
      <section className="space-y-2">
        <h2 className="text-xl font-semibold">Users</h2>
        <ClientSideTable columns={userColumns} data={users} />
      </section>

      <section className="space-y-2 mt-8">
        <h2 className="text-xl font-semibold">Orders</h2>
        {/* This instance overrides the global csvExport: false */}
        <ClientSideTable
          columns={orderColumns}
          data={orders}
          config={{
            features: {
              csvExport: true,
            },
          }}
        />
      </section>
    </TableProvider>
  );
}

Config-Driven Table Generation

Define reusable table configurations as named constants using createTableConfig. Import and apply them wherever a consistent preset is needed across your application.

tsx
// lib/table-configs.ts
import { createTableConfig } from "@/components/table";

export const ADMIN_TABLE_CONFIG = createTableConfig({
  features: {
    search: true,
    filters: true,
    columnVisibility: true,
    csvExport: true,
    cardView: true,
  },
  pagination: {
    defaultPageSize: 50,
    pageSizeOptions: [25, 50, 100],
  },
});

export const READONLY_TABLE_CONFIG = createTableConfig({
  features: {
    search: true,
    filters: false,
    columnVisibility: false,
    csvExport: false,
    cardView: false,
  },
  pagination: {
    defaultPageSize: 20,
  },
});

export const MINIMAL_TABLE_CONFIG = createTableConfig({
  features: {
    search: false,
    filters: false,
    columnVisibility: false,
    csvExport: false,
    cardView: false,
  },
  pagination: {
    defaultPageSize: 10,
    pageSizeOptions: [10],
  },
});

// Usage
// <ClientSideTable columns={columns} data={data} config={ADMIN_TABLE_CONFIG} />
// <ClientSideTable columns={columns} data={data} config={READONLY_TABLE_CONFIG} />

Plugins

Plugins allow you to extend table behavior with reusable, composable logic. Pass plugins as an array to the table instance to hook into lifecycle events and override rendering.

tsx
// lib/table-plugins.ts
import { TablePlugin } from "@/components/table";

// Logs sort, filter, and page change events to the console
export const loggingPlugin: TablePlugin = {
  name: "logging",
  onSortChange: (sorting) => {
    console.log("[Table] Sort changed:", sorting);
  },
  onFilterChange: (filters) => {
    console.log("[Table] Filters changed:", filters);
  },
  onPageChange: (page) => {
    console.log("[Table] Page changed:", page);
  },
};

// Replaces the default pagination with a compact single-row layout
export const compactPaginationPlugin: TablePlugin = {
  name: "compact-pagination",
  renderPagination: ({ page, pageCount, onPageChange }) => (
    <div className="flex items-center gap-2 text-sm">
      <button
        onClick={() => onPageChange(page - 1)}
        disabled={page <= 1}
        className="px-2 py-1 border rounded disabled:opacity-40"
      >
        Prev
      </button>
      <span>
        {page} / {pageCount}
      </span>
      <button
        onClick={() => onPageChange(page + 1)}
        disabled={page >= pageCount}
        className="px-2 py-1 border rounded disabled:opacity-40"
      >
        Next
      </button>
    </div>
  ),
};

// Usage
// <ClientSideTable
//   columns={columns}
//   data={data}
//   plugins={[loggingPlugin, compactPaginationPlugin]}
// />