Server transactions - Enterprise
A row was added on the server and the grid should show it now, not after a
refetch. A socket said a price moved. A row was deleted and its group should
lose it. Transactions apply those changes to the rows the
server-side row model already holds, in place, with
no request: the block cache of the level named by route is patched, the row
count follows, and the display list is rebuilt. A refresh is still the way to
bring in what the server knows and the grid does not; a transaction is for
what the app already knows. The demo below is a feed of changes the
server already made: price ticks patch loaded rows in place, new and
shipped orders arrive as batched transactions at their warehouse, and the
log shows the status of each.
Open the live example: Server transactions (live feed) (Server-Side Row Model)
One transaction
const result = ctl.applyTransaction({
route: ['EMEA', 'Germany'], // the level to change; omit for the top level
add: [newRow], // appended, or at `addIndex`
addIndex: 0,
update: [savedRow], // matched by id
remove: ['row-42', otherRow], // ids or rows
})
result.status // 'applied' | 'storeNotFound' | 'storeLoading' | 'storeWaitingToLoad' | 'storeLoadingFailed' | 'cancelled'
result.add // ids placed, result.update ids found and patched, result.remove ids found and removed
Rows are matched by getRowId at a leaf level and by the group column's
value at a group level - a group row is its key - so update: [{ region: 'EMEA', amount: 120 }] on the root patches the EMEA group's subtotal. The
grand-total row answers to the fixed id sv-grand-total and a group footer
to sv-group-total:<route>.
getRowId is required: a transaction with no way to name a row throws with
a clear message.
Rules worth knowing, each one a test:
- A
removeof a row outside the loaded blocks is ignored when the level's count is known - there is nothing loaded to remove - and the id is not inresult.remove. PassrowCountwhen you know the count changed anyway. - A route whose group has not been expanded is
storeNotFound. A transaction does not create levels; add the group row to the parent level instead. - A row added under a group inherits that group's selection state.
- Aggregates are not recomputed by a transaction. Call
refresh({ route: parentRoute })after a leaf edit when the subtotal must follow; the flagship demo does exactly that.
Async transactions
High-frequency updates - a feed, a socket - queue with
applyTransactionAsync(tx, callback) and apply in one batch every
asyncTransactionWaitMs (default 50 ms), each callback receiving its own
result. flushAsyncTransactions() applies the queue now, and
onAsyncTransactionsFlushed(results) fires once per batch.
socket.on('tick', (row) => ctl.applyTransactionAsync({ route: routeOf(row), update: [row] }))
isApplyTransaction(tx) is a veto: return false and the transaction
resolves as cancelled, which is how a refresh in flight discards updates
that predate it.
One row, no transaction
updateRowData(id, patch, { replace }) patches one loaded row on whatever
level holds it, without a request and without changing its id - the path for
a ticking cell. replace: true swaps the whole row instead of merging the
patch. It returns whether the row was found.
Editing goes through the server, then the transaction
createRow, updateRow and deleteRow on the model call the datasource's
method of the same name, then apply the saved row back as a transaction on
the route that holds it - no refetch of the block. Wire the grid's edit event
to updateRow, and refresh the parent level if a subtotal should follow:
<SvGrid
rowModel={ctl}
{columns}
editable
onCellValueChange={async (e) => {
const meta = e.row.__group
if (meta.kind !== 'leaf') return
await ctl.updateRow(String(e.row.id), { [e.columnId]: e.newValue })
ctl.refresh({ route: meta.route.slice(0, -1) })
}}
/>
Group rows do not take edits: the grid refuses them itself when a row model
marks a row as a group, so the handler only ever sees leaves. Mark the value
columns cellFlash: true and an updated cell flashes when the transaction
lands.
Delivering a whole level
applyRowData({ route, rows, rowCount, startRow }) fills a level's store
from data you already have - children that shipped with their parent, a
level pushed over a socket - bypassing the datasource, the debounce and the
concurrency cap.
More examples
Server-Side Row Model: 1,000,000 rows
Inline edits applied back as transactions with the subtotal following, add and delete, over a million server-side rows.
Open the live example: Server-Side Row Model: 1,000,000 rows (Server-Side Row Model)
Optimistic updates and rollback
The free controller's optimistic path: edit a cell, the row changes at once, the server validates, and a rejection rolls the row back.
Open the live example: Optimistic updates + rollback (Server-Side Data)
WebSocket live updates
Insert, update and delete deltas merged into the grid by id with cell flashes - the same shape a socket feeds applyTransactionAsync.
Open the live example: WebSocket live updates (Server-Side Data)
See also
- Server grouping - the model these transactions apply to.
- Server selection - what a new row inherits, and the bulk edit by rule.
- Server editing - the datasource's write methods.
Live examples
- Server transactions (live feed) - A socket-style feed of changes the server already made, applied without a refetch: a price tick patches the loaded row in place with a flash (updateRowData), a new order lands at the top of its warehouse and a shipped one leaves (applyTransactionAsync, batched every 500 ms, addressed by route). Every result carries a status the log shows: applied, cancelled under the veto hook, storeNotFound for a warehouse whose level is not cached. Refresh totals recomputes the sums a transaction leaves alone.
- Server-Side Row Model: 1,000,000 rows - One grid, one rowModel prop, a million rows that stay on the server. Sort, filter, global search, grouping to any depth (Region > Country > Rep), infinite scroll or paging, inline edits applied back as transactions with the subtotal following, add and delete, select-all across rows the grid never loaded with a bulk edit by rule, failed blocks with Retry, and a request log that shows every call to the columnar warehouse behind it. The row model ships in @svgrid/enterprise; the datasource contract is free.
- Optimistic updates + rollback - UI updates immediately; server validates async; on reject the value rolls back with a toast.
- WebSocket live updates - Insert / update / delete deltas merged by id, cell-flash on update, pause / resume, throughput slider.
Related articles
- Going AI-Native: The SvGrid MCP Server - Most AI assistants invent data grid APIs. We built an MCP server so they look up the real one instead.
- Using SvGrid with TanStack Query in Svelte - Wire TanStack Query's caching and background refetch into SvGrid for a server-driven grid that pages instantly and never shows a blank screen.
- A Svelte Data Grid with SvelteKit and Supabase - Wire SvGrid to a Supabase Postgres backend with server-side pagination, sorting, and filtering - keeping credentials on the server and queries fast with proper indexing.