Docs
Display and explore

URL State (Nuqs)

Sort, filters, pagination and column state in the URL with Nuqs

URL State (Nuqs)

The table keeps sort, filters, pagination, column visibility, order and widths, display mode, grouping, and pinning in the URL using Nuqs. This provides:

  • Shareable links – Send a link and the recipient sees the same view (same sort, filters, page).

  • Back/forward – Browser history reflects table state.

  • SSR-friendly – The same URL can be used for server-side rendering or prefetching.

Set table.syncUrl: false or the component syncUrl={false} prop when the host page owns navigation. React and Vue then keep the same table state in shared, instance-isolated memory and perform no table URL reads or writes. Nested tablePicker form fields use this mode by default so they cannot overwrite the parent table's query parameters; opt into picker URL state only with an explicit syncUrl: true.

React ignores repeated writes of equivalent table state in URL and memory modes. Reapplying an unchanged column order does not retrigger table subscribers during row actions or form opening.

Back and forward set only what the URL changed. Returning to the same query, or to another column order, loads neither the rows nor the display modes (File tree, Feed, Calendar, Chart, Map) again: in React, and in Vue since v3.9.2.

Setup (Next.js App Router)

  1. Install nuqs:

npm install nuqs
  1. Add NuqsAdapter in your root layout:

// app/layout.tsx
import { NuqsAdapter } from "nuqs/adapters/next/app";

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <NuqsAdapter>
          {children}
        </NuqsAdapter>
      </body>
    </html>
  );
}

No other configuration is required; the table uses Nuqs internally.

What is stored in the URL

Query params are prefixed by table id (your tableType, e.g. products), or by the instanceId when you set one. Typical names:

ParamExampleDescription
{tableId}-sortproducts-sortSort state (array of { id, desc }).
{tableId}-filtersproducts-filtersColumn filters.
{tableId}-advancedFiltersproducts-advancedFiltersAdvanced filter rules; date rule values are calendar days (YYYY-MM-DD). Links written by versions before v3.8.0 hold instants and open on the same days.
{tableId}-qproducts-qGlobal search.
{tableId}-pageproducts-page0-based page index.
{tableId}-pageSizeproducts-pageSizePage size.
{tableId}-visibilityproducts-visibilityColumn visibility (object).
{tableId}-orderproducts-orderColumn order. React writes it when the person moves a column or applies a view; without it, the table shows columns.order.
{tableId}-sizingproducts-sizingUser-defined column widths in pixels.
{tableId}-displayproducts-displayDisplay mode: table, list, kanban, gallery, filetree, calendar, chart, feed, map, form or gantt, among those the table offers.
{tableId}-kanbanproducts-kanbanKanban overrides such as lane grouping, title column, card properties, and label visibility.
{tableId}-kanbanGroupByproducts-kanbanGroupByLegacy Kanban lane grouping param. Still read as a fallback when {tableId}-kanban.groupBy is absent.
{tableId}-galleryproducts-galleryGallery overrides such as image column, title column, card properties, media ratio, fit, size, and label visibility.
{tableId}-groupingproducts-groupingGrouping column ids.
{tableId}-pinningproducts-pinningPinned columns (left/right).

Values are JSON-encoded or plain strings. The table reads and writes these via Nuqs; you don’t need to read them yourself unless you want to use them on the server (e.g. for SSR).

Several tables on one page

By default, a table's URL keys start with its table id (products-sort, products-q…), and the active saved view uses the shared view key (React also writes historyIndex). Two tables with the same table id, or two tables with saved views, on one page would read and write the same keys. Give each of them an instanceId:

two-task-tables.tsx
<DataTable tableType="tasks" instanceId="my-tasks" />
<DataTable tableType="tasks" instanceId="team-tasks" />
TwoTaskTables.vue
<template>
  <YayawDataTable :config="tasksConfig" :get-table-actions="() => tasksActions" instance-id="my-tasks" />
  <YayawDataTable :config="tasksConfig" :get-table-actions="() => tasksActions" instance-id="team-tasks" />
</template>

With instanceId, the instance's keys are <instanceId>-view, <instanceId>-historyIndex (React) and <instanceId>-<key>, such as my-tasks-sort, instead of view, historyIndex and <tableId>-<key>. It reads only its own keys from an incoming URL. In React, each instance also keeps its state in a store of its own, so two instances of one table never share filters, selection or pagination; Vue instances already keep their state apart. Config, actions and saved views are still those of the table. Without instanceId, keys and state are unchanged.

A table embedded without a toolbar, such as a side panel or a dashboard widget, can start from a saved view with initialView: { id?, config }: a saved view (its id becomes the active view) or a view config. It applies before the first request, so the first list already carries the view's filters and sorting. initialView needs URL sync off (table.syncUrl: false in the table's config) and is ignored otherwise; with URL sync on, use initialActiveViewId.

open-tasks-panel.tsx
const embeddedTasks = defineTableConfig({
  ...tasksConfig,
  table: { ...tasksConfig.table, syncUrl: false, showToolbar: false },
});

<DataTable
  tableType="tasks"
  getTableConfig={() => embeddedTasks}
  getTableActions={() => tasksActions}
  instanceId="open-tasks-panel"
  initialView={{ id: openTasksView.id, config: openTasksView.config }}
/>;

In Vue, pass the same values as instance-id and :initial-view.

Saved views

Persisted local or remote views load when the table mounts even when initialViews is omitted or empty. This includes returning to the page after a reload.

Saved views are built from the same URL-backed state. The view manager is enabled by default and can be disabled with enableViews: false. Use allowViewSave: false when users can select existing views but should not create, update, or delete them. Use allowViewSharing: true to show the “Share with team” option when saving a view.

When a view is saved, Yayaw Table persists this DB-friendly snapshot:

  • Global search ({tableId}-q)

  • Column filters and advanced filters

  • Sorting

  • Column visibility and column order

  • Column widths

  • Display mode, Kanban overrides, and Gallery overrides

  • Grouping

  • Column pinning

  • Page size

  • Effective table density (density)

density is part of the saved snapshot, rather than an independent query parameter. Changing it marks an active saved view as modified. Applying a legacy view without density restores the configured table default; other tables keep their own density.

The current page, expanded rows, and browser history index are not persisted. Applying a view always resets {tableId}-page to 0 so a filtered view does not open on an out-of-range page.

Kanban and Gallery views persist display choices only. Kanban stores the selected lane/title/property columns and label visibility; Gallery stores the selected image/title/property columns and card layout settings. Neither view stores row data or image data.

For production persistence, expose view actions from getTableActions:

const getTableActions = (tableType: string) => {
  if (tableType !== "products") {
    return;
  }

  return {
    list: listProducts,
    views: {
      list: async ({ tableId }) => ({ data: await db.views.list(tableId) }),
      create: async (input) => await db.views.create(input), // input.isGlobal is true for team-shared views
      update: async (id, input) => await db.views.update(id, input),
      delete: async (id, context) => await db.views.delete(id, context),
    },
  };
};

If no views actions are provided, the copied component uses a localStorage fallback so the UI remains usable in prototypes. Consumer applications should replace that fallback with database-backed actions when saved views need to follow authenticated users or teams.

Use one generic table_views table for all Yayaw Table instances instead of creating one view table per business entity. A saved view describes UI state, not a database table, so identify the target table with a stable application key such as products, customers, or orders. Avoid using a SQL table name as the long-term identifier because one UI table can be backed by joins, search indexes, API responses, or renamed database tables.

create table table_views (
  id uuid primary key,
  workspace_id uuid not null,
  table_key text not null,
  name text not null,
  config jsonb not null,
  visibility text not null default 'private',
  owner_user_id uuid,
  created_by_id uuid,
  is_system boolean not null default false,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  deleted_at timestamptz
);

Store the normalized view config in config rather than the full URL string. Keeping the snapshot structured makes it easier to validate, migrate, inspect, and apply across clients.

{
  "version": 1,
  "sorting": [],
  "columnFilters": [],
  "advancedFilters": [],
  "globalSearch": "samsung",
  "columnVisibility": {},
  "columnOrder": [],
  "columnSizing": { "name": 280, "price": 140 },
  "displayMode": "gallery",
  "kanban": {
    "groupBy": "status",
    "titleColumn": "name",
    "cardColumnIds": ["brand", "price"],
    "showCardLabels": false
  },
  "gallery": {
    "imageColumn": "imageUrl",
    "titleColumn": "name",
    "cardColumnIds": ["brand", "category", "price", "status"],
    "aspectRatio": "square",
    "imageFit": "cover",
    "cardSize": "medium"
  },
  "columnPinning": { "left": [], "right": [] },
  "grouping": [],
  "pageSize": 50
}

Use visibility to model whether a view is private to one user, shared with a workspace, or provided by the system. If users can choose their own default view, keep that preference outside table_views so the default is unambiguous:

create table table_view_preferences (
  user_id uuid not null,
  workspace_id uuid not null,
  table_key text not null,
  default_view_id uuid references table_views(id),
  primary key (user_id, workspace_id, table_key)
);

This lets the same shared view be the default for one user without becoming the default for everyone else. For workspace-wide defaults, store that separately as a workspace preference or add an explicit scope column to the preference table.

Sharing and reset

  • Copy link – The Data menu can offer “Copy link” / “Share” using the current URL (all table params are already in it).

  • Reset – Resetting the table state clears these params (or restores defaults), so the URL is updated accordingly.

Server-side use of URL state

If you render the table on the server (e.g. in a Server Component), you can read the same params from searchParams and pass initial data or use them in your Server Action. The client will hydrate with the same URL and Nuqs will stay in sync.

Dependencies

  • nuqs – Required for URL state. The table uses useQueryState and custom parsers.

  • Next.js – Use nuqs/adapters/next/app for the App Router.

See also:

Saved-view compatibility and management

The shared snapshot uses globalSearch, columnFilters, and columnPinning. Vue's older search, filters, and pinning names remain accepted; canonical values win when both exist. Legacy Kanban grouping is normalized into grouping. Advanced filters retain their AND/OR combination and inactive rules.

In Vue, a default view applies only when no requested view or explicit table URL state takes precedence. The manager indicates unsaved changes and offers save-as, update, rename, default selection, and deletion as permitted by configuration. System views are protected against update/deletion in the UI; enforce those permissions on the server too. Failed persistence keeps the draft available for correction or retry.

The Vue manager uses English/French translations, keyboard-accessible menus and dialogs, focus restoration, and table theme tokens in portals. Native Vue URL synchronization does not require Nuqs; the Nuqs setup above is specific to React.

Organization-shared views

allowViewSharing: true exposes the sharing choice, and isGlobal: true marks a team view in the persistence contract. Neither flag supplies organization membership, cross-device storage, or authorization. The built-in localStorage fallback, including the public demo, is confined to the current browser; marking a local view as shared does not distribute it to colleagues.

A production views adapter must derive the active organization and authenticated user on the server. views.list returns only that user's private views and views shared with the active organization for the requested table. Creation, update, and deletion must check the same scope and the caller's permissions. Recheck access to a requested view ID; a URL, tableId, or client-supplied isGlobal value is not permission to read or change another organization's data. Saving a shared layout never shares the underlying rows: the data query still enforces its own access rules.

The preference schema above is an application integration proposal. Wire views.getFavorite and views.setFavorite to that separate per-user record. A favorite can reference any accessible private, shared, or system view without changing the shared snapshot or setting a default for every member. Keep organization defaults separate.

Clearing filters from an empty result preserves the active view ID and modifies only the current draft. It does not update or delete the saved view; use Save changes when the modified configuration should persist.

Personal favorite on arrival

The built-in Default view also has a favorite star. Select it and press the star to clear the personal override with setFavorite(null, context); no synthetic view record is created. Its toolbar and menu stars are filled when no accessible saved favorite exists. Pressing its already-filled star is a no-op. A failed write leaves the previous favorite intact and can be retried. Temporary unsaved view IDs cannot be starred. Shared isDefault arrival precedence remains unchanged.

With saved views enabled, select a view and press the star beside its name to use it on arrival. For a saved view, press the filled star to remove the favorite; choosing a different favorite replaces the previous one. Selecting another view alone does not change the favorite. This works in React and Vue, including for shared or system views and with allowViewSave: false.

The favorite references the saved configuration. Starring a modified view does not save its pending edits; use Save changes first to include them. Favoriting never changes the view's config, isDefault, isGlobal, or owner.

Explicit table URL state keeps precedence when URL synchronization is enabled. Otherwise the arrival order is initialActiveViewId, the accessible favorite, an isDefault view, then the catalogue configuration. Only the incoming URL counts as explicit state: the table reads it once on mount, and a write the table makes itself on arrival never cancels the view. Since v3.9.2, React writes no column order of its own, only when the person moves a column or applies a view; before, a React table whose columns.order differed from its definitions wrote its definitions' order right after mounting, and opened on Default view instead of the favorite or an isDefault view. An invalid explicit initialActiveViewId leaves the normal configuration in place; a deleted or inaccessible favorite falls back to the default. Later responses do not overwrite edits made during loading. Clearing a favorite keeps the current view selected until the next arrival, and manually resetting the view does not immediately restore the favorite.

Refresh the copied registry to obtain the favorite API. Without preference handlers, the preference uses browser localStorage keyed by the table type and instance ID. This supports remotely loaded views but does not synchronize devices or isolate accounts sharing the browser automatically.

Supply both optional handlers alongside your existing complete CRUD adapter:

views: {
  ...viewCrudActions,
  getFavorite: async (context) => ({
    success: true,
    data: { viewId: await readMyFavorite(context) },
  }),
  setFavorite: async (viewId, context) => {
    await saveMyFavorite(context, viewId);
    return { success: true, data: { viewId } };
  },
}

Both receive { tableId, tableType }; setFavorite also receives the view ID or null to clear it. Return { success: true, data: { viewId: null } } when no preference exists, or { success: false, error: "Unable to save your favorite view" } on failure. React requires success; Vue also accepts its omission. Failures are reported without replacing the saved preference.

Implement readMyFavorite and saveMyFavorite on the server with the authenticated user and active organization. Use a unique (organizationId, userId, tableType, tableId) preference key and validate access to the target view. Clear references when deleting a view or revoking access. Scope the client instance ID and query cache to the active user/organization, remount on scope changes, and clear the old React cache at sign-out; client IDs never replace server authorization. Shared-view CRUD permissions remain independent of personal preferences.