Updating Data in Real-Time
Real-Time updates of data are possible via the DataSource API.
In this page we explain some of the complexities and features involved.
Getting a reference to the DataSource API
Note
Data Updates are related to the DataSource
component, therefore make sure you use the DataSource API for this.
You can get a reference to the DataSource API
- either by using the DataSource onReady prop
const onReady = (dataSourceApi) => {
// do something with the dataSourceApi
};
<DataSource onReady={onReady} />;
- or by using the InfiniteTable onReady prop.
const onReady = ({ api, dataSourceApi }) => {
// note for InfiniteTable.onReady, you get back an object
// with both the InfiniteTable API (the `api` property)
// and the DataSource API (the `dataSourceApi` property)
}
<DataSource {...}>
<InfiniteTable onReady={onReady}/>
</DataSource>
Updating Rows
To update the data of a row, you need to know the primaryKey
for that row and use the updateData
method of the DataSource API.
dataSourceApi.updateData({
// if the primaryKey is the "id" field, make sure to include it
id: 1,
// and then include any properties you want to update - in this case, the name and age
name: 'Bob Blue',
age: 35,
});
To update multiple rows, you need to pass the array of data items to the updateDataArray
method.
dataSourceApi.updateDataArray([
{
id: 1, // if the primaryKey is the "id" field, make sure to include it
name: 'Bob Blue',
age: 35,
},
{
id: 2, // primaryKey for this row
name: 'Alice Green',
age: 25,
},
]);
The DataSource has 10k items - use the Start/Stop button to see updates in real-time.
In this example, we're updating 5 rows (in the visible viewport) every 30ms.
The update rate could be much higher, but we're keeping it at current levels to make it easier to see the changes.
import * as React from 'react'; import { DataSourceApi, InfiniteTable, InfiniteTableApi, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import { DataSource } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; salary: number; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; age: number; reposCount: number; }; const dataSource = () => { return fetch(`${'https://infinite-table.com/.netlify/functions/json-server'}/developers10k-sql`) .then((r) => r.json()) .then((data: Developer[]) => { return data; }); }; export function getRandomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } const CURRENCIES = ['USD', 'CAD', 'EUR']; const stacks = ['frontend', 'backend', 'fullstack']; const updateRow = (api: DataSourceApi<Developer>, data: Developer) => { const getDelta = (num: number): number => Math.ceil(0.2 * num); const initialData = data; if (!initialData) { return; } const salaryDelta = getDelta(initialData?.salary); const reposCountDelta = getDelta(initialData?.reposCount); const newSalary = initialData.salary + getRandomInt(-salaryDelta, salaryDelta); const newReposCount = initialData.reposCount + getRandomInt(-reposCountDelta, reposCountDelta); const newData: Partial<Developer> = { id: initialData.id, salary: newSalary, reposCount: newReposCount, currency: CURRENCIES[getRandomInt(0, CURRENCIES.length - 1)] || CURRENCIES[0], stack: stacks[getRandomInt(0, stacks.length - 1)] || stacks[0], age: getRandomInt(0, 100), }; api.updateData(newData); }; const ROWS_TO_UPDATE_PER_FRAME = 5; const UPDATE_INTERVAL_MS = 30; const columns: InfiniteTablePropColumns<Developer> = { firstName: { field: 'firstName', }, age: { field: 'age', type: 'number', style: ({ value, rowInfo }) => { if (rowInfo.isGroupRow) { return {}; } return { color: 'black', background: value > 80 ? 'tomato' : value > 60 ? 'orange' : value > 40 ? 'yellow' : value > 20 ? 'lightgreen' : 'green', }; }, }, salary: { field: 'salary', type: 'number', }, reposCount: { field: 'reposCount', type: 'number', }, stack: { field: 'stack', renderMenuIcon: false }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; export default function App() { const [running, setRunning] = React.useState(false); const [apis, onReady] = React.useState<{ api: InfiniteTableApi<Developer>; dataSourceApi: DataSourceApi<Developer>; }>(); const intervalIdRef = React.useRef<any>(); React.useEffect(() => { const { current: intervalId } = intervalIdRef; if (!running || !apis) { return clearInterval(intervalId); } intervalIdRef.current = setInterval(() => { const { dataSourceApi, api } = apis!; const { renderStartIndex, renderEndIndex } = api.getVerticalRenderRange(); const dataArray = dataSourceApi.getRowInfoArray(); const data = dataArray .slice(renderStartIndex, renderEndIndex) .map((x) => x.data as Developer); for (let i = 0; i < ROWS_TO_UPDATE_PER_FRAME; i++) { const row = data[getRandomInt(0, data.length - 1)]; if (row) { updateRow(dataSourceApi, row); } } return () => { clearInterval(intervalIdRef.current); intervalIdRef.current = null; }; }, UPDATE_INTERVAL_MS); }, [running, apis]); return ( <React.StrictMode> <button style={{ border: '2px solid var(--infinite-cell-color)', borderRadius: 10, padding: 10, background: running ? 'tomato' : 'var(--infinite-background)', color: running ? 'white' : 'var(--infinite-cell-color)', margin: 10, }} onClick={() => { setRunning(!running); }} > {running ? 'Stop' : 'Start'} updates </button> <DataSource<Developer> data={dataSource} primaryKey="id"> <InfiniteTable<Developer> domProps={domProps} onReady={onReady} columnDefaultWidth={130} columnMinWidth={50} columns={columns} /> </DataSource> </React.StrictMode>
Note
For updating multiple rows, use the updateDataArray
method.
When updating a row, the data object you pass to the updateData
method needs to at least include the primaryKey
field. Besides that field, it can include any number of properties you want to update for the specific row.
Batching updates
All the methods for updating/inserting/deleting rows exposed via the DataSource API are batched by default. So you can call multiple methods on the same raf (requestAnimationFrame), and they will trigger a single render.
All the function calls made in the same raf return the same promise, which is resolved when the data is persisted to the DataSource
const promise1 = dataSourceApi.updateData({
id: 1,
name: 'Bob Blue',
});
const promise2 = dataSourceApi.updateDataArray([
{ id: 2, name: 'Alice Green' },
{ id: 3, name: 'John Red' },
]);
promise1 === promise2; // true
Inserting Rows
To insert a new row into the DataSource
, you need to use the insertData
method. For inserting multiple rows at once, use the insertDataArray
method.
dataSourceApi.insertData(
{
id: 10,
name: 'Bob Blue',
age: 35,
salary: 12_000,
stack: 'frontend',
//...
},
{
position: 'before',
primaryKey: 2,
},
);
When you insert new data, as a second parameter, you have to provide an object that specifies the insert position
.
Valid values for the insert position
are:
start
|end
- inserts the data at the beginning or end of the data source. In this case, noprimaryKey
is needed.
dataSourceApi.insertData({ ... }, { position: 'start'})
// or insert multiple items via
dataSourceApi.insertDataArray([{ ... }, { ... }], { position: 'start'})
before
|after
- inserts the data before or after the data item that has the specified primary key. In thise case, theprimaryKey
is required.
dataSourceApi.insertData(
{ /* ... all data properties here */ },
{
position: 'before',
primaryKey: 2
}
)
// or insert multiple items via
dataSourceApi.insertDataArray([{ ... }, { ... }], {
position: 'after',
primaryKey: 10
})
Click any row in the table to make it the current active row, and then use the second button to add a new row after the active row.
import * as React from 'react'; import { DataSourceApi, InfiniteTable, InfiniteTableApi, InfiniteTablePropColumns, } from '@infinite-table/infinite-react'; import { DataSource } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; currency: string; salary: number; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; age: number; reposCount: number; }; export function getRandomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } const CURRENCIES = ['USD', 'CAD', 'EUR']; const stacks = ['frontend', 'backend', 'fullstack']; let ID = 0; const firstNames = ['John', 'Jane', 'Bob', 'Alice', 'Mike', 'Molly']; const lastNames = ['Smith', 'Doe', 'Johnson', 'Williams', 'Brown', 'Jones']; const getRow = (count?: number): Developer => { return { id: ID++, firstName: ID === 1 ? 'ROCKY' : firstNames[getRandomInt(0, firstNames.length - 1)] + (count ? ` ${count}` : ''), lastName: lastNames[getRandomInt(0, firstNames.length - 1)], currency: CURRENCIES[getRandomInt(0, 2)], salary: getRandomInt(1000, 10000), preferredLanguage: 'JavaScript', stack: stacks[getRandomInt(0, 2)], canDesign: getRandomInt(0, 1) === 0 ? 'yes' : 'no', age: getRandomInt(20, 100), reposCount: getRandomInt(0, 100), }; }; const dataSource: Developer[] = [...Array(10)].map(getRow); const columns: InfiniteTablePropColumns<Developer> = { firstName: { field: 'firstName', }, age: { field: 'age', type: 'number', style: ({ value, rowInfo }) => { if (rowInfo.isGroupRow) { return {}; } return { color: 'black', background: value > 80 ? 'tomato' : value > 60 ? 'orange' : value > 40 ? 'yellow' : value > 20 ? 'lightgreen' : 'green', }; }, }, salary: { field: 'salary', type: 'number', }, reposCount: { field: 'reposCount', type: 'number', }, stack: { field: 'stack', renderMenuIcon: false }, currency: { field: 'currency' }, }; const domProps = { style: { height: '100%', }, }; const buttonStyle = { border: '2px solid magenta', color: 'var(--infinite-cell-color)', background: 'var(--infinite-background)', }; export default () => { const [apis, onReady] = React.useState<{ api: InfiniteTableApi<Developer>; dataSourceApi: DataSourceApi<Developer>; }>(); const [currentActivePrimaryKey, setCurrentActivePrimaryKey] = React.useState<string>(''); return ( <React.StrictMode> <button style={buttonStyle} onClick={() => { if (apis) { const dataSourceApi = apis.dataSourceApi!; dataSourceApi.insertData( getRow(dataSourceApi.getRowInfoArray().length), { primaryKey: 0, position: 'after', }, ); } }} > Add row after Rocky </button> <button style={buttonStyle} disabled={!currentActivePrimaryKey} onClick={() => { if (apis) { const dataSourceApi = apis.dataSourceApi!; dataSourceApi.insertData( getRow(dataSourceApi.getRowInfoArray().length), { primaryKey: currentActivePrimaryKey, position: 'after', }, ); } }} > Add row after currently active row </button> <DataSource<Developer> data={dataSource} primaryKey="id"> <InfiniteTable<Developer> domProps={domProps} onReady={onReady} columnDefaultWidth={130} columnMinWidth={50} columns={columns} keyboardNavigation="row" onActiveRowIndexChange={(rowIndex) => { if (apis) { const id = apis.dataSourceApi.getRowInfoArray()[rowIndex].id; setCurrentActivePrimaryKey(id); } }} /> </DataSource> </React.StrictMode>
Adding rows
In addition to the insertData
and insertDataArray
methods, the DataSource
also exposes the addData
and addDataArray
methods (same as insert with position=end
).
Deleting Rows
To delete rows from the DataSource
you either need to know the primaryKey
for the row you want to delete, or you can pass the data object (or at least a partial that contains the primaryKey
) for the row you want to delete.
All the following methods are available via the DataSource API: