DataTable Reference
Props and usage reference for the unified DataTable component
Native entry points
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>
);
}Sample data. Changes stay in this preview.
<script setup lang="ts">
import type { ExamplePresentation } from "./presentation";
defineProps<ExamplePresentation>();
import { YayawDataTable } from "@/components/ui/yayaw-table-vue";
import { getTableActions } from "./product-actions";
import { productConfig } from "./product-config";
</script>
<template>
<YayawDataTable v-bind="$props"
:config="productConfig"
:get-table-actions="getTableActions"
:get-row-id="row => String(row.id)"
table-id="products-example"
table-type="products"
:title="title ?? 'Products'"
/>
</template>DataTable Reference
The single entry-point component: you pass config and actions as props; it composes the provider, UI, and table logic internally.
import { DataTable } from "@/components/ui/yayaw-table";For a minimal working setup you need tableType, getTableConfig, and getTableActions. See Provider & Setup for the full list and Next.js layout (NuqsAdapter, QueryClientProvider).
If no QueryClient is available from context (and no explicit queryClient prop is provided), DataTable now throws an explicit runtime error.
Props
List of available props for configuring DataTable.
tableType
The type of table configuration to use. Used to resolve config and actions (e.g. getTableConfig("products"), getTableActions("products")).
<DataTable tableType="products" />Required: true
tableId
Stable table instance id for URL state, cache keys, selection, pagination, and toolbar state. Defaults to tableType.
Use it when one table instance displays several business models but should still share one URL/cache/selection surface.
Type: string
instanceId
Isolates this instance on a page with other tables: its URL keys become <instanceId>-view, <instanceId>-historyIndex and <instanceId>-<key> instead of view, historyIndex and <tableId>-<key>, and its state lives in a store of its own. Config, actions and saved views still resolve by table. See Several tables on one page.
Type: string | Default: undefined
initialView
The saved view (its id becomes the active view) or view config an instance with URL sync off (table.syncUrl: false) starts from, applied before its first request. Ignored when URL sync is on; use initialActiveViewId then. For tables embedded without a toolbar, such as dashboard widgets.
Type: { id?: string | null; config: TableViewConfig } | Default: undefined
onViewConfigChange
Called with the view the table shows (canonicalViewConfig(view): its search, filters, sort, grouping, columns, display mode, each mode's settings and page size, sanitized like saved views, without the select and actions columns, keys in one order) when the table starts and after each change, never twice for an equal view. The dashboard's view editor and "Make the current view the screen default" read it. Vue emits view-config-change with the same config and exposes getViewConfig().
Type: (config: ViewConfig) => void | Default: undefined
formType
Default form configuration type for create/edit forms. Defaults to tableType. Table config can still override it with form.createFormType, form.editFormType, or form.resolveEditFormType(row).
Type: string
getTableConfig
Function that returns table and column configuration for the given tableType. Required for the table to know columns, sort, visibility, and options. See Configuration and Setup.
Type: (tableType: string) => Config | undefined
getTableActions
Function that returns actions (list, create, update, delete, bulk, etc.) for the given tableType. list is required for server-driven data. Can be Server Actions in Next.js. See Actions and Server-side & Server Actions.
Type: (tableType: string) => TableActions | undefined
getFormConfig
Function that returns form field definitions for create/edit and bulk edit dialogs. Optional but needed for forms.
Type: (formType: string, ctx?: FormConfigContext) => FormConfig | undefined
ctx includes { mode, tableId, tableType, formType, row, initialData, values }, so form fields can change for edit rows or for current create-form values.
queryClient
Optional explicit TanStack Query client instance. Yayaw Table no longer creates one internally. In most apps you should provide a shared app-level QueryClientProvider and omit this prop.
Type: QueryClient
initialData
Rows rendered into the first client table state. Use this when a Server Component has already loaded the first page and you want the table to hydrate with useful data before TanStack Query performs its client refresh. Without it, the table shows its loading state from the first render, on the server too, until the first page answers, then mounts its view once with those rows (since v3.9.1; React used to render an empty table first, and mount the view twice).
Type: Record<string, unknown>[] | Default: []
initialPageCount
Total page count that matches initialData. Provide it for server-paginated datasets so the first render keeps the correct pagination controls instead of assuming a single page.
Type: number | Default: 1 when initialData is present
initialRowCount
Total row count that matches initialData. Provide it when the server knows the full filtered total for the initial page.
Type: number | Default: initialData.length
className
Additional class names applied to the root wrapper.
Type: string | Default: undefined
loadingOverlay
Custom loading overlay to display while data is loading.
Type: React.ReactNode | Default: internal spinner overlay
<DataTable tableType="products" loadingOverlay={<MySpinner />} />onRowSelectionChange
Callback fired when selected rows change.
Type: (rows: Row<Record<string, unknown>>[]) => void | Default: undefined
activeRowId
Mark one row as active. Use this for master-detail tables where the table is the inventory surface and another panel renders the selected row.
Type: string | Default: undefined
getRowId
Return a stable row id for active-row highlighting, selection, and row metadata. If omitted, the table falls back to common id fields and the React Table row id.
Type: (row: Record<string, unknown>) => string | Default: undefined
onRowActivate
Callback fired when a non-interactive row or card area is clicked and the click resolves to opening the record view — table.rowClickMode: 'activate', or the default mode when no edit or row-link behavior applies. It runs alongside the built-in record view (see Record details) rather than replacing it; pass details={false} to rely on onRowActivate alone.
Type: (row: Record<string, unknown>, event: React.MouseEvent) => void | Default: undefined
onRowClick
Callback fired by link-style row interactions, including row-link table clicks and Gallery link buttons. Use it to route through app navigation or analytics instead of relying on the browser default.
Type: (url: string, row: Record<string, unknown>, event: React.MouseEvent) => void | Default: undefined
emptyState
Per-instance empty/no-results state override. It is merged over table.emptyState from configuration.
Type: { show?: boolean; title?: string; description?: string } | Default: undefined
onBulkEdit
Handle bulk edit of selected rows.
Type: (rows: Row<Record<string, unknown>>[]) => BulkActionResult | void | Promise<BulkActionResult | void> | Default: uses provider actions
onBulkDelete
Handle bulk deletion of selected rows.
Type: (rows: Row<Record<string, unknown>>[]) => BulkActionResult | BulkDeleteExecutionOutcome | void | Promise<...> | Default: uses provider bulkDelete / delete
onBulkCopy
Handle bulk duplication of selected rows.
Type: (rows: Row<Record<string, unknown>>[]) => BulkActionResult | void | Promise<BulkActionResult | void> | Default: internal clipboard copy
onExport
Override toolbar export behavior. Called with all rows matching current search/filters/sort state. If provided, default CSV download is skipped.
Type: (rows: Record<string, unknown>[]) => void \| Promise<void> | Default: internal CSV export
onBulkExport
Override bulk export behavior. Called with selected rows. If provided, default CSV download is skipped.
Type: (rows: Row<Record<string, unknown>>[]) => void \| Promise<void> | Default: internal CSV export
customBulkActions
Inject custom actions in the selected-row bulk actions menu. Supports a static array or a callback receiving live selected-row context.
Type: BulkAction<TData>[] | ((ctx: BulkActionContext<TData>) => BulkAction<TData>[]) | Default: undefined
Custom actions render after the built-in export action and before the built-in delete action. A custom action can be disabled from the current selection context, can ask for confirmation, and can return a BulkActionResult to control menu closing, selection clearing, and user feedback.
type BulkActionContext<TData> = {
selectedRows: Row<TData>[];
selectedOriginalRows: TData[];
selectedCount: number;
};
type BulkAction<TData> = {
id: string;
label: string;
icon: ComponentType<{ className?: string; size?: number }>;
onClick: (ctx: BulkActionContext<TData>) =>
| BulkActionResult
| void
| Promise<BulkActionResult | void>;
disabled?: boolean | ((ctx: BulkActionContext<TData>) => boolean);
variant?: "default" | "destructive";
confirm?: {
title?: string | ((ctx: BulkActionContext<TData>) => string);
description?: string | ((ctx: BulkActionContext<TData>) => string);
confirmLabel?: string | ((ctx: BulkActionContext<TData>) => string);
cancelLabel?: string | ((ctx: BulkActionContext<TData>) => string);
};
};toolbarActions
Inject custom actions in the main toolbar. Supports a static array or a callback receiving live toolbar context. On wide screens they render as toolbar buttons before View settings; on touch layouts they join the Data menu, alongside Export, Connect and Share.
Type: ToolbarAction[] | ((ctx: ToolbarActionContext) => ToolbarAction[]) | Default: undefined
toolbarActionsPlacement
Accepted for backward compatibility. It no longer changes the position of custom toolbar actions, since Export moved into the Data menu.
Type: "before-create" | "between-create-export" | "after-export" | Default: "between-create-export"
ToolbarAction and ToolbarActionContext
type ToolbarAction = {
id: string;
label: string;
icon?: ReactNode;
onClick: (ctx: ToolbarActionContext) => void | Promise<void>;
disabled?: boolean | ((ctx: ToolbarActionContext) => boolean);
loading?: boolean;
variant?: "default" | "outline" | "secondary" | "ghost" | "destructive";
requiresFooterCalculations?: boolean;
showInIconMode?: boolean; // default true
tooltip?: string;
};
type ToolbarActionContext = {
tableId: string;
actionsAsIcons: boolean;
isMobile: boolean;
isCreateEnabled: boolean;
isExportEnabled: boolean;
isExporting: boolean;
isFooterCalculationsEnabled: boolean;
hasListAction: boolean;
selectedRows: Row<Record<string, unknown>>[];
selectedOriginalRows: Record<string, unknown>[];
selectedRowIds: string[];
selectedCount: number;
tableActions?: TableActions;
/** The view the table shows, as `onViewConfigChange` reports it (v3.9.0). */
getViewConfig: () => ViewConfig;
};Code that builds a ToolbarActionContext itself, such as a test, adds getViewConfig since v3.9.0.
Multi-Model Table
A single table can keep one tableId while resolving table config and form config separately:
<DataTable
tableId="cms-entries"
tableType="content-index"
formType="content-entry"
getTableConfig={(tableType) => ({
...configs[tableType],
form: {
createFormType: "content-entry",
resolveEditFormType: (row) => `${row.modelId}-entry`,
},
})}
getFormConfig={(formType, ctx) =>
buildEntryForm({
formType,
modelId: String(ctx?.values?.modelId ?? ctx?.row?.modelId ?? ""),
})
}
customBulkActions={(ctx) => [
{
id: "publish-selected",
label: "Publish",
icon: Send,
disabled: ctx.selectedCount === 0,
onClick: async () => publishEntries(ctx.selectedOriginalRows),
},
{
id: "archive-selected",
label: "Archive",
icon: Archive,
variant: "destructive",
disabled: ctx.selectedCount === 0,
confirm: {
title: "Archive selected entries?",
description: `Archive ${ctx.selectedCount} selected entries.`,
},
onClick: async () => archiveEntries(ctx.selectedOriginalRows),
},
]}
/>Here cms-entries owns URL/cache/selection, content-index owns the table columns and filters, and each row can open its own edit form type.
Server-first initial data
For Next.js App Router pages, load the first page in a Server Component, then pass the rows and pagination metadata into a small Client Component that renders DataTable.
// app/products/page.tsx
import { listProducts } from "./actions/products";
import { ProductsTableClient } from "./products-table-client";
export default async function ProductsPage() {
const initial = await listProducts({ limit: 10, page: 1 });
return (
<ProductsTableClient
initialData={initial.data}
initialPageCount={initial.meta.pageCount}
initialRowCount={initial.meta.totalCount}
/>
);
}// app/products/products-table-client.tsx
"use client";
import { DataTable } from "@/components/ui/yayaw-table";
import { listProducts } from "./actions/products";
import { getTableConfig } from "./table-config";
export function ProductsTableClient({
initialData,
initialPageCount,
initialRowCount,
}: {
initialData: Record<string, unknown>[];
initialPageCount: number;
initialRowCount: number;
}) {
return (
<DataTable
getTableActions={() => ({ list: listProducts })}
getTableConfig={getTableConfig}
initialData={initialData}
initialPageCount={initialPageCount}
initialRowCount={initialRowCount}
tableType="products"
/>
);
}initialData is only the first cache value. Sorting, filtering, pagination, retry, and refresh still call your getTableActions().list action through TanStack Query, so authorization and data access stay in your server layer.
enableAdvancedFilters
Enable advanced filters UI when available in your configuration.
Type: boolean | Default: false
columnTypeMapping
Map dynamic backend types to internal column renderer types.
Type: Record<string, 'text' | 'number' | 'date' | 'select' | 'multiSelect'> | Default: {}
Usage
<DataTable
tableType="products"
loadingOverlay={<MySpinner />}
onRowSelectionChange={(rows) => console.log(rows)}
onBulkDelete={(rows) => console.log("delete", rows.length)}
onExport={(rows) => console.log("export all", rows.length)}
onBulkExport={(rows) => console.log("export selected", rows.length)}
/>Custom action in text mode:
<DataTable
tableType="products"
toolbarActions={[
{
id: "recalculate-prices",
label: "Recalculate prices",
onClick: async () => {
await recalculatePrices();
},
variant: "secondary",
},
]}
/>Custom action in icon mode with tooltip and explicit placement:
<DataTable
tableType="products"
toolbarActions={(ctx) => [
{
id: "recalculate-prices",
label: "Recalculate prices",
tooltip: "Recalculate prices",
icon: <RefreshCw className="h-4 w-4" />,
requiresFooterCalculations: true,
disabled: () => !ctx.hasListAction || ctx.isExporting,
onClick: async () => {
await recalculatePrices();
},
},
]}
toolbarActionsPlacement="between-create-export"
/>Backward compatibility: if toolbarActions is omitted, toolbar behavior stays unchanged.
Use requiresFooterCalculations: true for custom actions that only make sense when table.enableCalculations is enabled. The table hides those actions while footer calculations are disabled.
Master-detail row activation:
"use client";
import { useState } from "react";
import { DataTable } from "@/components/ui/yayaw-table";
import { getTableActions, getTableConfig } from "./table-config";
export function ProductsInventoryTable() {
const [activeProductId, setActiveProductId] = useState<string>();
return (
<DataTable
activeRowId={activeProductId}
emptyState={{
title: "No products found",
description: "Try changing your search or filters.",
}}
getRowId={(row) => String(row.id)}
getTableActions={getTableActions}
getTableConfig={(tableType) => {
const config = getTableConfig(tableType);
return {
...config,
table: {
...config?.table,
layoutPreset: "preview",
rowClickMode: "activate",
},
};
}}
onRowActivate={(row) => {
setActiveProductId(String(row.id));
}}
tableType="products"
/>
);
}See also: