Columns render the field value of the data they are bound to. This is the default behavior, which can be customized in a number of ways that we’re exploring below.

Note

If you want to explicitly use the TypeScript type definition for columns, import the InfiniteTableColumn type

import { InfiniteTableColumn } from '@infinite-table/infinite-react'

Note that it’s a generic type, so when you use it, you have to bind it to your DATA_TYPE (the type of your data object).

Note

When using custom rendering or custom components for columns, make sure all your rendering logic is controlled and that it doesn’t have local/transient state.

This is important because InfiniteTable uses virtualization heavily, in both column cells and column headers, so custom components can and will be unmounted and re-mounted multiple times, during the virtualization process (triggered by user scrolling, sorting, filtering and a few other interactions).

Change the value using valueGetter

The simplest way to change what’s being rendered in a column is to use the valueGetter prop and return a new value for the column.

const nameColumn: InfiniteTableColumn<Employee> = {
  header: 'Employee Name',
  valueGetter: ({ data }) =>
    `${data.firstName} ${data.lastName}`,
};

Note

The columns.valueGetter prop is a function that takes a single argument - an object with data and rowInfo properties.

Note that the data property is of type DATA_TYPE | Partial<DATA_TYPE> | null and not simply DATA_TYPE, because there are cases when you can have grouping (so for group rows with aggregations data will be Partial<DATA_TYPE>) or when there are lazily loaded rows or group rows with no aggregations - for which data is still null.

Column with custom valueGetter
Fork
import * as React from 'react';

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

import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';

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 dataSource = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' + '/developers1k'
  )
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: { field: 'id', defaultWidth: 80 },
  name: {
    header: 'Full Name',
    sortable: false,
    valueGetter: ({ data }) => {
      if (!data) {
        return null;
      }
      return `${data.firstName} ${data.lastName}`;
    },
  },

  preferredLanguage: { field: 'preferredLanguage' },
  stack: { field: 'stack' },
};

export default function ColumnValueGetterExample() {
  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}>
        <InfiniteTable<Developer>
          columns={columns}
          columnDefaultWidth={200}
        />
      </DataSource>
    </>
  );
}

The next step in customizing the column is to use the columns.renderValue or the columns.render props. In those functions, you have access to more information than in the columns.valueGetter function. For example, you have access to the current value of groupBy and pivotBy props.

renderValue and render can return any value that React can render.

The renderValue and render functions are called with an object that has the following properties:

  • data - the data object (of type DATA_TYPE | Partial<DATA_TYPE> | null) for the row.
  • rowInfo - very useful information about the current row:
    • rowInfo.value - the value that will be rendered by default
    • rowInfo.collapsed - if the row is collased or not.
    • rowInfo.groupBy - the current group by for the row
    • rowInfo.indexInAll - the index of the row in the whole data set
    • rowInfo.indexInGroup - the index of the row in the current group
    • … there are other useful properties that we’ll document in the near future

Note

The difference between columns.renderValue and columns.render is only for special columns (for now, only group columns are special columns, but more will come) when InfiniteTable renders additional content inside the column (eg: collapse/expand tool for group rows). The columns.render function allows you to override the additional content. So if you specify this function, it’s up to you to render whatever content, including the collapse/expand tool.

Note that for customizing the collapse/expand tool, you can use specify renderGroupIcon function on the group column.

Column with custom renderValue
Fork
import * as React from 'react';

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

import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';

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 dataSource = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' + '/developers1k'
  )
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: { field: 'id', defaultWidth: 80 },
  stack: {
    field: 'stack',
    renderValue: ({ data, rowInfo }) => {
      if (rowInfo.isGroupRow) {
        return <>{rowInfo.value} stuff</>;
      }

      return <b>🎇 {data?.stack}</b>;
    },
  },
  firstName: {
    field: 'firstName',
  },

  preferredLanguage: { field: 'preferredLanguage' },
};

const defaultGroupBy: DataSourceGroupBy<Developer>[] = [
  { field: 'stack' },
];

const groupColumn: InfiniteTablePropGroupColumn<Developer> =
  {
    defaultWidth: 250,

    renderValue: ({ rowInfo }) => {
      if (rowInfo.isGroupRow) {
        return (
          <>
            Grouped by <b>{rowInfo.value}</b>
          </>
        );
      }
    },
  };

export default function ColumnValueGetterExample() {
  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}
        defaultGroupBy={defaultGroupBy}>
        <InfiniteTable<Developer>
          groupColumn={groupColumn}
          columns={columns}
          columnDefaultWidth={200}
        />
      </DataSource>
    </>
  );
}

Changing the group icon using render. The icon can also be changed using renderGroupIcon.

Column with render - custom expand/collapse icon
Fork
import * as React from 'react';

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

import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';

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 dataSource = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' + '/developers1k'
  )
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: { field: 'id', defaultWidth: 80 },
  stack: {
    field: 'stack',
  },
  firstName: {
    field: 'firstName',
  },
  preferredLanguage: { field: 'preferredLanguage' },
};

const defaultGroupBy: DataSourceGroupBy<Developer>[] = [
  { field: 'stack' },
];

const groupColumn: InfiniteTablePropGroupColumn<Developer> =
  {
    defaultWidth: 250,
    render: ({ rowInfo, toggleCurrentGroupRow }) => {
      if (rowInfo.isGroupRow) {
        const { collapsed } = rowInfo;
        const expandIcon = (
          <svg
            style={{
              display: 'inline-block',
              fill: collapsed ? '#b00000' : 'blue',
            }}
            width="20px"
            height="20px"
            viewBox="0 0 24 24"
            fill="#000000">
            {collapsed ? (
              <>
                <path d="M0 0h24v24H0V0z" fill="none" />
                <path d="M12 5.83L15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z" />
              </>
            ) : (
              <path d="M7.41 18.59L8.83 20 12 16.83 15.17 20l1.41-1.41L12 14l-4.59 4.59zm9.18-13.18L15.17 4 12 7.17 8.83 4 7.41 5.41 12 10l4.59-4.59z" />
            )}
          </svg>
        );
        return (
          <div
            style={{
              cursor: 'pointer',
              display: 'flex',
              alignItems: 'center',
              color: collapsed ? '#b00000' : 'blue',
            }}
            onClick={() => toggleCurrentGroupRow()}>
            <i style={{ marginRight: 5 }}>Grouped by</i>{' '}
            <b>{rowInfo.value}</b>
            {expandIcon}
          </div>
        );
      }
    },
  };

export default function ColumnCustomRenderExample() {
  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}
        defaultGroupBy={defaultGroupBy}>
        <InfiniteTable<Developer>
          groupColumn={groupColumn}
          columns={columns}
          columnDefaultWidth={200}
        />
      </DataSource>
    </>
  );
}

Using hooks for custom rendering

Inside the columns.render and columns.renderValue functions, you can use hooks - both provided by InfiniteTable and any other React hooks.

When you’re inside a rendering function for a column cell, you can use useInfiniteColumnCell hook to get access to the current cell’s rendering information - the argument passed to the render or renderValue functions.

import {
  useInfiniteColumnCell,
  InfiniteTableColumn,
} from '@infinite-table/infintie-react';

function CustomName() {
  const { data, rowInfo } =
    useInfiniteColumnCell<Employee>();

  return (
    <>
      <b>{data.firstName}</b>, {data.lastName}
    </>
  );
}

const nameColumn: InfiniteTableColumn<Employee> = {
  header: 'Employee Name',
  renderValue: () => <CustomName />,
};
Column with render & useInfiniteColumnCell
Fork
import * as React from 'react';

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

import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import { HTMLProps } from 'react';

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 dataSource = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' + '/developers1k'
  )
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

function CustomCell(props: HTMLProps<HTMLElement>) {
  const { value, data } =
    useInfiniteColumnCell<Developer>();

  let emoji = '🤷';
  switch (value) {
    case 'photography':
      emoji = '📸';
      break;
    case 'cooking':
      emoji = '👨🏻‍🍳';
      break;
    case 'dancing':
      emoji = '💃';
      break;
    case 'reading':
      emoji = '📚';
      break;
    case 'sports':
      emoji = '⛹️';
      break;
  }

  const label = data?.stack === 'frontend' ? '⚛️' : '';

  return (
    <b>
      {emoji} + {label}
    </b>
  );
}

const columns: InfiniteTablePropColumns<Developer> = {
  id: { field: 'id', maxWidth: 80 },
  firstName: { field: 'firstName' },
  hobby: {
    field: 'hobby',
    // we're not using the arg of the render function directly
    // but CustomCell uses `useInfiniteColumnCell` to retrieve it instead
    render: () => <CustomCell />,
  },
};

export default function ColumnRenderWithHooksExample() {
  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}>
        <InfiniteTable<Developer>
          columns={columns}
          columnDefaultWidth={200}
        />
      </DataSource>
    </>
  );
}

For column headers, you can use useInfiniteHeaderCell hook to get access to the current header’s rendering information - the argument passed to the columns.header function.

import {
  useInfiniteHeaderCell,
  InfiniteTableColumn,
} from '@infinite-table/infintie-react';

function CustomHeader() {
  const { column } = useInfiniteHeaderCell<Employee>();

  return <b>{column.field}</b>;
}

const nameColumn: InfiniteTableColumn<Employee> = {
  header: 'Employee Name',
  field: 'firstName',
  header: () => <CustomHeader />,
};
Column Header with render & useInfiniteHeaderCell
Fork
import * as React from 'react';

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

import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';

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 dataSource = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' + '/developers1k'
  )
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: { field: 'id', maxWidth: 80 },
  stack: {
    field: 'stack',
  },
  hobby: {
    field: 'hobby',
    header: () => {
      const { column } = useInfiniteHeaderCell<Developer>();

      return (
        <b style={{ color: '#0000c2' }}>
          {column?.field} 🤷📸👨🏻‍🍳💃📚⛹️
        </b>
      );
    },
  },
};

export default function ColumnHeaderExampleWithHooks() {
  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}>
        <InfiniteTable<Developer>
          columns={columns}
          columnDefaultWidth={200}
        />
      </DataSource>
    </>
  );
}

There are cases when custom rendering via the columns.render and columns.renderValue props is not enough and you want to fully control the column cell and render your own custom component for that.

For such scenarios, you can specify column.components.HeaderCell and column.components.ColumnCell, which will use those components to render the DOM nodes of the column header and column cells respectively.

import { InfiniteTableColumn } from '@infinite-table/infintie-react';

const ColumnCell = (
  props: React.HTMLProps<HTMLDivElement>
) => {
  const { domRef, rowInfo } =
    useInfiniteColumnCell<Developer>();

  return (
    <div
      ref={domRef}
      {...props}
      style={{ ...props.style, color: 'red' }}>
      {props.children}
    </div>
  );
};

const HeaderCell = (
  props: React.HTMLProps<HTMLDivElement>
) => {
  const { domRef, sortTool } =
    useInfiniteHeaderCell<Developer>();

  return (
    <div
      ref={domRef}
      {...props}
      style={{ ...props.style, color: 'red' }}>
      {sortTool}
      First name
    </div>
  );
};

const nameColumn: InfiniteTableColumn<Developer> = {
  header: 'Name',
  field: 'firstName',
  components: {
    ColumnCell,
    HeaderCell,
  },
};

Note

When using custom components, make sure you get domRef from the corresponding hook (useInfiniteColumnCell for column cells and useInfiniteHeaderCell for header cells) and pass it on to the final JSX.Element that is the DOM root of the component.

// inside a component specified in column.components.ColumnCell
const { domRef } = useInfiniteColumnCell<DATA_TYPE>();

return <div ref={domRef}>
   ...
</div>

Also you have to make sure you spread all other props you receive in the component, as they are HTMLProps that need to end-up in the DOM (eg: className for theming and default styles, etc).

Both components.ColumnCell and components.HeaderCell need to be declared with props being of type HTMLProps<HTMLDivElement>.

Custom components
Fork
import * as React from 'react';

import {
  InfiniteTable,
  DataSource,
  DataSourceGroupBy,
  InfiniteTablePropGroupColumn,
  useInfiniteColumnCell,
  useInfiniteHeaderCell,
  InfiniteTablePropColumnTypes,
} from '@infinite-table/infinite-react';

import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';

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 dataSource = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' + '/developers1k'
  )
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const DefaultHeaderComponent: React.FunctionComponent<
  React.HTMLProps<HTMLDivElement>
> = (props) => {
  const { column, domRef, columnSortInfo } =
    useInfiniteHeaderCell<Developer>();

  const style = {
    ...props.style,
    border: '1px solid #fefefe',
  };

  let sortTool = '';
  switch (columnSortInfo?.dir) {
    case undefined:
      sortTool = '👉';
      break;
    case 1:
      sortTool = '👇';
      break;
    case -1:
      sortTool = '☝🏽';
      break;
  }

  return (
    <div ref={domRef} {...props} style={style}>
      {/* here you would usually have: */}
      {/* {props.children} {sortTool} */}
      {/* but in this case we want to override the default sort tool as well (which is part of props.children) */}
      {column.field} {sortTool}
    </div>
  );
};

const StackComponent: React.FunctionComponent<
  React.HTMLProps<HTMLDivElement>
> = (props) => {
  const { value, domRef } =
    useInfiniteColumnCell<Developer>();

  const isFrontEnd = value === 'frontend';
  const emoji = isFrontEnd ? '⚛️' : '💽';
  const style = {
    padding: '5px 20px',
    border: `1px solid ${isFrontEnd ? 'red' : 'green'}`,
    ...props.style,
  };
  return (
    <div ref={domRef} {...props} style={style}>
      {props.children} <div style={{ flex: 1 }} /> {emoji}
    </div>
  );
};

const columnTypes: InfiniteTablePropColumnTypes<Developer> =
  {
    default: {
      // override all columns to use these components
      components: {
        HeaderCell: DefaultHeaderComponent,
      },
    },
  };

const columns: InfiniteTablePropColumns<Developer> = {
  id: { field: 'id', defaultWidth: 80 },
  stack: {
    field: 'stack',
    renderValue: ({ data }) => 'Stack: ' + data?.stack,
    components: {
      HeaderCell: DefaultHeaderComponent,
      ColumnCell: StackComponent,
    },
  },
  firstName: {
    field: 'firstName',
  },
  preferredLanguage: {
    field: 'preferredLanguage',
  },
};

export default function ColumnValueGetterExample() {
  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}>
        <InfiniteTable<Developer>
          columns={columns}
          columnTypes={columnTypes}
        />
      </DataSource>
    </>
  );
}

Note

If you’re using the useInfiniteColumnCell hook inside the columns.render or columns.renderValue functions (and not as part of a custom component in columns.components.ColumnCell), you don’t need to pass on the domRef to the root of the DOM you’re rendering (same is true if you’re using useInfiniteHeaderCell inside the columns.header function).