Grouping rows

You can use any field available in the DataSource to do the grouping - it can even be a field that is not a column.

Note

When using TypeScript, both DataSource and InfiniteTable components are generic and need to be rendered/instantiated with a DATA_TYPE parameter. The fields in that DATA_TYPE can then be used for grouping.

type Person = {
  name: string;
  age: number;
  country: string;
  id: string;
}

const groupBy = [{field: 'country'}]

<DataSource<Person> groupBy={groupBy}>
  <InfiniteTable<Person> />
</DataSource>

In the example above, we're grouping by country, which is a field available in the Person type. Specifying a field not defined in the Person type would be a type error.

Additionally, a column object can be used together with the field to define how the group column should be rendered.

const groupBy = [
  {
    field: 'country',
    column: {
      // custom column configuration for group column
      width: 150,
      header: 'Country group',
    },
  },
];

The example below puts it all together.

Also see the groupBy API reference to find out more.

Simple row grouping
View Mode
Fork
import { InfiniteTable, DataSource } from '@infinite-table/infinite-react';
import type {
  DataSourcePropGroupBy,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';
import * as React from 'react';

const groupBy: DataSourcePropGroupBy<Developer> = [
  {
    field: 'country',
    column: {
      header: 'Country group',
      renderGroupValue: ({ value }) => <>Country: {value}</>,
    },
  },
];

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
    // specifying a style here for the column
    // note: it will also be "picked up" by the group column
    // if you're grouping by the 'country' field
    style: {
      color: 'tomato',
    },
  },
  firstName: { field: 'firstName' },
  age: { field: 'age' },
  salary: {
    field: 'salary',
    type: 'number',
  },

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

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

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

type Developer = {
  id

In groupBy.column you can use any column property - so, for example, you can define a custom renderValue function to customize the rendering.

const groupBy = [
  {
    field: 'country',
    column: {
      renderValue: ({ value }) => <>Country: {value}</>,
    },
  },
];

Note

The generated group column(s) - can be one for all groups or one for each group - will inherit the style/className/renderers from the columns corresponding to the group fields themselves (if those columns exist).

Additionally, there are other ways to override those inherited configurations, in order to configure the group columns:

Grouping strategies

Multiple grouping strategies are supported by, InfiniteTable DataGrid:

  • multi column mode - multiple group columns are generated, one for each specified group field
  • single column mode - a single group column is generated, even when there are multiple group fields

You can specify the rendering strategy explicitly by setting the groupRenderStrategy property to any of the following: multi-column, single-column. If you don't set it explicitly, it will choose the best default based on your configuration.

Multiple groups columns

When grouping by multiple fields, by default the component will render a group column for each group field

const groupBy = [
  {
    field: 'age',
    column: {
      width: 100,
      renderValue: ({ value }) => <>Age: {value}</>,
    },
  },
  {
    field: 'companyName',
  },
  {
    field: 'country',
  },
];

Let's see an example of how the component would render the table with the multi-column strategy.

Multi-column group render strategy
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  DataSourcePropGroupBy,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';
import * as React from 'react';

const groupBy: DataSourcePropGroupBy<Developer> = [
  {
    field: 'age',
    column: {
      renderGroupValue: ({ value }: { value: any }) => `Age: ${value}`,
    },
  },
  {
    field: 'stack',
  },
];

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
  },
  firstName: { field: 'firstName' },
  age: { field: 'age' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
  stack: { field: 'stack' },
};

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

const dataSource = () => {
  return fetch('https://infinite-table.com/.netlify/functions/json-server' + '/developers10k')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

type Developer = {
  id

For the multi-column strategy, you can use hideEmptyGroupColumns in order to hide columns for groups which are currently not visible.

Hide Empty Group Columns
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  DataSourcePropGroupBy,
  InfiniteTablePropColumns,
  DataSourceProps,
} from '@infinite-table/infinite-react';
import * as React from 'react';

const groupBy: DataSourcePropGroupBy<Developer> = [
  {
    field: 'stack',
  },
  {
    field: 'preferredLanguage',
  },
];

const domProps = {
  style: { flex: 1 },
};

const groupRowsState: DataSourceProps<Developer>['groupRowsState'] = {
  expandedRows: [],
  collapsedRows: true,
};

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
  },
  firstName: { field: 'firstName' },
  age: { field: 'age' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
  stack: { field: 'stack' },
};

export default function App() {
  const [hideEmptyGroupColumns, setHideEmptyGroupColumns] =
    React.useState(true);
  return (
    <div
      style={{
        display: 'flex',
        flex: 1,
        color: 'var(--infinite-cell-color)',
        flexFlow: 'column',
        background: 'var(--infinite-background)',
      }}
    >
      <div style={{ padding: 10 }}>
        <label>
          <input
            type="checkbox"
            checked={hideEmptyGroupColumns}
            onChange={() => {
              setHideEmptyGroupColumns(!hideEmptyGroupColumns);
            }}
          />
          Hide Empty Group Columns (make sure all `Stack` groups are collapsed
          to see it in action). Try to expand the group column to see the new
          group column being added on-the-fly.
        </label>
      </div>
      <DataSource<Developer>
        data={dataSource}
        primaryKey="id"
        defaultGroupRowsState={groupRowsState}
        groupBy={groupBy}
      >
        <InfiniteTable<Developer>
          domProps={domProps}
          hideEmptyGroupColumns={hideEmptyGroupColumns}
          groupRenderStrategy={'multi-column'}
          columns={columns}
          columnDefaultWidth={250}
        />
      </DataSource>
    </div>
  );
}

const dataSource = () => {
  return fetch('https://infinite-table.com/.netlify/functions/json-server' + '/developers10')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

type Developer = {
  id

Gotcha

You can specify an id for group columns. This is helpful if you want to size those columns (via columnSizing) or pin them (via columnPinning) or configure them in other ways. If no id is specified, it will be generated like this: "group-by-${field}"

Single group column

You can group by multiple fields, yet only render a single group column. To choose this rendering strategy, specify groupRenderStrategy property to be single-column (or specify groupColumn as an object.)

In this case, you can't override the group column for each group field, as there's only one group column being generated. However, you can specify a groupColumn property to customize the generated column.

Note

By default the generated group column will "inherit" many of the properties (the column style or className or renderers) of the columns corresponding to the group fields (if such columns exist, because it's not mandatory that they are defined).

Single-column group render strategy
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  DataSourcePropGroupBy,
  InfiniteTableColumn,
} from '@infinite-table/infinite-react';
import * as React from 'react';

import { columns, Employee } from './columns';

const groupBy: DataSourcePropGroupBy<Employee> = [
  {
    field: 'age',
  },
  {
    field: 'companyName',
  },
];

const groupColumn: InfiniteTableColumn<Employee> = {
  header: 'Grouping',
  defaultWidth: 250,
  // in this function we have access to collapsed info
  // and grouping info about the current row - see rowInfo.groupBy
  renderGroupValue: ({ value, rowInfo }) => {
    const groupBy = rowInfo.groupBy || [];
    const collapsed = rowInfo.collapsed;
    const currentGroupBy = groupBy[groupBy.length - 1];

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

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

export default function App() {
  return (
    <DataSource<Employee> data={dataSource} primaryKey="id" groupBy={groupBy}>
      <InfiniteTable<Employee>
        groupRenderStrategy="single-column"
        groupColumn={groupColumn}
        columns={columns}
        columnDefaultWidth={150}
      />
    </DataSource>
  );
}

const dataSource = () => {

Note

If groupColumn is specified to an object and no groupRenderStrategy is passed, the render strategy will be single-column.

groupColumn can also be a function, which allows you to individually customize each group column - in case the multi-column strategy is used.

Gotcha

You can specify an id for the single groupColumn. This is helpful if you want to size this column (via columnSizing) or pin it (via columnPinning) or configure it in other ways. If no id is specified, it will default to "group-by".

Customizing the group column

There are many ways to customize the group column(s) and we're going to show a few of them below:

Binding the group column to a field

By default, group columns only show values in the group rows - but they are normal columns, so why not bind them to a field of the DATA_TYPE?

const groupColumn = {
  id: 'the-group', // can specify an id
  style: {
    color: 'tomato',
  },
  field: 'firstName', // non-group rows will render the first name
};
const columns = {
  theFirstName: {
    field: 'firstName',
    style: {
      // this style will also be applied in the group column,
      // since it is bound to this same `field`
      fontWeight: 'bold',
    },
  },
};

This makes the column display the value of the field in non-group/normal rows. Also, if you have another column bound to that field, the renderers/styling of that column will be used for the value of the group column, in non-group rows.

Bind group column to a field
View Mode
Fork
import { InfiniteTable, DataSource } from '@infinite-table/infinite-react';
import type {
  DataSourcePropGroupBy,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';
import * as React from 'react';

const groupBy: DataSourcePropGroupBy<Developer> = [
  {
    field: 'stack',
  },
  {
    field: 'preferredLanguage',
  },
];

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
  },
  theFirstName: {
    field: 'firstName',
    style: {
      color: 'orange',
    },
    renderLeafValue: ({ value }) => {
      return `${value}!`;
    },
  },
  stack: {
    field: 'stack',
    style: {
      color: 'tomato',
    },
  },
  age: { field: 'age' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
};

const groupColumn = {
  field: 'firstName' as keyof Developer,
};

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

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

type Developer = {
  id

Use groupColumn to customize rendering

The groupColumn will inherit its own rendering and styling from the columns that are bound to the fields used in groupBy.field. However, you can override any of those properties so you have full control over the rendering process.

const groupColumn = {
  field: 'firstName',
  renderGroupValue: ({ value }) => {
    return `Group: ${value}`;
  },
  renderLeafValue: ({ value }) => {
    return `First name: ${value}`;
  },
};
Customize group column renderer

The column that renders the firstName has a custom renderer that adds a . at the end. The group column is bound to the same firstName field, but specifies a different renderer, which will be used instead.

View Mode
Fork
import { InfiniteTable, DataSource } from '@infinite-table/infinite-react';
import type {
  DataSourcePropGroupBy,
  InfiniteTableColumn,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';
import * as React from 'react';

const groupBy: DataSourcePropGroupBy<Developer> = [
  {
    field: 'stack',
  },
  {
    field: 'preferredLanguage',
  },
];

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
  },
  firstName: {
    field: 'firstName',
    style: {
      color: 'orange',
    },
    renderValue: ({ value, rowInfo }) =>
      rowInfo.isGroupRow ? null : `${value}.`,
  },
  stack: {
    field: 'stack',
    style: {
      color: 'tomato',
    },
  },
  age: { field: 'age' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
};

const groupColumn: InfiniteTableColumn<Developer> = {
  field: 'firstName',
  renderValue: ({ value }) => {
    return `First name: ${value}`;
  },
};

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

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

type Developer = {
  id

Column rendering

Learn more about customizing column rendering via multiple renderer functions.

Hiding columns when grouping

When grouping is enabled, you can choose to hide some columns. Here are the two main ways to do this:

Valid values for columns.defaultHiddenWhenGroupedBy are:

  • "*" - when any grouping is active, hide the column that specifies this property
  • true - when the field this column is bound to is used in grouping, hides this column
  • keyof DATA_TYPE - specify an exact field that, when grouped by, makes this column be hidden
  • {[k in keyof DATA_TYPE]: true} - an object that can specify more fields. When there is grouping by any of those fields, the current column gets hidden.
Hide columns when grouping

In this example, the column bound to firstName field is set to hide when any grouping is active, since the group column is anyways found to the firstName field.

In addition, hideColumnWhenGrouped is set to true, so the stack and preferredLanguage columns are also hidden, since they are grouped by.

View Mode
Fork
import { InfiniteTable, DataSource } from '@infinite-table/infinite-react';
import type {
  DataSourcePropGroupBy,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';
import * as React from 'react';

const groupBy: DataSourcePropGroupBy<Developer> = [
  {
    field: 'stack',
  },
  {
    field: 'preferredLanguage',
  },
];

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
  },
  theFirstName: {
    field: 'firstName',
    style: {
      color: 'orange',
    },
    // hide this column when grouping active
    // as the group column is anyways bound to this field
    defaultHiddenWhenGroupedBy: '*',
  },
  stack: {
    field: 'stack',
    style: {
      color: 'tomato',
    },
  },
  preferredLanguage: { field: 'preferredLanguage' },
  age: { field: 'age' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
};

const groupColumn = {
  field: 'firstName' as keyof Developer,
};

export default function App() {
  return (
    <DataSource<Developer> data={dataSource} primaryKey="id" groupBy={groupBy}>
      <InfiniteTable<Developer>
        groupColumn={groupColumn}
        columns={columns}
        hideColumnWhenGrouped
        columnDefaultWidth={250}
      />
    </DataSource>
  );
}

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

type Developer = {
  id

Sorting the group column

When groupRenderStrategy="single-column" is used, the group column is sortable by default if all the columns that are involved in grouping are sortable. Sorting the group column makes the sortInfo have a value that looks like this:

const sortInfo = [
  {
    dir: 1,
    id: 'group-by',
    field: ['stack', 'age'],
    type: ['string', 'number'],
  },
];

groupRenderStrategy="multi-column", each group column is sortable by default if the column with the corresponding field is sortable.

Note

The columnDefaultSortable property can be used to override the default behavior.

Group column with initial descending sorting
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  DataSourcePropSortInfo,
} from '@infinite-table/infinite-react';
import type {
  DataSourcePropGroupBy,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';
import * as React from 'react';

const groupBy: DataSourcePropGroupBy<Developer> = [
  {
    field: 'stack',
  },
  {
    field: 'preferredLanguage',
  },
];

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
  },
  theFirstName: {
    field: 'firstName',
    style: {
      color: 'orange',
    },
    // hide this column when grouping active
    // as the group column is anyways bound to this field
    defaultHiddenWhenGroupedBy: '*',
  },
  stack: {
    field: 'stack',
    style: {
      color: 'tomato',
    },
  },
  preferredLanguage: { field: 'preferredLanguage' },
  age: { field: 'age' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
};

const groupColumn = {
  field: 'firstName' as keyof Developer,
};

const defaultSortInfo: DataSourcePropSortInfo<Developer> = [
  {
    field: ['stack', 'preferredLanguage'],
    dir: -1,
    id: 'group-by',
  },
];

export default function App() {
  return (
    <DataSource<Developer>
      data={dataSource}
      primaryKey="id"
      defaultSortInfo={defaultSortInfo}
      groupBy={groupBy}
    >
      <InfiniteTable<Developer>
        groupColumn={groupColumn}
        columns={columns}
        hideColumnWhenGrouped
        columnDefaultWidth={250}
      />
    </DataSource>
  );
}

const dataSource = () => {
  return fetch('https://infinite-table.com/.netlify/functions/json-server' + '/developers10')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

type Developer = {
  id

Note

When a group column is configured and the groupBy fields are not bound to actual columns in the table, the group column will not be sortable by default.

If you want to make it sortable, you have to specify a columns.sortType array, of the same length as the groupBy array, that specifies the sort type for each group field.

Aggregations

When grouping, you can also aggregate the values of the grouped rows. This is done via the DataSource.aggregationReducers property. See the example below

Grouping with aggregations
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  GroupRowsState,
} from '@infinite-table/infinite-react';
import type {
  InfiniteTableColumn,
  InfiniteTablePropColumns,
  InfiniteTableColumnRenderValueParam,
  DataSourcePropAggregationReducers,
  DataSourceGroupBy,
} 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 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,
  },
};

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  age: { field: 'age' },

  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
  country: { field: 'country' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};
const groupColumn: InfiniteTableColumn<Developer> = {
  header: 'Grouping',
  defaultWidth: 250,

  // 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: [],
});

export default function App() {
  const groupBy: DataSourceGroupBy<Developer>[] = React.useMemo(
    () => [
      {
        field: 'country',
      },
      { field: 'stack' },
    ],
    [],
  );
  return (
    <DataSource<Developer>
      data={dataSource}
      primaryKey="id"
      defaultGroupRowsState={defaultGroupRowsState}
      aggregationReducers={aggregationReducers}
      groupBy={groupBy}
    >
      <InfiniteTable<Developer>
        groupRenderStrategy="single-column"
        groupColumn={groupColumn}
        columns={columns}
        columnDefaultWidth={150}
      />
    </DataSource>
  );
}

const dataSource = () => {

Each reducer from the aggregationReducers map can have the following properties:

  • field - the field to aggregate on
  • getter(data) - a value-getter function, if the aggregation values are are not mapped directly to a field
  • initialValue - the initial value to start with when computing the aggregation (for client-side aggregations only)
  • reducer: string | (acc, current, data: DATA_TYPE, index)=>value - the reducer function to use when computing the aggregation (for client-side aggregations only). For server-side aggregations, this will be a string
  • done(value, arr) - a function that is called when the aggregation is done (for client-side aggregations only) and returns the final value of the aggregation
  • name - useful especially in combination with pivotBy, as it will be used as the pivot column header.

If an aggregation reducer is bound to a field in the dataset, and there is a column mapped to the same field, that column will show the corresponding aggregation value for each group row, as shown in the example above.

Gotcha

If you want to prevent the user to expand the last level of group rows, you can override the render function for the group column

Customized group expand on last group level
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  DataSourcePropAggregationReducers,
  InfiniteTablePropColumns,
  DataSourceGroupBy,
  GroupRowsState,
  InfiniteTableGroupColumnFunction,
  InfiniteTableGroupColumnBase,
  InfiniteTableColumnCellContextType,
} 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 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,
  },
};

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  age: { field: 'age' },

  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
  country: { field: 'country' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

// TODO remove this after the next release
//@ts-ignore
const groupColumn: InfiniteTableGroupColumnFunction<Developer> = (arg) => {
  const column = {} as Partial<InfiniteTableGroupColumnBase<Developer>>;

  if (arg.groupIndexForColumn === arg.groupBy.length - 1) {
    column.render = (param: InfiniteTableColumnCellContextType<Developer>) => {
      const { value, rowInfo } = param;
      if (
        rowInfo.isGroupRow &&
        rowInfo.groupBy?.length != rowInfo.rootGroupBy?.length
      ) {
        // we are on a group row that is the last grouping level
        return null;
      }
      return value;
    };
  }
  return column;
};

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

export default function App() {
  const groupBy: DataSourceGroupBy<Developer>[] = React.useMemo(
    () => [
      {
        field: 'country',
      },
      { field: 'stack' },
    ],
    [],
  );
  return (
    <DataSource<Developer>
      data={dataSource}
      primaryKey="id"
      defaultGroupRowsState={defaultGroupRowsState}
      aggregationReducers={aggregationReducers}
      groupBy={groupBy}
    >
      <InfiniteTable<Developer>
        groupRenderStrategy="multi-column"
        groupColumn={groupColumn}
        columns={columns}
        columnDefaultWidth={250}
      />
    </DataSource>
  );
}

const dataSource = () => {

Aggregations

Dive deeper into the aggregation reducers and how they work.

Server side grouping with lazy loading

Lazy loading becomes all the more useful when working with grouped data.

The DataSource data function is called with an object that has all the information about the current DataSource state(grouping/pivoting/sorting/lazy-loading, etc) - see the paragraphs above for details.

Server side grouping needs two kinds of data responses in order to work properly:

  • response for non-leaf row groups - these are groups that have children. For such groups (including the top-level group), the DataSource.data function must return a promise that's resolved to an object with the following properties:
    • totalCount - the total number of records in the group
    • data - an array of objects that describes non-leaf child groups, each object has the following properties:
      • keys - an array of the group keys (usually strings) that uniquely identifies the group, from the root to the current group
      • data - an object that describes the common properties of the group
      • aggregations - an object that describes the aggregations for the current group
  • response for leaf rows - these are normal rows - rows that would have been served in the non-grouped response. The resolved object should have the following properties:
    • data - an array of objects that describes the rows
    • totalCount - the total number of records on the server, that are part of the current group

Here's an example, that assumes grouping by country and city and aggregations by age and salary (average values):

//request:
groupKeys: [] // empty keys array, so it's a top-level group
groupBy: [{"field":"country"},{"field":"city"}]
reducers: [{"field":"salary","id":"avgSalary","name":"avg"},{"field":"age","id":"avgAge","name":"avg"}]
// lazyLoadStartIndex: 0, - passed if lazyLoad is configured with a batchSize
// lazyLoadBatchSize: 20 - passed if lazyLoad is configured with a batchSize

//response
{
  cache: true,
  totalCount: 20,
  data: [
    {
      data: {country: "Argentina"},
      aggregations: {avgSalary: 20000, avgAge: 30},
      keys: ["Argentina"],
    },
    {
      data: {country: "Australia"},
      aggregations: {avgSalary: 25000, avgAge: 35},
      keys: ["Australia"],
    }
    //...
  ]
}

Now let's expand the first group and see how the request/response would look like:


//request:
groupKeys: ["Argentina"]
groupBy: [{"field":"country"},{"field":"city"}]
reducers: [{"field":"salary","id":"avgSalary","name":"avg"},{"field":"age","id":"avgAge","name":"avg"}]

//response
{
  totalCount: 4,
  data: [
    {
      data: {country: "Argentina", city: "Buenos Aires"},
      aggregations: {avgSalary: 20000, avgAge: 30},
      keys: ["Argentina", "Buenos Aires"],
    },
    {
      data: {country: "Argentina", city: "Cordoba"},
      aggregations: {avgSalary: 25000, avgAge: 35},
      keys: ["Argentina", "Cordoba"],
    },
    //...
  ]
}

Finally, let's have a look at the leaf/normal rows and a request for them:


//request
groupKeys: ["Argentina","Buenos Aires"]
groupBy: [{"field":"country"},{"field":"city"}]
reducers: [{"field":"salary","id":"avgSalary","name":"avg"},{"field":"age","id":"avgAge","name":"avg"}]

//response
{
  totalCount: 20,
  data: [
    {
      id: 34,
      country: "Argentina",
      city: "Buenos Aires",
      age: 30,
      salary: 20000,
      stack: "full-stack",
      firstName: "John",
      //...
    },
    {
      id: 35,
      country: "Argentina",
      city: "Buenos Aires",
      age: 35,
      salary: 25000,
      stack: "backend",
      firstName: "Jane",
      //...
    },
    //...
  ]
}

Note

When a row group is expanded, since InfiniteTable has the group keys from the previous response when the node was loaded, it will use the keys array and pass them to the DataSource.data function when requesting for the children of the respective group.

You know when to serve last-level rows, because in that case, the length of the groupKeys array will be equal to the length of the groupBy array.

Server side grouping with lazy loding
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  DataSourceData,
  InfiniteTablePropColumns,
  GroupRowsState,
  DataSourceGroupBy,
  DataSourcePropAggregationReducers,
} 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 aggregationReducers: DataSourcePropAggregationReducers<Developer> = {
  salary: {
    name: 'Salary (avg)',
    field: 'salary',
    reducer: 'avg',
  },
  age: {
    name: 'Age (avg)',
    field: 'age',
    reducer: 'avg',
  },
};

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  age: { field: 'age' },

  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
  country: { field: 'country' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

const groupRowsState = new GroupRowsState({
  expandedRows: [],
  collapsedRows: true,
});

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

  return (
    <DataSource<Developer>
      primaryKey="id"
      data={dataSource}
      groupBy={groupBy}
      aggregationReducers={aggregationReducers}
      defaultGroupRowsState={groupRowsState}
      lazyLoad={true}
    >
      <InfiniteTable<Developer>
        scrollStopDelay={10}
        hideEmptyGroupColumns
        columns={columns}
        columnDefaultWidth={220}
      />
    </DataSource>
  );
}

const dataSource: DataSourceData<Developer> = ({
  pivotBy,
  aggregationReducers,
  groupBy,

  lazyLoadStartIndex,
  lazyLoadBatchSize,
  groupKeys = [],
  sortInfo,
}) => {
  if (sortInfo && !Array.isArray(sortInfo)) {
    sortInfo = [sortInfo];
  }
  const startLimit: string[] = [];
  if (lazyLoadBatchSize && lazyLoadBatchSize > 0) {
    const start = lazyLoadStartIndex || 0;
    startLimit.push(`start=${start}`);
    startLimit.push(`limit=${lazyLoadBatchSize}`);
  }
  const args = [
    ...startLimit,
    pivotBy
      ? 'pivotBy=' + JSON.stringify(pivotBy.map((p) => ({ field: p.field })))
      : null,
    `groupKeys=${JSON.stringify(groupKeys)}`,
    groupBy
      ? 'groupBy=' + JSON.stringify(groupBy.map((p) => ({ field: p.field })))
      : null,
    sortInfo
      ? 'sortInfo=' +
        JSON.stringify(
          sortInfo.map((s) => ({
            field: s.field,
            dir: s.dir,
          })),
        )
      : null,
    aggregationReducers
      ? 'reducers=' +
        JSON.stringify(
          Object.keys(aggregationReducers).map((key) => ({
            field

Eager loading for group row nodes

When using lazy-loading together with batching, node data (without children) is loaded when a node (normal or grouped) comes into view. Only when a group node is expanded will its children be loaded. However, you can do this loading eagerly, by using the dataset property on the node you want to load.

Note

This can be useful in combination with using dataParams.groupRowsState from the data function - so your datasource can know which groups are expanded, and thus it can serve those groups already loaded with children.

//request:
groupKeys: [] // empty keys array, so it's a top-level group
groupBy: [{"field":"country"},{"field":"city"}]
reducers: [{"field":"salary","id":"avgSalary","name":"avg"},{"field":"age","id":"avgAge","name":"avg"}]
// lazyLoadStartIndex: 0, - passed if lazyLoad is configured with a batchSize
// lazyLoadBatchSize: 20 - passed if lazyLoad is configured with a batchSize

//response
{
  cache: true,
  totalCount: 20,
  data: [
    {
      data: {country: "Argentina"},
      aggregations: {avgSalary: 20000, avgAge: 30},
      keys: ["Argentina"],
      // NOTE this dataset property used for eager-loading of group nodes
      dataset: {
        // the shape of the dataset is the same as the one normally returned by the datasource
        cache: true,
        totalCount: 4,
        data: [
          {
            data: {country: "Argentina", city: "Buenos Aires"},
            aggregations: {avgSalary: 20000, avgAge: 30},
            keys: ["Argentina", "Buenos Aires"],
          },
          {
            data: {country: "Argentina", city: "Cordoba"},
            aggregations: {avgSalary: 25000, avgAge: 35},
            keys: ["Argentina", "Cordoba"],
          },
        ]
      }
    },
    {
      data: {country: "Australia"},
      aggregations: {avgSalary: 25000, avgAge: 35},
      keys: ["Australia"],
    }
    //...
  ]
}