Claude-skill-registry 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 following a proven architecture with centralized reusable data-table component.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/data-table" ~/.claude/skills/majiayu000-claude-skill-registry-nextjs-data-table-page && rm -rf "$T"
manifest: skills/data/data-table/SKILL.md
source content

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)