Build admin/CRUD data table pages with server-side fetching, SWR caching, and optimistic mutations. Use when creating table pages, implementing CRUD operations, handling data mutations, or working with pagination, filters, and bulk operations. Covers the complete pattern from API to UI.
This skill teaches how to build production-grade data table pages following the established patterns in this codebase. The architecture combines:
CRITICAL: Always follow existing patterns. Do not introduce new state management libraries or data fetching approaches.
Activate when request involves:
app/(pages)/{feature}/
├── page.tsx # Server component - data fetching
├── _components/
│ ├── table/
│ │ ├── {feature}-table.tsx # SWR setup + context provider
│ │ ├── {feature}-table-body.tsx # DataTable + toolbars
│ │ ├── {feature}-table-columns.tsx # Column definitions
│ │ ├── {feature}-table-header.tsx # Search, export, print
│ │ └── {feature}-table-controller.tsx # Bulk actions
│ ├── actions/
│ │ ├── actions-menu.tsx # Row action buttons
│ │ └── add-{item}-button.tsx # Add button
│ ├── filters/ # Filter components
│ ├── sidebar/
│ │ └── status-panel.tsx # Summary + filters sidebar
│ └── modal/
│ ├── add-{item}-sheet.tsx # Create form
│ ├── edit-{item}-sheet.tsx # Update form
│ └── view-{item}-sheet.tsx # Read-only view
└── context/
└── {feature}-actions-context.tsx # CRUD actions context
| Component | Path | Purpose |
|---|---|---|
| DataTable | components/data-table/table/data-table.tsx | TanStack Table wrapper |
| TableHeader | components/data-table/table/table-header.tsx | Search + export bar |
| TableController | components/data-table/table/table-controller.tsx | Bulk actions bar |
| Pagination | components/data-table/table/pagination.tsx | URL-synced pagination |
| SearchInput | components/data-table/controls/search-input.tsx | Debounced search |
| ExportButton | components/data-table/actions/export-button.tsx | CSV/Excel export |
| Type | Path Pattern |
|---|---|
| Server Actions | lib/actions/{feature}.actions.ts |
| Client API | lib/api/{feature}.ts |
| Types | types/{feature}.ts |
1. page.tsx (Server)
↓ Fetch initial data with server actions
↓ Pass to client component as initialData
2. {feature}-table.tsx (Client)
↓ SWR hook with fallbackData: initialData
↓ Define updateItems() for cache mutations
↓ Wrap children in ContextProvider
3. Column definitions
↓ Receive callbacks: updateItems, markUpdating, clearUpdating
↓ Call API → Update cache → Show toast
4. User interaction
↓ markUpdating([id]) → API call → updateItems([result]) → clearUpdating()
export default async function Page({ searchParams }) {
const data = await getData(limit, skip, filters);
return <Table initialData={data} />;
}
const { data, mutate } = useSWR(apiUrl, fetcher, {
fallbackData: initialData,
keepPreviousData: true,
revalidateOnMount: false,
});
const updateItems = async (updatedItems) => {
await mutate((current) => ({
...current,
items: current.items.map(item =>
updatedMap.has(item.id) ? updatedMap.get(item.id) : item
),
}), { revalidate: false });
};
markUpdating([id]);
try {
const result = await apiCall(id, data);
await updateItems([result]);
toast.success(message);
} finally {
clearUpdating([id]);
}
For complete patterns, see PATTERNS.md.
DO:
DON'T:
Before completing data table work: