# Building pivoted React DataGrids with generated columns

> Turn grouped data into cross-tab reports with Infinite Table pivoting, generated columns, custom pivot headers, and total columns.

Published: 2026-07-10  
Author: radu  
Tags: pivoting, grouping, analytics

Canonical page: https://infinite-table.com/blog/2026/07/10/building-pivoted-react-datagrids-with-generated-columns

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](https://infinite-table.com/docs/learn/grouping-and-pivoting/pivoting/overview.md). 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.

```tsx
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`](https://infinite-table.com/docs/reference/datasource-props/index.md#pivotBy) and [`aggregationReducers`](https://infinite-table.com/docs/reference/datasource-props/index.md#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.

**Example: Pivoting with generated columns**

You can find this demo in our [pivoting docs](https://infinite-table.com/docs/learn/grouping-and-pivoting/pivoting/overview.md). Expand a group to see the aggregated salary values distributed across generated pivot columns.

```tsx
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(process.env.NEXT_PUBLIC_BASE_URL + '/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.

```tsx
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`](https://infinite-table.com/docs/reference/datasource-props/index.md#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.

```tsx
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:

- [`pivotTotalColumnPosition`](https://infinite-table.com/docs/reference/infinite-table-props.md#pivotTotalColumnPosition) for totals inside pivot groups
- [`pivotGrandTotalColumnPosition`](https://infinite-table.com/docs/reference/infinite-table-props.md#pivotGrandTotalColumnPosition) for grand-total columns across the configured aggregations

Pivot total columns only appear when you pivot by two or more fields (`pivotBy.length > 1`). With a single pivot field, enabling [`pivotTotalColumnPosition`](https://infinite-table.com/docs/reference/infinite-table-props.md#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.

```tsx
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.

**Example: Pivot totals and grand-total columns**

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.

```tsx
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(process.env.NEXT_PUBLIC_BASE_URL + '/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](https://infinite-table.com/docs/learn/grouping-and-pivoting/pivoting/overview.md) for the core `pivotBy` setup.

Then read [customizing pivot columns](https://infinite-table.com/docs/learn/grouping-and-pivoting/pivoting/customizing-pivot-columns.md) 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.
