Skip to main content
Urbicon UI

Live Updates

Non-disruptive real-time data updates with buffering and user-controlled application.

Overview

Writing rows straight into a filtered, sorted, paged table is disorienting: rows jump position mid-read, disappear behind an active filter, or land in a selection the reader is still building. enableLiveUpdates buffers them instead. A banner above the rows counts what is waiting (“3 new, 2 updated, 1 deleted”) and the reader merges it with a click. By default the table also merges at the next sort, filter or page change, when the view is reorganizing anyway.

You push changes from any data source (WebSocket, SSE, polling) through three methods on the table context: pushInsert, pushUpdate and pushDelete. Rows are matched by item.id, and pushUpdate(id, changes) merges changes into the row, so a message carrying only { status } leaves the other fields alone.

Demo

The panel below plays the role of your backend. Push a few events and the LiveUpdateBanner appears above the rows, counting what is pending. Apply merges the buffer into the table, Dismiss drops it. The demo passes autoApplyOnNavigation={false} so pending changes always wait for your click.

Simulated server feed

Stand-in for a WebSocket/SSE handler: each button calls the corresponding push method.

Order
Customer
Status
Total
Placed
ORD-1008
Helix Health
pending
310
2026-07-13
ORD-1006
Fjord Analytics
shipped
1780
2026-07-12
ORD-1007
Granite & Co
pending
420
2026-07-12
ORD-1004
Delta Foods
pending
95
2026-07-11
ORD-1005
Ember Studio
paid
640
2026-07-11
ORD-1003
Cobalt Works
paid
2150
2026-07-10
ORD-1002
Baltic Trade
shipped
380
2026-07-09
ORD-1001
Aurora Labs
delivered
1240
2026-07-08

Enabling live updates

Enable live updates

One prop. When changes are pending, the LiveUpdateBanner renders automatically between the toolbar and the rows, with Apply and Dismiss actions.
<Table
  {items}
  {columns}
  enableLiveUpdates
/>

Push changes from your data source

onReady hands you the table context from outside the table, with pushInsert, pushUpdate and pushDelete on it. Wire them to whatever delivers your server events: WebSocket, SSE, or polling.
<script>
  import { Table, type TableContext } from '@urbicon-ui/table';

  let table = $state<TableContext | null>(null);

  $effect(() => {
    const ctx = table;
    if (!ctx) return;
    const socket = new WebSocket('wss://api.example.com/orders');
    socket.onmessage = (event) => {
      const message = JSON.parse(event.data);
      if (message.type === 'order:created') ctx.pushInsert(message.order);
      if (message.type === 'order:updated') ctx.pushUpdate(message.id, message.changes);
      if (message.type === 'order:deleted') ctx.pushDelete(message.id);
    };
    return () => socket.close();
  });
</script>

<Table
  {items}
  {columns}
  enableLiveUpdates
  onReady={(context) => (table = context)}
/>

Alternative: a feed component inside the table tree

If the feed is its own component it can call getTableContext(), which resolves through component context: it has to render inside the table, and the toolbar snippet is the natural mount point. Overriding toolbar replaces the default one, so re-add SmartFilterBar to keep the search field.
<script>
  // LiveFeed.svelte calls getTableContext(), so it must render inside the table
  import { Table, SmartFilterBar } from '@urbicon-ui/table';
  import LiveFeed from './LiveFeed.svelte';
</script>

<Table {items} {columns} enableLiveUpdates>
  {#snippet toolbar()}
    <LiveFeed />
    <SmartFilterBar />
  {/snippet}
</Table>

Buffer & merge semantics

The buffer holds at most one pending outcome per row, so a busy feed cannot make it grow past the number of rows it touched. A second pushInsert replaces the first, consecutive pushUpdate calls merge into one, a pushDelete for a row that is still a pending insert cancels both, and a pushUpdate for one is folded into the insert so the row lands already updated. Order of arrival does not matter.

Applying runs deletes, then updates, then inserts. A row that just took an update is highlighted for three seconds, so the change is findable in a long list, and a deleted row drops out of the selection along with the table. Updates and deletes are matched against all of items, not just the rows on screen, so a filter or a page never swallows one; an id that is nowhere in the data is skipped, with a warning in dev.

Auto-apply on navigation

With autoApplyOnNavigation (default true), pending changes are merged automatically when the user changes page, sort, filter or search: the view is reorganizing anyway, so merging at that moment is non-disruptive. Set it to false to make the banner the only way changes are applied.

Explicit apply only

Pending changes are never merged implicitly: the user clicks Apply, or you call applyAllUpdates() on the context. Its counterpart is dismissAllUpdates().
<Table
  {items}
  {columns}
  enableLiveUpdates
  autoApplyOnNavigation={false}
/>