YYayaw

DataTable Reference

Props and usage reference for the unified DataTable component

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

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.

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 table.rowClickMode is set to 'activate' and a non-interactive row or card area is clicked.

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.

Type: ToolbarAction[] | ((ctx: ToolbarActionContext) => ToolbarAction[]) | Default: undefined

toolbarActionsPlacement

Control where custom toolbar actions are rendered relative to built-in actions.

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;
};

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' | 'option' | 'multiOption'> | 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: