react-datatable
Guides

Add current-view persistence

Use this guide when a table should remember how one user last left it.

When to persist the current view

Current-view persistence is for per-user continuity.

Add it when users should be able to come back and find the table roughly where they left it, including things like:

  • hidden or reordered columns
  • sorting and grouping choices
  • display settings
  • current search and column filters
  • the currently active saved view ID, when one was explicitly chosen

Do not use it for state that should stay temporary, collaborative, or route-specific.

Add a persistence adapter and stable table key

Current-view persistence is enabled with persistState.

import { localStorageAdapter } from "./react-datatable/persistence"

<Datatable
  tableKey="customers"
  data={rows}
  columns={columns}
  getRowId={(row) => row.id}
  persistState={{
    adapter: localStorageAdapter,
    workspaceId: workspace.id,
    userId: currentUser.id,
  }}
/>

The adapter inherits the top-level tableKey, which is the main identity for the product surface.

Choose a stable key like customers, workspace-members, or billing-invoices. Do not bake temporary route params into it.

[!NOTE] Screenshot placeholder: the same table before and after reload, showing remembered column layout, sorting, filters, and display settings restored for one user.

Choose an adapter

Use localStorageAdapter when one browser on one device is enough.

Use sessionStorageAdapter when the table should remember state only until the browser session ends.

Use a backend adapter when the same person should get the same table state across devices or browsers.

persistState={{
  adapter,
  workspaceId: workspace.id,
  userId: currentUser.id,
  debounceMs: 1500,
}}

The built-in persistence hook auto-saves the extracted persisted state with a debounce, so backend adapters should be designed for repeated small writes.

Add a backend adapter

A backend adapter implements the TableStateAdapter contract: get, set, and delete.

import type {
  PersistedTableState,
  TableStateAdapter,
} from "../../../kit/src/react-datatable/persistence"

export const tableStateServerAdapter: TableStateAdapter = {
  async get({ tableKey, workspaceId, userId }) {
    const res = await fetch(`/api/datatable-state?tableKey=${tableKey}&workspaceId=${workspaceId}&userId=${userId}`)
    if (res.status === 404) return null
    if (!res.ok) throw new Error("Failed to load table state")
    return (await res.json()) as PersistedTableState
  },

  async set({ tableKey, workspaceId, userId }, state) {
    const res = await fetch("/api/datatable-state", {
      method: "PUT",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ tableKey, workspaceId, userId, state }),
    })
    if (!res.ok) throw new Error("Failed to save table state")
  },

  async delete({ tableKey, workspaceId, userId }) {
    const res = await fetch("/api/datatable-state", {
      method: "DELETE",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ tableKey, workspaceId, userId }),
    })
    if (!res.ok) throw new Error("Failed to reset table state")
  },
}

Use a server adapter when persistence should survive device changes or when support staff need to inspect and reset saved state centrally.

What gets persisted

The source of truth is PersistedTableState.

It includes durable working-state choices such as:

  • column visibility, order, and widths
  • sticky column count
  • display settings like grid lines, column headers, and ordering badges
  • sorting and grouping
  • global search, column filters, and filter mode
  • activeViewId when the user explicitly selected a saved view
type PersistedTableState = {
  showColumnHeaders: boolean
  stickyColumnsCount: number
  showHorizontalLines: boolean
  showVerticalLines: boolean
  showEmptyGroups: boolean
  showOrderingBadge: boolean
  columnOrder: string[]
  columnVisibility: Record<string, boolean>
  columnWidths: Record<string, number>
  sorting: DatatableState["sorting"]
  grouping: string[]
  groupingOrder: Record<string, Record<string, number>>
  groupExpanded: DatatableState["groupExpanded"]
  globalFilter: string
  columnFilters: DatatableState["columnFilters"]
  filterMode: DatatableState["filterMode"]
  activeViewId?: string | null
}

Keep transient state out

Do not treat everything in the table as a preference.

The table intentionally does not persist:

  • row selection
  • active-row keyboard focus
  • preview open or closed state
  • temporary popovers
  • in-progress action flows

A user expects their preferred table shape to come back. They usually do not expect yesterday's half-finished bulk selection or open preview panel to return with it.

Defaults, views, and URLs

Persistence loads after higher-priority startup inputs.

The coordinator merges state in this order:

  1. built-in defaults
  2. initialState
  3. workspace default view
  4. user default view
  5. persisted current view
  6. URL state

That means persisted state beats default views, but URL state still wins when someone opens a shareable link.

That lets a table feel personal by default without breaking intentional sharing.

Scope persistence correctly

Use the adapter config to prevent unrelated tables from overwriting each other.

persistState={{
  adapter,
  workspaceId: workspace.id,
  userId: currentUser.id,
}}

A good rule is:

  • the top-level tableKey separates product surfaces
  • workspaceId separates organizations or accounts
  • userId separates personal preferences inside the same workspace

If one of those boundaries matters in your product, include it.

Save timing and failures

Persistence writes happen after a debounce.

persistState={{
  adapter,
  debounceMs: 2000,
  onSave: () => analytics.track("customers_table_persisted"),
  onError: (error) => reportError(error),
}}

Increase debounceMs when writes go over the network or when users make many rapid layout adjustments.

Always decide where save failures should surface. Silent failure makes the table feel unreliable.

Add a reset path

If a remembered table state can become confusing, expose a reset action.

await adapter.delete({
  tableKey: "customers",
  workspaceId,
  userId,
})

That is especially useful for support workflows, debugging, and major table migrations.

[!NOTE] Screenshot placeholder: a simple reset-table-state control in product UI, with the table returning from a customized remembered layout back to its default view.

Verify current-view persistence before you move on

Before you continue, confirm that:

  • the table has a stable tableKey
  • persistence scope matches the product boundaries that matter
  • only durable preferences are saved
  • transient interaction state stays transient
  • debounce timing fits the adapter cost
  • failures are observable somewhere useful
  • URL-based sharing still overrides remembered state when that is intentional

On this page