Getting Started

Infinite Table is a UI component for data virtualization - helps you display huge datasets of tabular data.

It's built specifically for React from the ground up and with performance in mind.

Installation

Infinite Table is available on the public npm registry - install it by running the following command:

Terminal
npm i @infinite-table/infinite-react

Meet the Code

This is an example to get you started with Infinite Table with minimal setup.

View Mode
Fork
import * as React from 'react';

import {
  InfiniteTable,
  DataSource,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';

type Person = {
  Id: number;
  FirstName: string;
  Age: number;
};

const columns: InfiniteTablePropColumns<Person> = {
  id: {
    // specifies which field from the data source
    // should be rendered in this column
    field: 'Id',
    type: 'number',
    defaultWidth: 80,
  },

  firstName: {
    field: 'FirstName',
    header: 'First Name',
  },
  age: { field: 'Age', type: 'number' },
};

const data: Person[] = [
  {
    Id: 1,
    FirstName: 'Bob',
    Age: 3,
  },
  {
    Id: 2,
    FirstName: 'Alice',
    Age: 50,
  },
  {
    Id: 3,
    FirstName: 'Bill',
    Age: 5,
  },
];

export default function App() {
  return (
    <DataSource<Person> data={data} primaryKey="Id">
      <InfiniteTable<Person> columns={columns} />
    </DataSource>

Using the Components

In the code snippet above, you notice we're using 2 components:

  • DataSource - this needs to be a parent (or ancestor, at any level) of the InfiniteTable component - it controls which data the table is rendering
  • InfiniteTable - the actual virtualized table component - needs to be inside a DataSource (can be at any level of nesting).

Both components are named exports of the @infinite-table/infinite-react package.

TypeScript Types

Our TypeScript types are published as part of the package, as named exports from the root of the package.

There are 2 components that you can use and import:

  • InfiniteTable
  • DataSource

Each of those has types provided for all the props it exposes, with the pattern of <COMPONENT_NAME>Prop<PROP_NAME>, so here are a few examples to clarify the rule:

import {
  InfiniteTablePropColumns, // or accessible as InfiniteTableProps['columns']
  // corresponding to the `columns` prop
  DataSourcePropGroupBy, // or accessible as DataSourceProps['groupBy']
  // corresponding to the `groupBy` prop
} from '@infinite-table/infinite-react';

Learn about our TypeScript typings

Read more about how to use our TypeScript types.

Built for React from the ground-up

Infinite Table is built specifically for React and is fully declarative and fully typed. When you use Infinite Table, it feels at-home in your React application - every prop has both a controlled and uncontrolled version so you get full control over every area of the component.

This is an example of how you might configure InfiniteTable in a real-world application and puts together several functionalities:

  • grouping
  • aggregation
  • pinned columns
  • sorting
  • multiple selection
  • custom cell rendering
View Mode
Fork
'use client';
import {
  InfiniteTable,
  DataSource,
  GroupRowsState,
  InfiniteTablePropColumnTypes,
  DataSourcePropRowSelection_MultiRow,
  InfiniteTableColumn,
  InfiniteTableColumnRenderValueParam,
  DataSourcePropAggregationReducers,
  DataSourceGroupBy,
  components,
  DataSourcePropFilterValue,
} from '@infinite-table/infinite-react';
import * as React from 'react';
import { useState } from 'react';

const { CheckBox } = components;

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const showLogo =
  typeof window === 'undefined'
    ? true
    : window.location.host.startsWith('localhost:') ||
      window.location.host.startsWith('infinite-table.com');

const avgReducer = {
  initialValue: 0,
  reducer: (acc: number, sum: number) => acc + sum,
  done: (value: number, arr: any[]) =>
    arr.length ? Math.floor(value / arr.length) : 0,
};
const aggregationReducers: DataSourcePropAggregationReducers<Developer> = {
  salary: {
    field: 'salary',

    ...avgReducer,
  },
  age: {
    field: 'age',
    ...avgReducer,
  },
  currency: {
    field: 'currency',
    initialValue: new Set<string>(),
    reducer: (acc: Set<string>, value: string) => {
      acc.add(value);
      return acc;
    },
    done: (value: Set<string>) => {
      return value.size > 1 ? 'Mixed' : value.values().next().value;
    },
  },
  canDesign: {
    field: 'canDesign',

    initialValue: false,
    reducer: (acc: boolean | null, value: 'yes' | 'no') => {
      if (acc === null) {
        return acc;
      }
      if (acc === false && value === 'yes') {
        return null;
      }
      if (acc === true && value === 'no') {
        return null;
      }
      return acc;
    },
  },
};

const flags = {
  'United States': '🇺🇸',
  Canada: '🇨🇦',
  France: '🇫🇷',
  Germany: '🇩🇪',
  'United Kingdom': '🇬🇧',
  'South Africa': '🇿🇦',
  'New Zealand': '🇳🇿',
  Sweden: '🇸🇪',
  China: '🇨🇳',
  Brazil: '🇧🇷',
  Turkey: '🇹🇷',
  Italy: '🇮🇹',
  India: '🇮🇳',
  Indonesia: '🇮🇩',
  Japan: '🇯🇵',
  Argentina: '🇦🇷',
  'Saudi Arabia': '🇸🇦',
  Mexico: '🇲🇽',
  'United Arab Emirates': '🇦🇪',
};
function getColumns(): Record<string, InfiniteTableColumn<Developer>> {
  return {
    age: {
      field: 'age',
      header: 'Age',
      type: 'number',
      defaultWidth: 100,
      renderValue: ({ value }) => value,
    },
    salary: {
      header: 'Compensation',
      field: 'salary',
      type: 'number',
      defaultWidth: 210,
    },
    currency: { field: 'currency', header: 'Currency', defaultWidth: 100 },
    preferredLanguage: {
      field: 'preferredLanguage',
      header: 'Programming Language',
    },

    canDesign: {
      defaultWidth: 135,
      field: 'canDesign',
      header: 'Design Skills',
      renderMenuIcon: false,
      renderValue: ({ value }) => {
        return (
          <div style={{ display: 'flex', alignItems: 'center' }}>
            <CheckBox
              defaultChecked={value === null ? null : value === 'yes'}
              domProps={{
                style: {
                  marginRight: 10,
                },
              }}
            />
            {value === null ? 'Some' : value === 'yes' ? 'Yes' : 'No'}
          </div>
        );
      },
    },
    country: {
      field: 'country',
      header: 'Country',
      renderValue: ({ value }) => {
        return (
          <span>
            {(flags as any)[value] || null} {value}
          </span>
        );
      },
    },
    firstName: { field: 'firstName', header: 'First Name' },
    stack: { field: 'stack', header: 'Stack' },

    city: { field: 'city', header: 'City' },
  };
}

// → 123.456,789
const groupColumn: InfiniteTableColumn<Developer> = {
  header: 'Grouping',
  field: 'firstName',
  defaultWidth: 250,
  renderSelectionCheckBox: true,
  defaultFilterable: false,

  // in this function we have access to collapsed info
  // and grouping info about the current row - see rowInfo.groupBy
  renderValue: ({
    value,
    rowInfo,
  }: InfiniteTableColumnRenderValueParam<Developer>) => {
    if (!rowInfo.isGroupRow) {
      return value;
    }
    const groupBy = rowInfo.groupBy || [];
    const collapsed = rowInfo.collapsed;
    const currentGroupBy = groupBy[groupBy.length - 1];

    if (currentGroupBy?.field === 'age') {
      return `🥳 ${value}${collapsed ? ' 🤷‍♂️' : ''}`;
    }

    return `${value}`;
  },
};

const defaultGroupRowsState = new GroupRowsState({
  //make all groups collapsed by default
  collapsedRows: true,
  expandedRows: [
    ['United States'],
    ['United States', 'backend'],
    ['France'],
    ['Turkey'],
  ],
});

const columnTypes: InfiniteTablePropColumnTypes<Developer> = {
  number: {
    align: 'end',
    style: () => {
      return {};
    },
    renderValue: ({ value, data, rowInfo }) => {
      return new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency:
          rowInfo.isGroupRow && rowInfo.data?.currency === 'Mixed'
            ? 'USD'
            : data?.currency || 'USD',
      }).format(value);
    },
  },
};

const defaultRowSelection: DataSourcePropRowSelection_MultiRow = {
  selectedRows: [['United States'], ['India'], ['France'], ['Turkey']],
  deselectedRows: [['United States', 'frontend']],
  defaultSelection: false,
};

const defaultFilterValue: DataSourcePropFilterValue<Developer> = [
  {
    field: 'age',
    filter: {
      operator: 'gt',
      type: 'number',
      value: null,
    },
  },
];

const domProps = {
  style: {
    minHeight: '50vh',
    height: 600,
    margin: 5,
  },
};

export default function App() {
  const [{ min, max }, setMinMax] = useState({ min: 0, max: 0 });
  const columns = React.useMemo(() => {
    const cols = getColumns();

    if (cols.salary) {
      cols.salary.render = ({ renderBag, value }) => {
        const increase: number = Math.abs(max - min);
        const percentage = ((value - min) / increase) * 100;
        const alpha = Number((percentage / 100).toPrecision(2)) + 0.2;

        const backgroundColor = `rgba(255, 0, 0, ${alpha})`;
        return (
          <div
            style={{
              position: 'absolute',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'flex-end',
              top: 0,
              bottom: 0,
              left: 0,
              right: 0,
              backgroundColor,
            }}
          >
            {renderBag.all}
          </div>
        );
      };
    }

    return cols;
  }, [min, max]);

  const groupBy: DataSourceGroupBy<Developer>[] = React.useMemo(
    () => [
      {
        field: 'country',
      },
      { field: 'stack' },
    ],
    [],
  );

  const Link = ({
    children,
    href,
  }: {
    children: React.ReactNode;
    href: string;
  }) => {
    return (
      <a
        href={href}
        style={{
          textDecoration: 'underline',
          color: '#3bff7f',
          padding: 20,
          display: 'flex',
          alignItems: 'center',
        }}
      >
        {children}
      </a>
    );
  };
  return (
    <>
      {showLogo ? (
        <div style={{ display: 'flex' }}>
          <Link href="https://infinite-table.com">
            <img
              src="https://infinite-table.com/logo-infinite.svg"
              width={50}
              title="Infinite Logo"
              style={{ marginRight: 20 }}
            />
            Go Back Home
          </Link>

          <Link href="/docs/learn/getting-started#built-for-react-from-the-ground-up">
            View source
          </Link>
        </div>
      ) : null}
      <DataSource<Developer>
        data={dataSource}
        primaryKey="id"
        defaultFilterValue={defaultFilterValue}
        filterMode="local"
        useGroupKeysForMultiRowSelection
        defaultRowSelection={defaultRowSelection}
        defaultSortInfo={{
          dir: -1,
          field: 'country',
        }}
        onDataArrayChange={(data) => {
          const min = Math.min(...data.map((data) => data.salary ?? 0));
          const max = Math.max(...data.map((data) => data.salary ?? 0));

          setMinMax({ min, max });
        }}
        defaultGroupRowsState={defaultGroupRowsState}
        aggregationReducers={aggregationReducers}
        groupBy={groupBy}
      >
        <InfiniteTable<Developer>
          groupRenderStrategy="single-column"
          defaultColumnPinning={{
            'group-by': true,
          }}
          domProps={domProps}
          defaultActiveRowIndex={0}
          groupColumn={groupColumn}
          licenseKey={process.env.NEXT_PUBLIC_INFINITE_LICENSE_KEY}
          columns={columns}
          columnTypes={columnTypes}
          columnDefaultWidth={150}
        />
      </DataSource>
    </>
  );
}

const dataSource = () => {

Licensing

You can use @infinite-table/infinite-react in 2 ways:

  • with a license - requests for license quotations and additional quotations must be made by email to admin@infinite-table.com. After purchasing, you will receive a licenseKey which you will provide as a prop when you instantiate Infinite Table. This will make the Powered by Infinite Table footer go away.
  • without a license, but it will include a Powered by Infinite Table link in the table footer. This way you can use it for free in any product, but make sure the footer is always visible when Infinite Table is visible. For demo purposes, we don't show any license error for embeds in codesandbox.io - which are used throughout this demo site. Check the demo below to see the license footer in action.
Invalid License Demo
View Mode
Fork
import { InfiniteTable, DataSource } from '@infinite-table/infinite-react';
import type { InfiniteTableColumn } from '@infinite-table/infinite-react';
import * as React from 'react';

import { data, Person } from './data';

const columns: Record<string, InfiniteTableColumn<Person>> = {
  id: {
    // specifies which field from the data source
    // should be rendered in this column
    field: 'Id',
    type: 'number',
    defaultWidth: 80,
  },
  firstName: {
    field: 'FirstName',
  },
  age: { field: 'Age', type: 'number' },
};

export default function App() {
  return (
    <DataSource<Person> data={data} primaryKey="Id">
      <InfiniteTable<Person>
        licenseKey="<INVALID>"
        columnDefaultWidth={130}
        columns={columns}
      />
    </DataSource>

More on Licensing

Read more about our licensing model and how you can use Infinite Table.

About the Docs

We're grateful for the work done by the team behind reactjs.org and the new React documentation found at beta.reactjs.org - we've built our documentation on their excellent work 🙏 and we're grateful for that.

The documentation is versioned, and we will publish a new version of the documentation when there are any significant changes in the corresponding @infinite-table/infinite-react version.