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
activeViewIdwhen 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:
- built-in defaults
initialState- workspace default view
- user default view
- persisted current view
- 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
tableKeyseparates product surfaces workspaceIdseparates organizations or accountsuserIdseparates 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