State Persistence
Everything the user can rearrange goes into one versioned, JSON-serializable snapshot: column order, widths, visibility and pinning, plus density and whatever each feature decides to contribute.
Persisting to localStorage
Rearrange the grid below: drag a column, resize one, hide one from the chooser, sort, filter or change the page size. Then reload the page and it comes back the way you left it.
Loading the persisted layout...
nothing stored yet
<!-- Column layout, sort, filter, page size and density are mirrored into
localStorage and restored before the first client paint. -->
<DataGrid {grid} persistState={{ key: 'orders-grid' }} />
<!-- With a migration for snapshots an older version of your app wrote.
Return undefined to discard one you cannot upgrade: the grid then
falls back to the column defaults rather than half-applying it. -->
<DataGrid
{grid}
persistState={{
key: 'orders-grid',
migrate: (stored) => (stored.version === 1 ? stored : undefined)
}}
/>Snapshots by Hand
getState and setState are what persistState calls. Because the snapshot is plain JSON, the same object can go to a server
as a saved view, or into a URL as a shareable one.
The two are always on grid.api,
whatever features are registered, because they belong to the kernel rather than to a
feature. Sort and filter the grid, save, change your mind, then restore.
// The same thing by hand, which is what persistState calls.
const snapshot = grid.api.getState();
grid.api.setState(snapshot);
// The snapshot is versioned and JSON-serializable, so it travels to a
// server or into a URL just as well as into localStorage.
await fetch('/api/views', { method: 'POST', body: JSON.stringify(snapshot) });
// SNAPSHOT_VERSION is what a migrate hook checks against.
import { SNAPSHOT_VERSION } from '@sv5ui/datagrid';What a Snapshot Holds
The object below is rebuilt as you work. Sort a column, filter, drag a column edge, move a column, hide one from the chooser, change the density: each lands in its own key. Identity is by id throughout, so a column or feature that disappeared is dropped on restore, and one added since keeps its defaults rather than inheriting someone else's.
{
"version": 1,
"features": {
"pagination": 5
}
}// What a snapshot holds. Everything is optional except version: a grid
// with nothing rearranged writes almost nothing.
{
version: 1,
columns: {
order: ['name', 'team', 'salary'],
widths: { name: 240 },
hidden: { country: true },
pinned: { name: 'left' },
collapsed: { pay: true } // header groups, by group id
},
density: 'compact',
features: {
sorting: [{ columnId: 'salary', direction: 'desc' }],
filtering: { quick: 'core', columns: {} },
pagination: 25
}
}
// Columns are keyed by id: an id that disappeared is dropped, an id added
// since keeps its defaults, and a column that appeared since the snapshot
// was written goes last rather than losing its place. Groups are keyed
// apart from columns, because a folded group is not a hidden column: what
// the chooser put away stays away when its group opens. Features are keyed by feature id, so one
// registered after the snapshot was written simply starts fresh.What Is Deliberately Left Out
Only three features write a slice: sorting, filtering and pagination, and pagination stores the page size rather than the page. Selection and edits are not layout, so they are not in the snapshot at all.
Try it: sort a column and tick a row or two, save, then clear the sort and untick
everything. Restoring brings the sort back and leaves the selection where you left it. A
feature decides its own slice through serialize, so your own features join the snapshot the same way the built-ins do.
// What a feature contributes is its own decision, through serialize.
// sorting the sort array, when there is one
// filtering the whole filter model, when anything is active
// pagination the page size only
// The page number is deliberately absent: restoring page 7 of a list the
// user has since filtered lands them nowhere. The selection and the edit
// history are absent for the same reason, being about a session rather
// than about a view.Rendering Client-Side
There is no grid to show here, because the point is the absence of one: on the server there
is no localStorage, so a persisted grid rendered there paints its defaults and corrects itself after
hydration, which is a visible flash. Gate the grid behind a mounted flag and it paints once.
This page keeps its own SSR and gates only the grid, which is the smaller hammer of the two. The first demo on this page is behind exactly this flag, and it currently reads server render, grid withheld .
// localStorage is a browser thing, so a server-rendered grid paints the
// defaults and corrects after hydration, which reads as a flash.
// Render a persisted grid client-side to avoid it:
export const ssr = false;
// Or gate the grid itself, which keeps the rest of the page server-rendered:
let mounted = $state(false);
$effect(() => { mounted = true; });GridSnapshot
| Field | Description |
|---|---|
version | SNAPSHOT_VERSION at the time it was written |
columns.order | Column ids in their current order |
columns.widths | Widths a resize changed |
columns.hidden | What the chooser hid |
columns.pinned | Pin sides |
columns.collapsed | Folded header groups, keyed by the group id rather than by column, since no column carries it |
density | Written only when it is not standard |
features | One slice per feature, keyed by feature id |
Feature Slices
What each built-in feature writes into snapshot.features.
| Feature | Contributes |
|---|---|
sorting | The sort array, when there is one |
filtering | The whole filter model, when anything is active. Values are stored in the row's own units, so a percent column restores the 0.05 behind a panel that read 5%, and a set filter holds JSON-safe keys, so the values that come back are the ones that went in |
pagination | The page size only, never the page number |
Everything else | Selection, editing history and row pinning stay out: they are about a session rather than a view |