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.If you want to explicitly use the TypeScript type definition for columns, import the
InfiniteTableColumn
typeimport { 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).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}
COPY
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
.View Mode
Fork Forkimport { 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> </>
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.
Column renderValue vs render
columns.render
is the last function called in the rendering pipeline for a column cell, while columns.renderValue
is called before render, towards the beginning of the rendering pipeline (read more about this below).Avoid over-writing
columns.render
for special columns (like group columns) unless you know what you're doing. Special columns use the render
function to render additional content inside the column (eg: collapse/expand tool for group rows). The columns.render
function allows you to override this additional content. So if you specify this function, it's up to you to render whatever content, including the collapse/expand tool.However, there are easier ways to override the collapse/expand group icon, like using
columns.renderGroupIcon
.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 />,
},
};
COPY
View Mode
Fork Forkimport { 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.View Mode
Fork Forkimport { 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.View Mode
Fork Forkimport { 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 />
COPY
View Mode
Fork Forkimport { 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 />
COPY
View Mode
Fork Forkimport { 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', components: { HeaderCell: 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,
}
COPY
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>;
COPY
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>
.View Mode
Fork Forkimport { 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> </>
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.
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}
COPY
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>
COPY
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} </>
COPY
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} </>
COPY
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