Building pivoted React DataGrids with generated columns

By raduยท

View as Markdown
Pivot tables are one of those features that start as a reporting request and quickly become a UI architecture problem.
You need to group rows, aggregate values, create columns from data values, keep those columns typed, and still leave enough room to customize headers, widths, formatting, totals, and server-side loading. Infinite Table's pivoting feature keeps that work inside the DataSource pipeline so your React grid can render a report-style layout without hardcoding every possible result column.
The docs cover this in the pivoting guide. This article walks through the key idea: define pivoting at the data level, then render the generated columns in <InfiniteTable />.

Pivoting belongs in the DataSource#

In Infinite Table, the DataSource owns grouping, pivoting, and aggregation because those features all reshape the data before the grid renders it.
const groupBy = [{ field: 'department' }, { field: 'country' }];
const pivotBy = [{ field: 'team' }];

<DataSource<Developer>
  groupBy={groupBy}
  pivotBy={pivotBy}
  aggregationReducers={aggregationReducers}
>
  {({ pivotColumns, pivotColumnGroups }) => {
    return (
      <InfiniteTable<Developer>
        columns={columns}
        pivotColumns={pivotColumns}
        pivotColumnGroups={pivotColumnGroups}
      />
    );
  }}
</DataSource>;
The children render prop of the DataSource is an important handoff in the Pivot DataGrid. The DataSource looks at the configured pivotBy and aggregationReducers, does all the data grouping and computations and then gives the table the generated pivotColumns and pivotColumnGroups.
Without that split, you would have to scan the dataset yourself, discover every pivot value, construct matching columns, wire column groups, and keep the result in sync as grouping or pivoting changes.

A practical example: grouping by role, pivoting by geography#

Imagine a developers analytics grid where rows are grouped by preferredLanguage and stack, then pivoted by country and whether a developer can design. Instead of showing one salary column, the grid can show aggregated salary columns for each country/design combination.
The docs example below does exactly that. It groups developers, pivots the salary aggregation into generated columns, and starts with all groups collapsed so the cross-tab shape is easy to scan.
You can find this demo in our pivoting docs. Expand a group to see the aggregated salary values distributed across generated pivot columns.
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  GroupRowsState,
} from '@infinite-table/infinite-react';
import type {
  InfiniteTableColumnAggregator,
  InfiniteTablePropColumns,
  DataSourcePropAggregationReducers,
  DataSourceGroupBy,
  DataSourcePivotBy,
} 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://data.infinite-table.com' + '/developers100')
    .then((r) => r.json())
    .then((data: Developer[]) => data)
    .then(
      (data) =>
        new Promise<Developer[]>((resolve) => {
          setTimeout(() => resolve(data), 1000);
        }),
    );
};

const avgReducer: InfiniteTableColumnAggregator<Developer, any> = {
  initialValue: 0,
  field: 'salary',
  reducer: (acc, sum) => acc + sum,
  done: (sum, arr) => (arr.length ? sum / arr.length : 0),
};

const reducers: DataSourcePropAggregationReducers<Developer> = {
  salary: avgReducer,
};

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

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

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

  const pivotBy: DataSourcePivotBy<Developer>[] = React.useMemo(
    () => [
      { field: 'country' },
      {
        field: 'canDesign',
      },
    ],
    [],
  );

  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}
        groupBy={groupBy}
        pivotBy={pivotBy}
        aggregationReducers={reducers}
        defaultGroupRowsState={groupRowsState}
      >
        {({ pivotColumns, pivotColumnGroups }) => {
          return (
            <InfiniteTable<Developer>
              debugId="pivoting-example"
              columns={columns}
              pivotColumns={pivotColumns}
              pivotColumnGroups={pivotColumnGroups}
              columnDefaultWidth={200}
              pivotTotalColumnPosition="end"
            />
          );
        }}
      </DataSource>
    </>
  );
}

Generated does not mean generic#

Generated columns still need to feel like part of your app. Infinite Table gives you a few places to customize them.
The first option is inheritance. If an aggregation reducer is bound to a field that already has a column, the generated pivot column inherits the original column configuration.
const columns: InfiniteTablePropColumns<Developer> = {
  salary: {
    field: 'salary',
    type: 'number',
    style: { color: 'red' },
  }
}

const aggregationReducers = {
  avgSalary: {
    field: 'salary',
    reducer: 'avg',
  }
}
That means your formatting, sizing, and column behavior can stay close to the original field definition.
When you need more control, use pivotBy.column. It can be an object applied to all generated columns for that pivot level, or a function that receives the generated column metadata.
const pivotBy: DataSourcePivotBy<Developer>[] = [
  { field: 'country' },
  {
    field: 'canDesign',
    column: ({ column }) => {
      const lastKey = column.pivotGroupKeys[column.pivotGroupKeys.length - 1];

      return {
        header: lastKey === 'yes' ? 'Designer' : 'Non-designer',
      };
    },
  },
];
That small callback is enough to turn raw data values into business-friendly column headers while keeping the column generation automatic.

Totals and grand totals#

Pivoted reports usually need totals. Infinite Table supports both:
Pivot total columns only appear when you pivot by two or more fields (pivotBy.length > 1). With a single pivot field, enabling pivotTotalColumnPosition has no effect โ€” the totals would be the same as the already displayed values. The example below uses two pivot levels (stack and canDesign) so both kinds of totals are visible.
const pivotBy = [{ field: 'stack' }, { field: 'canDesign' }];

<InfiniteTable<Developer>
  columns={columns}
  pivotColumns={pivotColumns}
  pivotColumnGroups={pivotColumnGroups}
  pivotTotalColumnPosition="end"
  pivotGrandTotalColumnPosition="start"

This gives you the spreadsheet-like summary columns people expect from a pivot view, while keeping the placement explicit.
This docs example pivots by stack and canDesign, so pivot total columns are shown at the end of each stack group. Grand-total columns are placed at the start of the grid.
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  DataSourceGroupBy,
  DataSourcePivotBy,
  InfiniteTableColumnAggregator,
  DataSourcePropAggregationReducers,
} 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://data.infinite-table.com' + '/developers1k')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

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

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

const defaultPivotBy: DataSourcePivotBy<Developer>[] = [
  {
    field: 'stack',
  },
  {
    field: 'canDesign',
    columnGroup: ({ columnGroup }) => {
      return {
        ...columnGroup,
        header:
          columnGroup.pivotGroupKey === 'yes' ? 'Designer' : 'Non-designer',
      };
    },
  },
];

const avgReducer: InfiniteTableColumnAggregator<Developer, any> = {
  initialValue: 0,

  reducer: (acc, sum) => acc + sum,
  done: (sum, arr) => Math.round(arr.length ? sum / arr.length : 0),
};

const aggregations: DataSourcePropAggregationReducers<Developer> = {
  salary: {
    ...avgReducer,
    name: 'Salary (avg)',
    field: 'salary',
  },
  age: {
    ...avgReducer,
    name: 'Age (avg)',
    field: 'age',
  },
};

export default function ColumnValueGetterExample() {
  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        defaultGroupBy={defaultGroupBy}
        defaultPivotBy={defaultPivotBy}
        aggregationReducers={aggregations}
        data={dataSource}
      >
        {({ pivotColumns, pivotColumnGroups }) => {
          return (
            <InfiniteTable<Developer>
              debugId="pivot-grand-total-column-position-example"
              groupRenderStrategy="single-column"
              columns={columns}
              columnDefaultWidth={200}
              pivotColumns={pivotColumns}
              pivotColumnGroups={pivotColumnGroups}
              pivotTotalColumnPosition="end"
              pivotGrandTotalColumnPosition="start"
            />
          );
        }}
      </DataSource>
    </>
  );
}

When pivoting is a good fit#

Reach for pivoting when the question is not "which rows match this filter?" but "how do values compare across dimensions?"
Good use cases include:
  • revenue by region and product line
  • headcount by department and location
  • average salary by technology stack and country
  • support volume by priority and channel
  • inventory by warehouse and status
In each case, the column structure depends on the data. That is where generated pivot columns help: you describe the dimensions and aggregations, and Infinite Table creates the report surface.

Go deeper in the docs#

Start with the pivoting overview for the core pivotBy setup.
Then read customizing pivot columns to tune generated headers, widths, inherited column behavior, and per-pivot-value column configuration.
If your data is too large to reshape in the browser, the same pivoting guide also covers server-side pivoting, where DataSource.data returns already-pivoted groups while Infinite Table keeps the UI model consistent.