Column Rendering
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 field
properties.
Note that the columns.valueGetter
is only called for non-group rows, so the data
property is of type DATA_TYPE
.
import { InfiniteTable, DataSource } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React 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); }; const columns: InfiniteTablePropColumns<Developer> = { id: { field: 'id', defaultWidth: 80 }, name: { header: 'Full Name', valueGetter: ({ data }) => `${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> </>
Note
The column value getter should not return JSX or other markup, because the value return by columns.valueGetter
will be used when the column is sorted (when sorting is done client-side and not remotely). For more in-depth information on sorting see the column sorting page.
Use renderValue
and render
to display custom content
The next step in customizing the rendering for a 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 typeDATA_TYPE | Partial<DATA_TYPE> | null
) for the row.rowInfo
- very useful information about the current row:rowInfo.collapsed
- if the row is collased or not.rowInfo.groupBy
- the current group by for the rowrowInfo.indexInAll
- the index of the row in the whole data setrowInfo.indexInGroup
- the index of the row in the current grouprowInfo.value
- the value (only for group rows) that will be rendered by default in group column cells.- ... there are other useful properties that we'll document in the near future
column
- the current column being renderedcolumnsMap
- theMap
of columns available to the table. Note these might not be all visible. The keys in this map will be column ids.fieldsToColumn
aMap
that linksDataSource
fields to columns. Columns bound to fields (so withcolumns.field
specified) will be included in thisMap
.api
- A reference to the Infinite Table API object.
Deep Dive
Column renderValue vs render
Note
Inside the columns.renderValue
and columns.render
functions (and other rendering functions), you can use the useInfiniteColumnCell
hook to retrieve the same params that are passed to the render functions.
This is especially useful when inside those functions you render a custom component that needs access to the same information.
type Developer = { country: string; name: string; id: string };
const CountryInfo = () => {
const { data, rowInfo, value } = useInfiniteColumnCell<Developer>();
return <div>Country: {value}</div>;
};
const columns = {
country: {
field: 'country',
renderValue: () => <CountryInfo />,
},
};
import { InfiniteTable, DataSource, DataSourceGroupBy, InfiniteTablePropGroupColumn, InfiniteTableColumnRenderValueParam, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React 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); }; 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, }: InfiniteTableColumnRenderValueParam<Developer>) => { if (rowInfo.isGroupRow) { return ( <> Grouped by <b>{rowInfo.value}</b> </> ); } return null; }, }; 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 columns.renderGroupIcon
.
This snippet shows overriding the group collapse/expand tool via the columns.render
function.
import { InfiniteTable, DataSource, DataSourceGroupBy, InfiniteTablePropGroupColumn, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React 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); }; 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> ); } return null; }, }; export default function ColumnCustomRenderExample() { return ( <> <DataSource<Developer> primaryKey="id" data={dataSource} defaultGroupBy={defaultGroupBy} > <InfiniteTable<Developer> groupColumn={groupColumn} columns={columns} columnDefaultWidth={200} /> </DataSource> </>
This snippet shows how you can override the group collapse/expand tool via the columns.renderGroupIcon
function.
import { InfiniteTable, DataSource, DataSourcePropGroupBy, InfiniteTablePropColumns, InfiniteTableColumn, } from '@infinite-table/infinite-react'; import * as React 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); }; const columns: InfiniteTablePropColumns<Developer> = { id: { field: 'id', defaultWidth: 80 }, stack: { field: 'stack', }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage' }, }; const groupBy: DataSourcePropGroupBy<Developer> = [ { field: 'country', column: { header: 'Country group', renderGroupValue: ({ value }) => <>Country: {value}</>, }, }, { field: 'preferredLanguage' }, ]; const groupColumn: InfiniteTableColumn<Developer> = { renderGroupIcon: ({ rowInfo, toggleCurrentGroupRow }) => { return ( <div onClick={toggleCurrentGroupRow} style={{ cursor: 'pointer', marginRight: 10 }} > {rowInfo.isGroupRow ? (rowInfo.collapsed ? '👇' : '👉') : ''} </div> ); }, }; export default function App() { return ( <DataSource<Developer> data={dataSource} primaryKey="id" groupBy={groupBy}> <InfiniteTable<Developer> columns={columns} groupColumn={groupColumn} /> </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.
Hook: useInfiniteColumnCell
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 />,
};
import { InfiniteTable, DataSource, useInfiniteColumnCell, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React from '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> </>
Hook: useInfiniteHeaderCell
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 />,
};
import { InfiniteTable, DataSource, useInfiniteHeaderCell, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React 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); }; const HobbyHeader: React.FC = function () { const { column } = useInfiniteHeaderCell<Developer>(); return <b style={{ color: '#0000c2' }}>{column?.field} 🤷📸👨🏻🍳💃📚⛹️</b>; }; const columns: InfiniteTablePropColumns<Developer> = { id: { field: 'id', maxWidth: 80 }, stack: { field: 'stack', }, hobby: { field: 'hobby', header: HobbyHeader, }, }; export default function ColumnHeaderExampleWithHooks() { return ( <> <DataSource<Developer> primaryKey="id" data={dataSource}> <InfiniteTable<Developer> columns={columns} columnDefaultWidth={200} /> </DataSource> </>
Use column.components
to customize the column
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>
.
import { InfiniteTable, DataSource, useInfiniteColumnCell, useInfiniteHeaderCell, InfiniteTablePropColumnTypes, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import * as React 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); }; 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).
If the above columns.components
is still not enough, read about the rendering pipeline below.
Rendering pipeline
The rendering pipeline for columns is a series of functions defined on the column that are called while rendering.
Note
All the functions that have the word render
in their name will be called with an object that has a renderBag
property, which contains values that will be rendered.
The default columns.render
function (the last one in the pipeline) ends up rendering a few things:
- a
value
- generally comes from thefield
the column is bound to - a
groupIcon
- for group columns - a
selectionCheckBox
- for columns that havecolumns.renderSelectionCheckBox
defined (combined with row selection)
When the rendering process starts for a column cell, all the above end up in the renderBag
object.
Rendering pipeline - renderBag.value
As already mentioned, the value
defaults to the value of the column field
for the current row.
If the column is not bound to a field, you can define a valueGetter
. The valueGetter
only has access to {data, field?}
in order to compute a value and return it.
After the valueGetter
is called, the valueFormatter
is next in the rendering pipeline.
This is called with more details about the current cell
const column: InfiniteTableColumn<T> = {
// the valueGetter can be useful when rows are nested objects
// or you want to compose multiple values from the row
valueGetter: ({ data }) => {
return data.person.salary * 10;
},
valueFormatter: ({
value,
isGroupRow,
data,
field,
rowInfo,
rowSelected,
rowActive,
}) => {
// the value here is what the `valueFormatter` returned
return `USD ${value}`;
},
};
After valueGetter
and valueFormatter
are called, the resulting value is the actual value used for the cell. This value will also be assigned to renderBag.value
When renderValue
and render
are called by InfiniteTable
, both value
and renderBag
will be available as properties to the arguments object.
const column: InfiniteTableColumn<T> = {
valueGetter: () => 'world',
renderValue: ({ value, renderBag, rowInfo }) => {
// at this stage, `value` is 'world' and `renderBag.value` has the same value, 'world'
return <b>{value}</b>;
},
render: ({ value, renderBag, rowInfo }) => {
// at this stage `value` is 'world'
// but `renderBag.value` is <b>world</b>, as this was the value returned by `renderValue`
return <div>Hello {renderBag.value}!</div>;
},
};
Note
After the renderValue
function is called, the following are also called (if available):
renderGroupValue
- for group rowsrenderLeafValue
- for leaf rows
You can think of them as an equivalent to renderValue
, but narrowed down to group/non-group rows.
Inside those functions, the renderBag.value
refers to the value returned by the renderValue
function.
Rendering pipeline - renderBag.groupIcon
In a similar way to renderBag.value
, the renderBag.groupIcon
is also piped through to the render
function.
const column: InfiniteTableColumn<T> = {
renderGroupIcon: ({ renderBag, toggleGroupRow }) => {
return <> [ {renderBag.groupIcon} ] </>;
},
render: ({ renderBag }) => {
return (
<>
{/* use the groupIcon from the renderBag */}
{renderBag.groupIcon}
{renderBag.value}
</>
);
},
};
Hint
Inside columns.renderGroupIcon
, you have access to renderBag.groupIcon
, which is basically the default group icon - so you can use that if you want, and build on that.
Also inside columns.renderGroupIcon
, you have access to toggleGroupRow
so you can properly hook the collapse/expand behaviour to your custom group icon.
Rendering pipeline - renderBag.selectionCheckBox
Like with the previous properties of renderBag
, you can customize the selectionCheckBox
(used when multiple selection is configured) to be piped-through - for columns that specify columns.renderSelectionCheckBox
.
const column: InfiniteTableColumn<T> = {
renderSelectionCheckBox: ({
renderBag,
rowSelected,
isGroupRow,
toggleCurrentRowSelection,
toggleCurrentGroupRowSelection,
}) => {
const toggle = isGroupRow
? toggleCurrentGroupRowSelection
: toggleCurrentRowSelection;
// you could return renderBag.groupIcon to have the default icon
const selection =
rowSelected === null
? '-' // we're in a group row with indeterminate state if rowSelected === null
: rowSelected
? 'x'
: 'o';
return <div onClick={toggle}> [ {selection} ] </div>;
},
render: ({ renderBag }) => {
return (
<>
{/* use the selectionCheckBox from the renderBag */}
{renderBag.selectionCheckBox}
{renderBag.groupIcon}
{renderBag.value}
</>
);
},
};
To recap, here is the full list of the functions in the rendering pipeline, in order of invocation:
columns.valueGetter
- doesn't have access torenderBag
columns.valueFormatter
- doesn't have access torenderBag
columns.renderGroupIcon
- can use all properties inrenderBag
columns.renderSelectionCheckBox
- can use all properties inrenderBag
columns.renderValue
- can use all properties inrenderBag
columns.renderGroupValue
- can use all properties inrenderBag
columns.renderLeafValue
- can use all properties inrenderBag
columns.render
- can use all properties inrenderBag
Additionally, the columns.components.ColumnCell
custom component does have access to the renderBag
via useInfiniteColumnCell