YYayaw

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, 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.

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). Typical names:

ParamExampleDescription
{tableId}-sortproducts-sortSort state (array of { id, desc }).
{tableId}-filtersproducts-filtersColumn filters.
{tableId}-advancedFiltersproducts-advancedFiltersAdvanced filter rules.
{tableId}-qproducts-qGlobal search.
{tableId}-pageproducts-page0-based page index.
{tableId}-pageSizeproducts-pageSizePage size.
{tableId}-visibilityproducts-visibilityColumn visibility (object).
{tableId}-orderproducts-orderColumn order.
{tableId}-displayproducts-displayDisplay mode (table, kanban, or gallery).
{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).

Saved views

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
  • Display mode, Kanban overrides, and Gallery overrides
  • Grouping
  • Column pinning
  • Page size

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": [],
  "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 toolbar 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: