Live Data
Stream real-time data mutations into the React grid with high-frequency updates, batched rendering, and stable scrolling during continuous insert flows.
This example demonstrates real-time data mutations using the useGridData hook, showcasing the transaction system's ability to handle high-frequency updates efficiently.
Interactive Demo
Try the live demo below. Click Start Stream to see real-time data insertion, or use the other controls to add/remove rows.
Overview
The demo simulates a live stock ticker that:
- Streams new data at configurable intervals
- Supports batch insertions
- Handles row removal operations
- Tracks row count in real-time via data source subscriptions
- Uses a custom cell renderer to colorize positive/negative changes
Full Example
import { useState, useCallback, useRef, useEffect } from "react";
import {
Grid,
useGridData,
type ColumnDefinition,
} from "@gp-grid/react";
interface StockTick {
id: number;
symbol: string;
price: number;
change: number;
volume: number;
timestamp: string;
}
const symbols = [
"AAPL", "GOOGL", "MSFT", "AMZN", "META", "TSLA", "NVDA", "AMD",
];
function getRandomPrice(): number {
return Math.round((Math.random() * 500 + 50) * 100) / 100;
}
function getRandomChange(): number {
return Math.round((Math.random() * 10 - 5) * 100) / 100;
}
function getRandomVolume(): number {
return Math.floor(Math.random() * 1000000) + 10000;
}
let nextId = 1;
function generateTick(): StockTick {
return {
id: nextId++,
symbol: symbols[Math.floor(Math.random() * symbols.length)],
price: getRandomPrice(),
change: getRandomChange(),
volume: getRandomVolume(),
timestamp: new Date().toISOString().slice(11, 23),
};
}
function generateInitialData(count: number): StockTick[] {
return Array.from({ length: count }, () => generateTick());
}
const columns: ColumnDefinition[] = [
{ field: "id", cellDataType: "number", width: 80, headerName: "ID" },
{ field: "symbol", cellDataType: "text", width: 100, headerName: "Symbol" },
{ field: "price", cellDataType: "number", width: 120, headerName: "Price" },
{ field: "change", cellDataType: "number", width: 100, headerName: "Change" },
{ field: "volume", cellDataType: "number", width: 120, headerName: "Volume" },
{ field: "timestamp", cellDataType: "text", width: 140, headerName: "Time" },
];
export function LiveDataDemo() {
const [rowCount, setRowCount] = useState(10);
const [isStreaming, setIsStreaming] = useState(false);
const [streamInterval, setStreamInterval] = useState(100);
const [batchSize, setBatchSize] = useState(10);
const streamingRef = useRef<ReturnType<typeof setInterval> | null>(null);
// Create mutable data source via useGridData hook
const { dataSource, addRows, removeRows } = useGridData<StockTick>(
generateInitialData(10),
{
getRowId: (row) => row.id,
debounceMs: 50,
},
);
// Subscribe to data changes to track row count
useEffect(() => {
const unsubscribe = dataSource.subscribe(() => {
setRowCount(dataSource.getTotalRowCount());
});
return () => unsubscribe();
}, [dataSource]);
// Add a single row
const handleAddRow = useCallback(() => {
addRows([generateTick()]);
}, [addRows]);
// Add multiple rows at once
const handleAddBatch = useCallback(() => {
const batch = Array.from({ length: batchSize }, () => generateTick());
addRows(batch);
}, [addRows, batchSize]);
// Remove the first row
const handleRemoveFirst = useCallback(async () => {
await dataSource.flushTransactions();
const count = dataSource.getTotalRowCount();
if (count > 0) {
const firstId = 1;
removeRows([firstId]);
}
}, [dataSource, removeRows]);
// Clear all rows
const handleClearAll = useCallback(async () => {
await dataSource.flushTransactions();
const count = dataSource.getTotalRowCount();
const ids = Array.from({ length: count }, (_, i) => i + 1);
removeRows(ids);
}, [dataSource, removeRows]);
// Toggle streaming mode
const toggleStreaming = useCallback(() => {
if (isStreaming) {
if (streamingRef.current) {
clearInterval(streamingRef.current);
streamingRef.current = null;
}
setIsStreaming(false);
} else {
streamingRef.current = setInterval(() => {
addRows([generateTick()]);
}, streamInterval);
setIsStreaming(true);
}
}, [isStreaming, streamInterval, addRows]);
return (
<div>
<div style={{ marginBottom: "16px", display: "flex", gap: "8px" }}>
<button onClick={handleAddRow}>Add Row</button>
<button onClick={handleAddBatch}>Add {batchSize} Rows</button>
<button onClick={toggleStreaming}>
{isStreaming ? "Stop" : "Start"} Stream
</button>
<button onClick={handleRemoveFirst}>Remove First</button>
<button onClick={handleClearAll}>Clear All</button>
</div>
<div style={{ marginBottom: "16px" }}>
<strong>Rows:</strong> {rowCount.toLocaleString()} |{" "}
<strong>Debounce:</strong> 50ms |{" "}
<strong>Stream:</strong> {isStreaming ? "Active" : "Inactive"}
</div>
<div style={{ width: "100%", height: "400px" }}>
<Grid<StockTick>
columns={columns}
dataSource={dataSource}
rowHeight={36}
/>
</div>
</div>
);
}Key Concepts
Custom Cell Renderer
The "Change" column uses a custom renderer to display positive values in green and negative values in red:
import { type CellRendererParams } from "@gp-grid/react";
const ChangeRenderer = (params: CellRendererParams) => {
const value = params.value as number;
const isPositive = value >= 0;
return (
<div
style={{
display: "flex",
alignItems: "center",
height: "100%",
padding: "0 8px",
color: isPositive ? "#22c55e" : "#ef4444",
}}
>
{isPositive ? "+" : ""}
{value.toFixed(2)}
</div>
);
};
// Reference the renderer in the column definition
const columns: ColumnDefinition[] = [
// ...other columns
{
field: "change",
cellDataType: "number",
width: 100,
headerName: "Change",
cellRenderer: "change",
},
];
// Pass the renderer to the Grid
<Grid
columns={columns}
dataSource={dataSource}
cellRenderers={{ change: ChangeRenderer }}
/>Subscribing to Data Changes
Use dataSource.subscribe to react to data mutations (adds, removes, updates):
const { dataSource } = useGridData(data, {
getRowId: (row) => row.id,
debounceMs: 50,
});
useEffect(() => {
const unsubscribe = dataSource.subscribe(() => {
// Fires after each batch of operations is processed
const totalRows = dataSource.getTotalRowCount();
setRowCount(totalRows);
});
return () => unsubscribe();
}, [dataSource]);Debouncing for Performance
The debounceMs option batches rapid operations:
// With debounceMs: 50, these three calls become one transaction
dataSource.addRows([tick1]);
dataSource.addRows([tick2]);
dataSource.addRows([tick3]);
// Grid updates once after 50ms with all 3 rowsStreaming Data
For continuous data streams, use setInterval with addRows:
const { addRows } = useGridData(data, { getRowId: (row) => row.id });
const intervalId = setInterval(() => {
addRows([generateNewData()]);
}, 100); // Add new row every 100ms
// Cleanup
clearInterval(intervalId);Flushing Transactions
Before operations that depend on current state, flush pending transactions:
const { dataSource, removeRows } = useGridData(data, {
getRowId: (row) => row.id,
});
async function removeAllRows() {
// Ensure all pending adds are processed first
await dataSource.flushTransactions();
const count = dataSource.getTotalRowCount();
const ids = getAllRowIds();
removeRows(ids);
}Performance Tips
-
Adjust debounce for your use case - Lower values (10-50ms) for real-time feel, higher values (100-500ms) for better batching.
-
Use batch operations -
addRows([row1, row2, row3])is more efficient than three separateaddRowscalls. -
Leverage subscriptions - Update derived state in
dataSource.subscriberather than polling. -
Clean up intervals - Always clear streaming intervals when components unmount:
useEffect(() => {
return () => {
if (streamingRef.current) {
clearInterval(streamingRef.current);
}
};
}, []);Custom Renderers
Render custom React components inside grid cells and headers, including badges, charts, action menus, and any other interactive UI you need to embed today.
Highlighting
Highlight rows, columns, and individual cells inside the React grid using conditional rules, hover styling, and custom CSS class overrides per cell today.