Real-time / streaming updates

How to drive the grid from a WebSocket / SSE / poll. Three patterns ranked by the rate of change:

  1. Periodic full refresh - poll for the latest rows, swap the array.
  2. Cell flash on change - the same swap, but the renderer highlights cells whose values just changed.
  3. 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:

Pattern 1: periodic full refresh

The simplest reactive pattern. Poll every N seconds, hand the new array down.

<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:

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
  ...
  onEditingChange={(state) => (isEditing = state.cell != null)}
  onRowSelectionChange={({ selectedRows }) => (hasSelection = selectedRows.length > 0)}
/>

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:

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.

See also

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.