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?
| Answer | Strategy | Use When |
|---|---|---|
| No | A: Simple Fetching (Default) | Settings, admin CRUD, most entity tables |
| Yes | B: SWR Fetching | Dashboards, multi-user editing, live monitoring |
Strategy A: Simple Fetching (Recommended for CRUD Tables)
- Use
useStatefor local data management - Update state from server mutation responses
- No automatic revalidation
- Lower complexity, no SWR dependency
- See: nextjs/references/simple-fetching-pattern.md
Strategy B: SWR Fetching (Requires Justification)
- Use
useSWRwith documented justification - Configure appropriate revalidation triggers
- For dashboards and multi-user scenarios
- See: nextjs/references/swr-fetching-pattern.md
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
- Gather requirements: Entity name, fields, API endpoints, actions needed
- Generate files in order: types → API routes → page → table → columns → context → actions
- 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 mutationActionsProviderwrapper
5. Table Body + Columns
See references/columns-pattern.md for:
- Column definitions with
updatingIdsloading 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:
useStatewith initialData - Strategy B:
useSWRwith fallbackData + justification comment
- Strategy A:
- 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)
