Skip to content
nextjs-data-table-page logo

Next.js Data Table Page Generator

nextjs-data-table-page

Create Next.js data table pages with SSR initial load, SWR caching, and server-response-based UI updates. Use when asked to create a new data table page, entity management page, CRUD table, or admin list view. Generates page.tsx (SSR), table components, columns, context, actions, and API routes f...

SKILL.md

Full skill instructions

Next.js Data Table Page Generator

Create production-ready data table pages with:

  • SSR initial data loading for fast first paint
  • Flexible data fetching - Simple state or SWR based on needs
  • URL-based state for filters/​pagination via nuqs
  • Centralized data-table from @/​components/​data-table
  • Type-safe columns with TanStack Table
  • Context-based actions for CRUD operations

Data Fetching Strategy

Before generating, determine the appropriate strategy:

Decision Question

Does this table's data change without user action?

AnswerStrategyUse When
NoA: Simple Fetching (Default)Settings, admin CRUD, most entity tables
YesB: SWR FetchingDashboards, multi-user editing, live monitoring

Strategy A: Simple Fetching (Recommended for CRUD Tables)

Strategy B: SWR Fetching (Requires Justification)

Decision framework: nextjs/​references/​data-fetching-strategy.md

Architecture Overview

app/(pages)/[section]/[entity]/
├── page.tsx                          # SSR entry point
├── context/
│   └── [entity]-actions-context.tsx  # Actions provider
└── _components/
    ├── table/
    │   ├── [entity]-table.tsx        # Main client wrapper (SWR)
    │   ├── [entity]-table-body.tsx   # DataTable + columns
    │   ├── [entity]-table-columns.tsx# Column definitions
    │   ├── [entity]-table-controller.tsx # Toolbar/​bulk actions
    │   └── [entity]-table-actions.tsx # Bulk action hooks
    ├── actions/
    │   ├── add-[entity]-button.tsx   # Add button + sheet
    │   └── actions-menu.tsx          # Row action menu
    ├── modal/
    │   ├── add-[entity]-sheet.tsx    # Create form
    │   ├── edit-[entity]-sheet.tsx   # Edit form
    │   └── view-[entity]-sheet.tsx   # View details
    └── sidebar/
        └── status-panel.tsx          # Stats sidebar

app/​api/[section]/[entity]/
├── route.ts                          # GET (list) + POST (create)
├── [entityId]/
│   ├── route.ts                      # PUT (update)
│   └── status/​route.ts               # PUT (status toggle)
└── status/​route.ts                   # POST (bulk status)

Quick Start

  1. Gather requirements: Entity name, fields, API endpoints, actions needed
  2. Generate files in order: types → API routes → page → table → columns → context → actions
  3. Verify data-table component exists at @/​components/​data-table

File Generation Order

1. Types (if not existing)

Define response types in @/​lib/​types/​api/[entity].ts. See references/​types-pattern.md.

2. API Routes

Create Next.js API routes that proxy to backend. See references/​api-routes-pattern.md.

3. SSR Page (page.tsx)

import { auth } from "@/​lib/​auth/​server-auth";
import { get[Entity]s } from "@/​lib/​actions/[entity].actions";
import { redirect } from "next/​navigation";
import [Entity]Table from "./​_components/​table/[entity]-table";

export default async function [Entity]Page({
  searchParams,
}: {
  searchParams: Promise<{
    is_active?: string;
    search?: string;
    page?: string;
    limit?: string;
  }>;
}) {
  const session = await auth();
  if (!session?.accessToken) redirect("/​login");

  const params = await searchParams;
  const pageNumber = Number(params.page) || 1;
  const limitNumber = Number(params.limit) || 10;
  const skip = (pageNumber - 1) * limitNumber;

  const data = await get[Entity]s(limitNumber, skip, {
    is_active: params.is_active,
    search: params.search,
  });

  return <[Entity]Table initialData={data} />;
}

4. Main Table Component

See references/​table-component-pattern.md for the full pattern with:

  • SWR setup with fallbackData: initialData
  • Action handlers that return server responses
  • updateItems() function for cache mutation
  • ActionsProvider wrapper

5. Table Body + Columns

See references/​columns-pattern.md for:

  • Column definitions with updatingIds loading states
  • Selection column with checkbox
  • Status toggle with confirmation
  • Actions column placeholder

6. Context + Actions

See references/​context-pattern.md for:

  • Context provider setup
  • Bulk action hooks
  • Error handling patterns

7. Edit/​Add Sheets

See references/​edit-sheet-pattern.md for:

  • Fetch-then-open pattern (load complete entity before opening sheet)
  • Row actions with loading states
  • Edit sheet component structure
  • Form handling with server response updates

Core Principles

Server Response Updates (NOT Optimistic)

Both strategies use server responses to update the UI:

// ✅ CORRECT: Use server response
const { data: updated } = await fetchClient.put(`/​api/​entity/​${id}`, body);
updateItems([updated]); // Update local state with server data

// ❌ WRONG: Optimistic update
const optimistic = { ...current, ...changes };
setData({ items: [...items.filter(i => i.id !== id), optimistic] });

Strategy A: Simple State Management (Default)

// No SWR - use React state
const [data, setData] = useState<Response>(initialData);

const updateItems = (serverResponse: Item[]) => {
  setData(current => {
    const responseMap = new Map(serverResponse.map(i => [i.id, i]));
    return {
      ...current,
      items: current.items.map(item =>
        responseMap.has(item.id) ? responseMap.get(item.id)! : item
      ),
    };
  });
};

Strategy B: SWR Configuration (When Justified)

/​**
 * SWR JUSTIFICATION:
 * - Reason: [Document why revalidation is needed]
 * - Trigger: [Interval / Focus / Manual]
 */
const { data, mutate } = useSWR<Response>(apiUrl, fetcher, {
  fallbackData: initialData ?? undefined,
  keepPreviousData: true,
  revalidateOnMount: false,
  revalidateIfStale: true,
  revalidateOnFocus: false,  // Set true only if justified
  revalidateOnReconnect: false,
});

URL-Based State

// Use nuqs for URL params (auto-triggers SWR refetch)
const [page, setPage] = useQueryState("page", parseAsInteger.withDefault(1));
const [filter] = useQueryState("filter");

// Build API URL from params
const params = new URLSearchParams();
params.append("skip", ((page - 1) * limit).toString());
if (filter) params.append("search", filter);
const apiUrl = `/​api/​entity?${params.toString()}`;

Cache Update Pattern

const updateItems = async (serverResponse: EntityResponse[]) => {
  const currentData = data;
  if (!currentData) return;

  const responseMap = new Map(serverResponse.map(item => [item.id, item]));
  const updatedList = currentData.items.map(item =>
    responseMap.has(item.id) ? responseMap.get(item.id)! : item
  );

  await mutate(
    { ...currentData, items: updatedList },
    { revalidate: false }
  );
};

Required Dependencies

Ensure project has:

  • nuqs - URL state management
  • @tanstack/​react-table - Table primitives
  • @/​components/​data-table - Reusable table components (must exist)
  • swr - Only if using Strategy B (SWR fetching)

Checklist

  • Data fetching strategy selected (A or B)
  • Types defined for entity and response
  • API routes created (GET, POST, PUT, bulk status)
  • SSR page fetches initial data
  • Main table uses selected strategy:
    • Strategy A: useState with initialData
    • Strategy B: useSWR with fallbackData + justification comment
  • Actions return server response (not optimistic)
  • Columns show loading state via updatingIds
  • Context provides actions to children
  • Bulk actions use hook pattern
  • URL params drive filtering/​pagination
  • Edit sheets use fetch-then-open pattern (not row.original)