Saved views and persistence
A grid that is open all day should remember how you left it. persist keeps the view (the
preferences, not the query) across reloads, in localStorage by default, and adds a Views menu where
the user saves the current view under a name and switches between them.
| Arjun Kim | Research | Intern | Invited | Free | $0.00 | 1 | Apr 4, 2026 |
| Saanvi Bose | Research | Manager | Invited | Pro | $48.00 | 4 | May 14, 2023 |
| Ananya Das | Growth | Engineer | Active | Team | $1,682.00 | 58 | Mar 22, 2025 |
| Ishaan Sharma | Support | Manager | Active | Pro | $36.00 | 3 | Sep 18, 2024 |
| Olivia Prabhakaran | Support | Lead | Invited | Team | $1,508.00 | 52 | Jan 24, 2025 |
| Sofia Reddy | Platform | Manager | Churned | Free | $0.00 | 1 | Oct 12, 2023 |
| Liam Nair | Research | PM | Invited | Enterprise | $4,187.00 | 53 | May 28, 2025 |
| Ananya Menon | Finance | Director | Active | Free | $0.00 | 1 | Sep 26, 2026 |
Hide a column, resize one, sort, change the density, then reload the page: the view comes back. Save it under a name from the Views menu (0 saved). Stored under kadal:data-grid:v1:anon:docs-persistence.
import { DataGrid, createDataGridColumns, sessionStorageStorage, useDataGrid,} from "@/components/ui/data-grid";
import { makePeople, PLAN_OPTIONS, STATUS_OPTIONS, type Person,} from "@/lib/data-grid-demo-data";
const col = createDataGridColumns<Person>();
const columns = col.columns([ col.text("name", { header: "Name" }), col.text("team", { header: "Team" }), col.text("role", { header: "Role" }), col.enum("status", { header: "Status", options: STATUS_OPTIONS }), col.enum("plan", { header: "Plan", options: PLAN_OPTIONS }), col.currency("mrr", { header: "MRR", currency: "USD" }), col.number("seats", { header: "Seats" }), col.date("joinedAt", { header: "Joined" }),]);
const data = makePeople(60, 39);
/** The view (columns, sort, sizes, pinning, order, grouping, density, page size) survives a reload. */export default function DataGridPersistence() { const grid = useDataGrid<Person>({ name: "persistence", data, columns, getRowId: (r) => r.id, pagination: { pageSize: 8 }, // sessionStorage: the view lasts this tab and no longer. Leave `storage` out for localStorage. persist: { key: "docs-persistence", storage: sessionStorageStorage }, }); return ( <div className="flex flex-col gap-3"> <DataGrid grid={grid} /> <p className="text-sm text-muted-foreground"> Hide a column, resize one, sort, change the density, then reload the page: the view comes back. Save it under a name from the <strong>Views</strong> menu ({grid.views.saved.length}{" "} saved). Stored under{" "} <code className="rounded bg-muted px-1 py-0.5 font-mono text-xs"> kadal:data-grid:v1:anon:docs-persistence </code> . </p> </div> );}Use it
Section titled “Use it”// Under the grid's key, in localStorage.useDataGrid({ name: "customers", data, columns, persist: true })
// Under a key of your own.<DataGrid data={data} columns={columns} persist="customers" />
// Everything configurable.useDataGrid({ data, columns, persist: { name: "customers", scope: user.id, // one view per signed-in user on a shared browser storage: sessionStorage, // see below; default localStorage savedViews: false, // keep the current view, drop the Views menu },})What is remembered
Section titled “What is remembered”The current view is a DataGridView:
| Field | What it holds |
|---|---|
sorting |
SortingState |
columnVisibility |
ColumnVisibilityState |
columnPinning |
{ start: string[], end: string[] } |
columnOrder |
ColumnOrderState |
columnSizing |
ColumnSizingState |
grouping |
GroupingState |
pageSize |
rows per page |
density |
"compact" | "comfortable" | "spacious" |
Deliberately not remembered: column filters, the search text, the page index, row and cell selection, expanded rows. They describe what the user is looking for right now, not how they like to look, and belong in the URL (URL state) or nowhere.
How it behaves
Section titled “How it behaves”- The stored view is read synchronously before the first render, so there is no flash of the defaults. Async storages land after mount and the view is applied then.
- Every change to the view is written 300 ms after the last one (resizing writes once, not per pixel).
- Whatever is in storage is sanitised: unknown fields are dropped, wrong types fall back to the
defaults, and anything that is not schema version
1is ignored. - Columns that no longer exist stay in the stored order and sizing harmlessly; TanStack ignores ids it does not know.
initialStateis the first visit and the Reset view target: its view slices (sorting,columnVisibility,columnPinning,columnOrder,columnSizing,grouping) seed the default view, and a stored view wins over them on later loads. The other slices (filters, search, expansion, selection) are not persisted and apply every time.
The key
Section titled “The key”Views are stored under kadal:data-grid:v1:<scope>:<key>, with anon as the scope when you give
none, so the example on this page is kadal:data-grid:v1:anon:docs-persistence. STORAGE_PREFIX
and storageKeyFor(key, scope) are exported if you need to read or clear it yourself. The record
is:
interface PersistedGridState { v: 1 current: DataGridView // the live view activeViewId: string | null // the saved view it was last applied from views: SavedView[] // { id, name, view, savedAt }}Storage
Section titled “Storage”storage is anything with this shape; sync or async, your choice:
interface DataGridStorage { get(key: string): PersistedGridState | null | Promise<PersistedGridState | null> set(key: string, state: PersistedGridState): void | Promise<void> remove(key: string): void | Promise<void>}Three come with the grid:
| Storage | For |
|---|---|
localStorageStorage (default) |
The same browser, forever. Private windows and blocked storage read as “nothing saved” and writes fail silently. |
sessionStorageStorage |
This tab, until it closes. |
memoryStorage() |
Tests, and grids that must never persist but still want the Views menu. |
A server-synced storage is a small adapter; the grid handles the promise:
import type { DataGridStorage } from "@/components/ui/data-grid"
export const serverStorage: DataGridStorage = { get: async (key) => { const res = await fetch(`/api/prefs/${encodeURIComponent(key)}`) return res.ok ? res.json() : null }, set: async (key, state) => { await fetch(`/api/prefs/${encodeURIComponent(key)}`, { method: "PUT", body: JSON.stringify(state) }) }, remove: async (key) => { await fetch(`/api/prefs/${encodeURIComponent(key)}`, { method: "DELETE" }) },}Pass it with scope: user.id so the key is per user even before the server checks the session.
Validate the body on the server with the same rules as sanitizeState if you store it as-is.
Saved views
Section titled “Saved views”With persistence on, the toolbar gains a Views menu (bookmark icon) naming the active view:
- Default view returns to the defaults and clears the active view.
- Each saved view applies on click; the active one is ticked.
- Save current view… opens a dialog for a name (blank becomes “View n”) and makes it active.
- Delete view removes the active view.
The same from code, on grid.views:
| Member | What it does |
|---|---|
saved |
SavedView[] |
activeId |
The applied view’s id, or null. |
current() |
A snapshot of the live view (what save would store). |
save(name) |
Stores the current view and makes it active; returns the SavedView. |
apply(id) |
Applies a saved view. |
rename(id, name) |
Renames one (there is no menu item for this). |
remove(id) |
Deletes one. |
reset() |
Back to the defaults, no active view. |
enabled |
true with persist. |
savedViewsEnabled |
enabled and savedViews !== false; the menu renders only then. |
Reset view
Section titled “Reset view”The View popover (columns) has a Reset view button. With persist it calls views.reset()
and restores every field above to its default: no sorting, all columns visible, nothing pinned,
natural order, default widths, no grouping, the pagination.pageSize, and defaultDensity
(falling back to initialState.density, then "comfortable"). Without persist the same button
only resets column visibility, order, sizing and pinning.
Options
Section titled “Options”persist: boolean | string | DataGridPersistOptions
| Option | Type | Default | What it does |
|---|---|---|---|
name |
string |
the grid name |
Storage key stem. |
storage |
DataGridStorage |
localStorageStorage |
Where the record lives. |
scope |
string | null |
"anon" |
Namespaces the key, usually the user id. |
savedViews |
boolean |
true |
The Views menu. false still persists the current view. |
See also
Section titled “See also”- URL state: the other half, for filters, search and the page.
- Density and Column visibility: two of the things a view holds.
- Composition: placing
<DataGridViewsMenu>yourself.