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

const onReady = (dataSourceApi) => {
  // do something with the dataSourceApi
};

<DataSource onReady={onReady} />;
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.

Updating_a_single_row_using_dataSourceApi.updateData
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.

Updating_multiple_rows
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,
  },
]);
Live data updates with DataSourceApi.updateData

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.

View Mode
Fork
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

Updates_made_on_the_same_raf_are_batched_together
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.

Inserting_a_single_row
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, no primaryKey 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, the primaryKey is required.
dataSourceApi.insertData(
  { /* ... all data properties here */ },
  {
    position: 'before',
    primaryKey: 2
  }
)
// or insert multiple items via
dataSourceApi.insertDataArray([{ ... }, { ... }], {
  position: 'after',
  primaryKey: 10
})
Using dataSourceApi.insertData

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.

View Mode
Fork
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: