Transaction API (batched)
api.applyTransaction({ add, update, remove }) applies a batch of row mutations in ONE data update - the high-frequency streaming path. update and remove-by-id match on getRowId; remove also accepts row refs. Live order book ticking via batched transactions.
A live, editable Svelte 5 data grid example from the SvGrid gallery (Real-time & Streaming). See the SvGrid documentation for the full API.
About this example
Batched row mutations in the Svelte 5 data grid with api.applyTransaction({ add, update, remove }), the high-frequency streaming path. One call applies inserts, updates and removals in a single data update, far cheaper than addRow or setCellValue per row; update and remove match rows by getRowId and remove also accepts row references. The demo ticks a live order book through batched transactions.
api.applyTransaction({ add, update, remove }) applies a batch of row mutations in ONE data update - the high-frequency / streaming path. Far cheaper than calling addRow / setCellValue per row, and update / remove-by-id match on getRowId.
api.applyTransaction({ add: [newOrder], update: [{ ...order, price: next }], // matched by id remove: ['ORD-1001'], // by id (or row ref) })
Imports, features and API used
Imports: @svgrid/grid
Columns: id (Order), symbol (Symbol), side (Side), qty (Qty), price (Price), ts (Updated)
SvGridApi methods called: api.applyTransaction(), api.getData()
Frequently asked questions
When should I use applyTransaction instead of setting data?
When you have a stream of small changes. Replacing the whole array makes the grid diff every row; a transaction tells it exactly which rows were added, changed or removed, so a tick with fifty updates costs fifty row renders.
How are updated rows matched?
By the id from getRowId. Pass the updated row object in update and the grid replaces the row with that id; remove takes ids or row references.
What does the call return?
Counts of the rows actually added, updated and removed, which the demo accumulates into a running total; api.getData() reflects the new state immediately.
Related documentation
Related articles
- Building a Real-Time Trading Grid in Svelte - How to wire a WebSocket tick feed into SvGrid without dropping frames - rAF batching, stable row identity, flash animations, and the exact patterns that scale past 150 ticks per second.
- Building a Logistics / Fleet Tracking Grid in Svelte - How to build a live fleet operations grid with real-time telemetry updates, expandable trip history, and exception-first row styling.
- Building an IoT Sensor Dashboard in Svelte - How to build a live IoT sensor dashboard with SvGrid - high-frequency updates, sparkline trends, threshold alerts, and stale-device detection that all stay smooth at scale.
Source code (145-transaction-api.svelte)
<!-- Documented in: docs/help/rows/transactions.md -->
<script lang="ts">
/**
* 145. Transaction API
* --------------------
* `api.applyTransaction({ add, update, remove })` applies a batch of row
* mutations in ONE data update - the high-frequency / streaming path. Far
* cheaper than calling addRow / setCellValue per row, and `update` /
* `remove`-by-id match on `getRowId`.
*
* api.applyTransaction({
* add: [newOrder],
* update: [{ ...order, price: next }], // matched by id
* remove: ['ORD-1001'], // by id (or row ref)
* })
*/
import { SvGrid, tableFeatures, type GridColumns, type SvGridApi } from '@svgrid/grid'
const features = tableFeatures({})
type Order = { id: string; symbol: string; side: 'BUY' | 'SELL'; qty: number; price: number; ts: string }
const SYMBOLS = ['AAPL', 'MSFT', 'NVDA', 'AMZN', 'TSLA', 'META', 'GOOG']
let nextId = 1000
let seed = 0x1234
const rnd = () => ((seed = (seed * 1103515245 + 12345) >>> 0) / 0xffffffff)
function newOrder(): Order {
return {
id: `ORD-${nextId++}`,
symbol: SYMBOLS[Math.floor(rnd() * SYMBOLS.length)]!,
side: rnd() > 0.5 ? 'BUY' : 'SELL',
qty: Math.round(10 + rnd() * 990),
price: Math.round((50 + rnd() * 450) * 100) / 100,
ts: new Date().toLocaleTimeString(),
}
}
const seedRows: Order[] = Array.from({ length: 40 }, newOrder)
let api = $state<SvGridApi<typeof features, Order> | null>(null)
let running = $state(true)
let applied = $state({ added: 0, updated: 0, removed: 0 })
let liveRows = $state(seedRows.length)
const columns: GridColumns<Order> = [
{ field: 'id', header: 'Order', width: 120 },
{ field: 'symbol', header: 'Symbol', width: 100 },
{ field: 'side', header: 'Side', width: 90 },
{ field: 'qty', header: 'Qty', width: 100, align: 'right' },
{ field: 'price', header: 'Price', width: 120, align: 'right', format: { type: 'currency', currency: 'USD' } },
{ field: 'ts', header: 'Updated', width: 130 },
]
function tick() {
if (!api) return
// The grid owns the data after mount; read it back via the API.
const rows = api.getData()
// update prices on a handful of existing rows (matched by id)
const update = rows
.filter(() => rnd() < 0.12)
.slice(0, 6)
.map((o) => ({ ...o, price: Math.round(o.price * (0.96 + rnd() * 0.08) * 100) / 100, ts: new Date().toLocaleTimeString() }))
// add 1-2 new orders
const add = Array.from({ length: 1 + Math.floor(rnd() * 2) }, newOrder)
// remove a couple of the oldest once the book gets large
const remove = rows.length > 60 ? rows.slice(0, 2).map((o) => o.id) : []
const r = api.applyTransaction({ add, update, remove })
applied = {
added: applied.added + r.added,
updated: applied.updated + r.updated,
removed: applied.removed + r.removed,
}
liveRows = api.getData().length
}
$effect(() => {
if (!running) return
const h = setInterval(tick, 600)
return () => clearInterval(h)
})
</script>
<section class="flex flex-col flex-1 min-h-0 gap-3">
<div
class="shrink-0 rounded-lg border px-4 py-3"
style="border-color: var(--sg-border); background: var(--sg-header-bg);"
>
<p class="text-sm font-semibold" style="color: var(--sg-fg);">
Batched streaming via <code>api.applyTransaction</code>
</p>
<div class="mt-2 flex flex-wrap items-center gap-3 text-xs" style="color: var(--sg-muted);">
<button type="button" class="tx-btn" onclick={() => (running = !running)}>
{running ? 'Pause' : 'Resume'}
</button>
<button type="button" class="tx-btn" onclick={tick} disabled={running}>Step</button>
<span><strong style="color: var(--sg-fg)">{liveRows}</strong> live rows</span>
<span style="color: #16a34a">+{applied.added} added</span>
<span style="color: #2563eb">~{applied.updated} updated</span>
<span style="color: #dc2626">-{applied.removed} removed</span>
<span>ยท one re-render per batch, matched by <code>getRowId</code></span>
</div>
</div>
<div class="flex-1 min-h-0">
<SvGrid responsive={true}
columnResize
data={seedRows}
columns={columns}
features={features}
getRowId={(o) => o.id}
selectionMode="none"
rowHeight={32}
containerHeight="100%"
fitColumns={true}
onApiReady={(a) => (api = a)}
/>
</div>
</section>
<style>
.tx-btn {
padding: 4px 12px;
border: 1px solid var(--sg-border);
border-radius: 6px;
background: var(--sg-bg);
color: var(--sg-fg);
font-size: 12px;
cursor: pointer;
}
.tx-btn:disabled { opacity: 0.5; cursor: default; }
</style>More Real-time & Streaming examples
- Stock market - live - WebSocket-style ticking feed. Cells flash on up/down ticks, pause control, throttle.
- Industrial - IoT sensors - Live sensor floor: threshold-driven status, sparkline trends, group by line.
- Industrial dashboard - KPI cards plus live line-status and active-alarms grids, on a 2-second tick.
- Real-time / streaming - WebSocket-style live order stream with delta merge, out-of-order safety, pause / backlog, disconnect-reconnect, throughput slider.
- Real-time collaboration - Presence (who is here + where their cursor is) and live edits (a change in one client lands in every other) over a pluggable transport. createCollaboration + broadcastChannelTransport sync cursors and edits across tabs with zero backend; swap the transport for a WebSocket to go cross-machine. Also the substrate for multiple AI agents editing one grid.