React quick start
A complete React table with typed configuration and working list actions.
Build a working, read-only product table with search, sorting, filters, and pagination. This example runs in a React application and uses a small in-memory list adapter. Follow installation first.
Project structure
products/
shared/products.ts
react/product-config.ts
react/product-actions.ts
react/products.tsx1. Define records
export interface Product extends Record<string, unknown> {
active: boolean;
id: string;
image: string;
name: string;
price: number;
/** Civil dates, inclusive at both ends: the window a batch is being restocked. */
restockFrom: string;
restockTo: string;
status: "draft" | "active" | "archived";
stock: number;
tags: string[];
updatedAt: string;
}
export const products: Product[] = [
{
id: "desk",
name: "Oak desk",
price: 420,
stock: 12,
status: "active",
active: true,
tags: ["office", "wood"],
image: "https://placehold.co/640x480/png?text=Oak+desk",
restockFrom: "2026-09-14",
restockTo: "2026-09-18",
updatedAt: "2026-09-01T09:00:00Z",
},
{
id: "lamp",
name: "Desk lamp",
price: 65,
stock: 0,
status: "draft",
active: false,
tags: ["office"],
image: "https://placehold.co/640x480/png?text=Desk+lamp",
restockFrom: "2026-09-17",
restockTo: "2026-09-24",
updatedAt: "2026-09-02T09:00:00Z",
},
{
id: "chair",
name: "Studio chair",
price: 180,
stock: 6,
status: "active",
active: true,
tags: ["office", "seating"],
image: "https://placehold.co/640x480/png?text=Studio+chair",
restockFrom: "2026-09-23",
restockTo: "2026-10-02",
updatedAt: "2026-09-03T09:00:00Z",
},
];2. Describe columns and behavior
Mutations and saved views are disabled for this first table. syncUrl: false keeps the example out of the page URL.
import { defineTableConfig } from "@/components/ui/yayaw-table";
export const productConfig = defineTableConfig({
id: "products",
translations: { namespace: "products", keys: {} },
columns: {
definitions: [
{ id: "name", header: "Name", type: "text" },
{ id: "price", header: "Price", type: "number" },
{ id: "stock", header: "Stock", type: "number" },
{
id: "status",
header: "Status",
type: "select",
options: [
{ label: "Draft", value: "draft" },
{ label: "Active", value: "active" },
{ label: "Archived", value: "archived" },
],
},
],
order: ["name", "price", "stock", "status"],
visible: ["name", "price", "stock", "status"],
mandatory: ["name"],
sort: [{ id: "name", desc: false }],
},
table: {
allowCreate: false,
allowEdit: false,
allowDelete: false,
allowDuplicate: false,
allowBulkEdit: false,
allowBulkDelete: false,
enableRowSelection: false,
enableViews: false,
syncUrl: false,
defaultPageSize: 10,
},
});
export const getTableConfig = (tableType: string) =>
tableType === "products" ? productConfig : undefined;3. Implement the list action
The adapter applies the query before slicing the page. Its data lives in memory; replace it with an HTTP API or Server Action for persistence. Only name participates in this example’s global search.
import type { TableActions } from "@/components/ui/yayaw-table/providers/table-provider";
import { products } from "../shared/products";
import { listRecords } from "./list-records";
export const productActions: TableActions = {
list: (input) => listRecords(products, input),
};
export const getTableActions = (tableType: string) =>
tableType === "products" ? productActions : undefined;Complete list adapter
import {
compatibleListParams,
matchesContractFilter,
normalizeFilterEnvelope,
recordValue,
} from "@/components/ui/yayaw-table/utils/table-contracts";
export function listRecords<T extends Record<string, unknown>>(
rows: T[],
input: unknown
) {
const params = compatibleListParams(recordValue(input));
const page = Number(params.page);
const limit = Number(params.limit);
const search = String(params.search).toLowerCase();
const filters = Object.entries(recordValue(params.filters));
const advanced = normalizeFilterEnvelope(params.advancedFilters).filters;
const matching = rows.filter((product) => {
const row: Record<string, unknown> = product;
const matchesSearch =
!search ||
["name", "reference", "price", "stock", "status", "active", "tags"].some(
(key) => {
const value = product[key];
return (
value !== null &&
value !== undefined &&
String(value).toLowerCase().includes(search)
);
}
);
const matchesColumns = filters.every(([id, value]) =>
matchesContractFilter(row[id], {
type: typeof row[id] === "number" ? "number" : "text",
operator: Array.isArray(value) ? "isAnyOf" : "contains",
values: Array.isArray(value) ? value : [value],
})
);
const matchesRule = (rule: Record<string, unknown>) =>
matchesContractFilter(row[String(rule.columnId)], rule);
const matchesAdvanced =
advanced.length === 0 ||
(params.advancedFilterJoin === "or"
? advanced.some(matchesRule)
: advanced.every(matchesRule));
return matchesSearch && matchesColumns && matchesAdvanced;
});
const sorting = Object.entries(recordValue(params.orderBy));
matching.sort((left, right) => {
for (const [id, direction] of sorting) {
const a = (left as Record<string, unknown>)[id];
const b = (right as Record<string, unknown>)[id];
const comparison =
typeof a === "number" && typeof b === "number"
? a - b
: String(a ?? "").localeCompare(String(b ?? ""));
if (comparison !== 0) {
return direction === "desc" ? -comparison : comparison;
}
}
return String(left.id).localeCompare(String(right.id));
});
return Promise.resolve({
// Model a server response: later store mutations must not alter cached rows.
data: structuredClone(matching.slice((page - 1) * limit, page * limit)),
meta: {
pageCount: Math.max(1, Math.ceil(matching.length / limit)),
totalCount: matching.length,
},
});
}4. Render the table
Keep one stable QueryClient in the application. This self-contained example creates it in component state. Nuqs still needs its adapter when URL writes are disabled because the table’s URL hooks remain mounted.
Sample data. Changes stay in this preview.
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { NuqsAdapter } from "nuqs/adapters/react";
import { useState } from "react";
import { DataTable } from "@/components/ui/yayaw-table";
import type { ExamplePresentation } from "./presentation";
import { getTableActions } from "./product-actions";
import { getTableConfig } from "./product-config";
export default function Products(presentation: ExamplePresentation = {}) {
// Reuse the application's provider instead when it already owns a QueryClient.
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<NuqsAdapter>
<DataTable
{...presentation}
getRowId={(row) => String(row.id)}
getTableActions={getTableActions}
getTableConfig={getTableConfig}
tableId="products-example"
tableType="products"
title={presentation.title ?? "Products"}
/>
</NuqsAdapter>
</QueryClientProvider>
);
}Next.js App Router
In Next.js, mount NuqsAdapter from nuqs/adapters/next/app in your application layout instead of the standalone React adapter. Keep the table catalog functions in a Client Component. Pass actual Server Actions to that client module, or call an HTTP API; avoid passing ordinary server functions across the boundary.
Check the result
Search for lamp, sort by Price, and filter Status. The table should display matching rows and correct totals. The next step is forms, saved views, or connecting a backend.
Common setup mistakes
Keep
tableTypeconsistent between the component and both lookup functions.Supply a QueryClient and the appropriate Nuqs adapter.
Keep row IDs stable across pages.
Return counts for the filtered dataset, not just the current page.
Add a single application Sonner outlet when enabling mutation and saved-view feedback.
Previous guide sections
Provider & Setup
Read the current guide: concepts.
Step-by-step setup (Next.js App Router)
Read the current guide: concepts.
1. Add NuqsAdapter in your root layout
Read the current guide: url-state.
2. Create your table actions (minimum: list)
Read the current guide: actions.
3. Create getTableConfig and getTableActions
Read the current guide: actions.
4. Render DataTable inside a shared QueryClientProvider
Read the current guide: query-integration.
5. Optional: add getFormConfig for create/edit dialogs
Read the current guide: forms.
Setup checklist
Read the current guide: concepts.
getTableConfig
Read the current guide: configuration.
getTableActions
Read the current guide: actions.
getFormConfig
Read the current guide: forms.
Collection fields
Read the current guide: collections.
Catalogue form layout
Read the current guide: form-layout.
Full props (single entry point)
Read the current guide: datatable.
Summary
Read the current guide: concepts.
Application notifications
Read the current guide: notifications.