Real-time / streaming updates
How to drive the grid from a WebSocket / SSE / poll. Three patterns ranked by the rate of change:
- Periodic full refresh - poll for the latest rows, swap the array.
- Cell flash on change - the same swap, but the renderer highlights cells whose values just changed.
- Delta merge with backlog - WebSocket pushes individual row patches; you merge them into the in-memory state, optionally batching while the user has the page paused.
Try a streaming order desk - cell flashes on every change, pause / resume, disconnect / reconnect, configurable throughput slider:
Open the live example: Real-time / streaming (Real-time & Streaming)
Pattern 1: periodic full refresh
The simplest reactive pattern. Poll every N seconds, hand the new array down.
Every example below runs against this setup:
<script lang="ts">
import { SvGrid, tableFeatures, rowSortingFeature, type ColumnDef } from '@svgrid/grid'
type Order = { id: string; symbol: string; qty: number; price: number; updatedAt: number }
const features = tableFeatures({ rowSortingFeature })
const fmtMoney = (n: number) =>
n.toLocaleString('en-US', { style: 'currency', currency: 'USD' })
let rows = $state<Order[]>([
{ id: 'o1', symbol: 'AAPL', qty: 120, price: 231.4, updatedAt: Date.now() },
{ id: 'o2', symbol: 'MSFT', qty: 80, price: 418.9, updatedAt: Date.now() },
{ id: 'o3', symbol: 'NVDA', qty: 45, price: 902.1, updatedAt: Date.now() },
])
const data = rows
const columns: ColumnDef<typeof features, Order>[] = [
{ field: 'symbol', header: 'Symbol', width: 110 },
{ field: 'qty', header: 'Qty', width: 90 },
{ field: 'price', header: 'Price', width: 120, format: { type: 'currency', currency: 'USD' } },
]
</script>
<script lang="ts">
let rows = $state<Order[]>([])
$effect(() => {
const id = setInterval(async () => {
rows = await fetch('/api/orders').then((r) => r.json())
}, 5_000)
return () => clearInterval(id)
})
</script>
<SvGrid data={rows} columns={columns} features={features} />
The grid re-renders the visible rows; virtualization keeps the cost proportional to the viewport, not the dataset.
Use when: dataset is small (< 1000 rows), update rate is low (≥ 5 s), "freshness" is the only requirement.
Avoid when: the user is mid-edit on a cell. A full swap mid-edit
will close the editor. Pause your refresh while
onActiveCellChange reports a non-null active cell.
Pattern 2: cell flash on change
Same swap, but each cell snippet tracks its previous value and renders a brief highlight when it differs.
<script lang="ts">
// Track per-row, per-field last-seen values.
let lastSeen = new Map<string, Record<string, unknown>>()
function diff(rowId: string, current: Record<string, unknown>): Record<string, boolean> {
const prev = lastSeen.get(rowId)
const changed: Record<string, boolean> = {}
if (prev) for (const k of Object.keys(current)) if (prev[k] !== current[k]) changed[k] = true
lastSeen.set(rowId, { ...current })
return changed
}
</script>
{#snippet PriceCell(props: { row: Order })}
{@const changes = diff(props.row.id, props.row)}
<span class={`tabular-nums ${changes.price ? 'flash' : ''}`}>
{fmtMoney(props.row.price)}
</span>
{/snippet}
.flash {
animation: flash-bg 800ms ease-out;
}
@keyframes flash-bg {
from { background: rgba(250, 204, 21, 0.4); }
to { background: transparent; }
}
Use when: user wants to spot changes. The stock-ticker pattern.
Avoid when: the flash interferes with selection or accessibility -
add prefers-reduced-motion guards if the flash is purely
decorative.
Pattern 3: delta merge with backlog
The grown-up pattern for higher-rate updates. The server pushes individual row patches; you maintain an in-memory map and reassign the array when needed.
<script lang="ts">
type OrderId = string
let rowsMap = $state(new Map<OrderId, Order>())
let paused = $state(false)
let backlog = $state<Order[]>([])
const flushDebounce = 200 // ms
const rows = $derived(Array.from(rowsMap.values()))
$effect(() => {
const ws = new WebSocket('/api/orders/stream')
ws.onmessage = (e) => {
const patch: Order = JSON.parse(e.data)
if (paused) {
backlog = [...backlog, patch]
return
}
applyPatch(patch)
}
return () => ws.close()
})
function applyPatch(patch: Order) {
const next = new Map(rowsMap)
next.set(patch.id, patch)
rowsMap = next
}
function resume() {
paused = false
for (const p of backlog) applyPatch(p)
backlog = []
}
</script>
<button onclick={() => (paused = !paused)}>
{paused ? `Resume (${backlog.length} pending)` : 'Pause'}
</button>
<SvGrid data={rows} columns={columns} features={features} />
A few production knobs:
Debounce the resignment. A WebSocket spitting out 20 patches per second creates 20 array allocations per second. Batch into the next
requestAnimationFrame:let pendingPatches: Order[] = [] let scheduled = false function applyPatch(patch: Order) { pendingPatches.push(patch) if (scheduled) return scheduled = true requestAnimationFrame(() => { const next = new Map(rowsMap) for (const p of pendingPatches) next.set(p.id, p) rowsMap = next pendingPatches = [] scheduled = false }) }Out-of-order safety. Each patch carries a server-side sequence number; drop patches older than the latest you've applied for that row. The streaming demo (#34) shows this with
lastSeqper row.Disconnect handling. On
ws.onclose, set a status flag the user sees ("reconnecting…") and reconnect with exponential backoff. The demo shows the recovery flow.
Pause during edit / selection
A common mistake: updates fly in while the user is mid-paste or mid-edit. The fix is two flags:
let isEditing = $state(false)
let hasSelection = $state(false)
const isInteracting = $derived(isEditing || hasSelection)
$effect(() => {
if (isInteracting) {
paused = true
} else if (paused) {
resume()
}
})
Wire to the grid:
<SvGrid
...
onCellDoubleClick={() => (isEditing = true)}
onCellValueChange={() => (isEditing = false)}
onRowSelectionChange={(selection, rows) => (hasSelection = rows.length > 0)}
/>
onRowSelectionChange receives two positional arguments - the
{ [rowId]: true } record and the array of selected rows.
There is no dedicated editing-state callback today, so the edit flag is
assembled from the two ends of the edit: a double-click opens the
editor, and a committed value closes it. That leaves one gap - an edit
abandoned with Escape commits nothing, so clear the flag on the
wrapper's onkeydown too:
<div onkeydown={(e) => { if (e.key === 'Escape') isEditing = false }}>
<SvGrid ... />
</div>
If your app starts edits itself through api.startEditing() /
api.stopEditing(), set the flag at those call sites instead - you
already know the state there, and it covers every exit path.
Backpressure (when the server is too fast)
If your producer can outrun the UI's frame budget, sample down at the client. Drop intermediate patches for the same row; only the newest one survives:
function applyPatch(patch: Order) {
// pendingPatches is keyed by row id so a fast-moving row only
// commits its NEWEST value per frame.
pendingPatches.set(patch.id, patch)
/* ...schedule rAF... */
}
For 1000 rows updating at 5 Hz, this caps the work at 1000 assignments per frame regardless of the actual message rate. The streaming demo's "throughput slider" stresses this exact path.
Combining with sort + filter
The grid's sort + filter run AFTER your patches land in rows. Two
implications:
- A row that no longer matches the filter disappears. Expected.
- The active row may move. When
onActiveCellChangefires with a new index because the row above shifted, your toolbar / detail panel should follow.
If you're using a side detail panel keyed on row id (not index), this is a non-issue - the detail stays bound to the order even as the row moves.
Frequently asked questions
How do I show real-time data in SvGrid?
Drive the grid's data from a WebSocket, SSE, or poll. This page covers three
patterns ranked by update rate: periodic full refresh, targeted row patches via
getRowId, and high-frequency cell updates with change-flash highlighting.
Will the grid keep my scroll and selection on live updates?
Yes, if you give rows a stable identity with getRowId. Then selection,
expansion, edit state, and scroll position survive incoming updates instead of
resetting on every refresh.
How fast can SvGrid update?
Fast enough for tick-by-tick feeds - the stock-market demo updates 25 symbols every 250 ms with green/red cell flashes while sorting and selection stay live. For very high rates, patch only changed rows rather than swapping the whole array.
See also
- Server-side data - the pull side of remote data.
- Saved views - persist a "live mode on / off" toggle alongside the rest of the view config.
- Performance benchmarks - measured per-frame cost of the patterns above.
Related articles
- Real-Time Grids - Live WebSocket Updates in SvGrid - How to wire a high-frequency WebSocket feed into SvGrid without frame drops - covering stable row identity, RAF batching, and flash feedback done right.
- 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.