Real-time updates
Update rows by id from WebSockets, server-sent events or polling; only changed rows repaint, numbers flash up or down, and connectLive reconnects and pauses.
Live demoLive data
Rows updated by id from a feed: only changed rows repaint, numbers flash green or red, and the feed can pause.
Code for this demo
import { createGrid, type Row } from '@freegridjs/core';
import { button, separator, text, toggle, tr, type DemoContext } from '../ui';
export default function mount(ctx: DemoContext) {
const { lang } = ctx;
const h = (en: string, ko: string) => tr(lang, { en, ko });
const symbols = ['SAMSUNG', 'SK HYNIX', 'NAVER', 'KAKAO', 'HYUNDAI', 'LG CHEM', 'POSCO', 'KB FIN', 'CELLTRION', 'KIA', 'SHINHAN', 'COUPANG'];
const data: Row[] = symbols.map((name, i) => {
const price = 20000 + i * 13700;
return { id: name, name, price, change: 0, volume: 1000 + i * 120, history: [price, price, price, price, price, price, price, price] };
});
const grid = createGrid(ctx.host, {
columns: [
{ field: 'name', header: h('Symbol', '종목'), width: 140, pinned: 'left' },
{ field: 'price', header: h('Price', '현재가'), type: 'number', format: '#,##0', width: 120 },
{ field: 'change', header: h('Change', '등락률'), type: 'number', format: '0.00%', width: 110,
cellClass: ({ value }) => (Number(value) > 0 ? 'demo-up' : Number(value) < 0 ? 'demo-down' : undefined) },
{ field: 'history', header: h('Last moves', '최근 흐름'), width: 140, sparkline: { type: 'area' }, sortable: false, filter: false },
{ field: 'volume', header: h('Volume', '거래량'), type: 'number', format: '#,##0', width: 120 },
],
data, locale: lang, getRowId: r => String(r.id), rowNumbers: true,
});
let timer = 0;
const tick = () => {
// a pretend market feed: a few symbols move each tick
const updates = data.filter(() => Math.random() < 0.35).map(r => {
const price = Math.max(1000, Math.round(r.price * (1 + (Math.random() - 0.5) * 0.02)));
const base = r.history[0] as number;
return { id: r.id, price, change: (price - base) / base, volume: (r.volume as number) + Math.round(Math.random() * 50), history: [...(r.history as number[]).slice(1), price] };
});
grid.applyTransactionAsync({ update: updates });
};
grid.on('transactionApplied', e => ctx.log(`transactionApplied → ${h('updated', '갱신')} ${e.updated.length}`));
text(ctx, h('Prices arrive like a websocket feed. Only the changed rows are updated, and changed cells flash. Sorting by Change keeps up as prices move.',
'웹소켓처럼 시세가 들어옵니다. 바뀐 행만 갱신되고 바뀐 셀이 깜빡입니다. 등락률로 정렬해 두면 순위가 계속 바뀝니다.'));
separator(ctx);
const start = () => { timer = window.setInterval(tick, 700); };
toggle(ctx, h('Live feed', '실시간 수신'), true, on => {
clearInterval(timer);
if (on) start();
});
button(ctx, h('Add a symbol', '종목 추가'), () => {
const name = `NEW ${Math.floor(Math.random() * 900 + 100)}`;
const row = { id: name, name, price: 10000, change: 0, volume: 0, history: Array(8).fill(10000) };
data.push(row);
grid.applyTransaction({ add: [row], addIndex: 0 });
});
button(ctx, h('Remove the last', '마지막 종목 삭제'), () => {
const row = data.pop();
if (row) grid.applyTransaction({ remove: [row] });
});
start();
// stop when the page goes away
window.addEventListener('pagehide', () => clearInterval(timer), { once: true });
return grid;
}For screens where data keeps changing (prices, order boards, monitoring), send only the rows that changed instead of replacing the data. Rows are found by getRowId and updated in place; when no sort, filter or group reads the changed fields, the grid keeps the order and repaints only those rows. Numbers flash green going up and red going down. The cell being edited, the selection and the scroll position stay where they are.
Transactions: send only what changed
applyTransaction adds, updates and removes rows in one go and returns what happened (added, updated, removed, notFound). Server changes are not edits: nothing goes into undo or getModifiedRows. For a fast stream, applyTransactionAsync batches updates into one render.
const grid = createGrid(el, { columns, data, getRowId: r => r.id });
// send only the rows that changed, by id; unknown ids come back in notFound
grid.applyTransaction({
update: [{ id: 'A100', price: 15200 }],
add: [{ id: 'A200', name: 'New', price: 9800 }],
remove: [{ id: 'A050' }],
upsert: [{ id: 'A300', price: 1200 }], // update if it exists, add if not
});
// hundreds of messages a second: batch them into one render (every 50 ms)
socket.onmessage = e => grid.applyTransactionAsync(JSON.parse(e.data));connectLive: WebSocket, server-sent events or polling
connectLive connects, reads messages, reconnects after a drop and pauses. A message that is an array of rows is an upsert, { add, update, remove, upsert } a transaction, and { rows } replaces all rows.
import { connectLive } from '@freegridjs/core';
// a WebSocket
const live = connectLive(grid, { url: 'wss://example.com/prices' });
// server-sent events, with your own message format
connectLive(grid, {
url: '/api/orders/stream',
parse: msg => ({ upsert: JSON.parse(msg).items }),
onStatus: s => (statusEl.textContent = s), // connecting · open · paused · closed · error
});
// polling: ask the server every 3 seconds
connectLive(grid, { poll: () => fetch('/api/orders/changes').then(r => r.json()), interval: 3000 });
live.pause(); // hold updates while someone edits; they are kept
live.resume(); // and applied together here
live.close();| Name | Type | Description |
|---|---|---|
url | string | ws:// or wss:// opens a WebSocket; anything else server-sent events (EventSource). |
poll / interval | () => Promise<unknown> / number | Or ask your server every interval ms (default 2000). |
parse | (message) => RowTransaction | Row[] | { rows } | null | Turn your message format into a transaction. Default: JSON; an array is an upsert; { rows } replaces all rows. |
reconnect / maxDelay | boolean / number | Reconnect after a drop, waiting 0.5 s, 1 s, 2 s … up to maxDelay (default 30 s). |
immediate | boolean | Apply each message at once instead of batching (applyTransactionAsync). |
onStatus / onError | (status: 'connecting' | 'open' | 'paused' | 'closed' | 'error') => void | Show the connection state; onError gets messages that could not be read. |
Flashes and speed
createGrid(el, {
columns, data, getRowId: r => r.id,
flashCells: true, // flash changed cells (default true)
flashDuration: 600, // ms (default 1000)
asyncTransactionWait: 100, // how long applyTransactionAsync collects, ms (default 50)
});
// colours are CSS variables
// .fg-root { --fg-up: #16a34a; --fg-down: #dc2626; }When a sorted column changes the order is fixed up, and when a filtered column changes the rows are filtered again. Changes to any other column keep the order and repaint just those rows, so frequent updates stay light even on large grids.
Sending from the server
// Node: push changed rows over a WebSocket (ws package)
wss.clients.forEach(c => c.send(JSON.stringify({ update: changedRows })));
// Spring Boot: server-sent events
@GetMapping(path = "/api/orders/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream() { SseEmitter e = new SseEmitter(0L); emitters.add(e); return e; }
// when something changes: emitter.send(Map.of("upsert", rows));Questions about this page
How do I update a grid in real time from a WebSocket?
Give the grid getRowId and call connectLive(grid, { url: "wss://…" }), or call applyTransactionAsync({ update: rows }) for each message. Only the changed rows repaint.
Do live updates lose the user’s selection or scroll position?
No. Rows are updated in place by id, so the selection, the scroll position and a cell being edited stay where they are.