Building pivoted React DataGrids with generated columns

By raduΒ·

View as Markdown
Pivot tables 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 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 /> β€” and maps it to real product scenarios.

Pivoting belongs in the DataSource#

In Infinite Table, the DataSource owns grouping, pivoting, and aggregation because those features 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 the handoff for a pivot DataGrid. The DataSource looks at pivotBy and aggregationReducers, groups and aggregates the data, then gives the table the generated pivotColumns and pivotColumnGroups.
Without that split, you would 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.

Map a business question to pivot config#

A useful way to design a pivot view is to write the question first, then fill in three lists:
  • Rows (groupBy) β€” what you compare down the page: region, department, product line
  • Columns (pivotBy) β€” what becomes generated headers: month, channel, plan tier
  • Values (aggregationReducers) β€” what you measure: sum of revenue, avg salary, ticket count
If the column headers depend on values in the data β€” countries that appear this quarter, warehouses that currently hold stock, channels that received tickets β€” you want generated pivot columns, not a static column list.

Scenario 1: People analytics β€” language Γ— stack, pivoted by country#

Imagine a developers analytics grid. Rows are grouped by preferredLanguage and stack, then pivoted by country and whether a developer can design. Instead of one salary column, the grid shows aggregated salary for each country/design combination.
That is the classic β€œcompare compensation across geography while keeping role structure on the rows” report β€” the same shape as headcount or average salary by department and location.
The demo below groups developers, pivots the salary aggregation into generated columns, and starts with groups collapsed so the cross-tab is easy to scan.
Expand a group to see aggregated salary values distributed across generated pivot columns. Full walkthrough: pivoting overview.
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>
    </>
  );
}
In product terms, the same shape answers questions like:
  • Average salary by technology stack, broken out by country
  • Headcount by department, broken out by office
  • Utilization by team, broken out by project phase

Scenario 2: Support ops β€” volume by priority Γ— channel#

Support and customer-success teams often need a matrix, not a flat ticket list:
  • Rows: priority (critical, high, normal) or product area
  • Columns: channel (email, chat, phone, portal)
  • Values: ticket count, average time-to-first-response, reopen rate
const groupBy = [{ field: 'priority' }, { field: 'productArea' }];
const pivotBy = [{ field: 'channel' }];

const aggregationReducers = {
  ticketCount: {
    name: 'Tickets',
    initialValue: 0,
    reducer: (acc) => acc + 1,
  },
  avgFirstResponseMins: {
    name: 'Avg first response (min)',
    field: 'firstResponseMins',
    initialValue: 0,
    reducer: (acc, value) => acc + value,
    done: (sum, arr) => (arr.length ? sum / arr.length : 0),
  },
};
As new channels appear in the feed, Infinite Table generates matching pivot columns. You do not ship a release every time marketing adds β€œWhatsApp” as a support channel.
The same pattern works for sales pipeline stages by region, or incidents by severity and service.

Scenario 3: Commerce β€” revenue by product line Γ— region (with totals)#

Finance and merchandising reports usually need both the matrix and the summary columns:
  • Rows: product category β†’ SKU family
  • Columns: region β†’ sales channel
  • Values: sum of revenue, sum of units
  • Pivot totals summarize inside a pivot group (for example, all channels under EMEA)
  • Grand totals summarize across the whole pivot surface for each aggregation
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 β€” those totals would duplicate the values already shown. 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"

Pivot total columns sit 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>
    </>
  );
}
For a commerce grid, that reads as: revenue for each region/channel cell, a subtotal per region, and a grand total for the selected filters β€” spreadsheet expectations, without building spreadsheet UI yourself.

Scenario 4: Inventory β€” warehouse Γ— status, readable headers#

Generated columns still need to feel like part of your app. Infinite Table gives you a few customization layers.

Inherit formatting from source columns#

If an aggregation reducer is bound to a field that already has a column, the generated pivot column inherits that column configuration β€” number formatting, styles, default width, and related behavior.
const columns: InfiniteTablePropColumns<Developer> = {
  salary: {
    field: 'salary',
    type: 'number',
    style: { color: 'red' },
  },
};

const aggregationReducers = {
  avgSalary: {
    field: 'salary',
    reducer: 'avg',
  },
};
Aggregations bound to salary / age pick up the original column styling. See also customizing pivot columns.
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  GroupRowsState,
} from '@infinite-table/infinite-react';
import type {
  DataSourcePropAggregationReducers,
  InfiniteTableColumnAggregator,
  InfiniteTablePropColumns,
  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);
};

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

const columnAggregations: DataSourcePropAggregationReducers<Developer> = {
  avgSalary: {
    field: 'salary',
    name: 'Average salary',
    ...avgReducer,
  },
  avgAge: {
    field: 'age',
    ...avgReducer,
    pivotColumn: {
      defaultWidth: 500,
      inheritFromColumn: 'firstName',
    },
  },
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: { field: 'id' },
  firstName: {
    field: 'firstName',
    style: {
      fontWeight: 'bold',
    },
    renderValue: ({ value }) => <>{value}!</>,
  },
  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',
    header: 'Salary',
    style: { color: 'red' },
  },
  currency: { field: 'currency' },
};

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

export default function PivotByExample() {
  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={columnAggregations}
        defaultGroupRowsState={groupRowsState}
      >
        {({ pivotColumns, pivotColumnGroups }) => {
          return (
            <InfiniteTable<Developer>
              debugId="pivot-column-inherit-example"
              columns={columns}
              hideEmptyGroupColumns
              pivotColumns={pivotColumns}
              pivotColumnGroups={pivotColumnGroups}
              columnDefaultWidth={180}
            />
          );
        }}
      </DataSource>
    </>
  );
}

Rename raw values into business labels#

Inventory status codes (in_stock, reserved, quarantine) or boolean flags rarely belong in a header as-is. Use pivotBy.column as an object for every generated column at that pivot level, or as 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 callback turns raw pivot keys into labels operators recognize, while column generation stays automatic.
The canDesign pivot values are rewritten to Designer / Non-designer headers. Try collapsing and expanding groups to see the column groups stay aligned.
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  GroupRowsState,
} from '@infinite-table/infinite-react';
import type {
  DataSourcePropAggregationReducers,
  InfiniteTableColumnAggregator,
  InfiniteTablePropColumns,
  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);
};

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

const columnAggregations: 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 PivotByExample() {
  const groupBy: DataSourceGroupBy<Developer>[] = React.useMemo(
    () => [
      {
        field: 'preferredLanguage',
      },
      { field: 'stack' },
    ],
    [],
  );

  const pivotBy: DataSourcePivotBy<Developer>[] = React.useMemo(
    () => [
      { field: 'country' },
      {
        field: 'canDesign',
        column: ({ column: pivotCol }) => {
          const lastKey =
            pivotCol.pivotGroupKeys[pivotCol.pivotGroupKeys.length - 1];

          return {
            header: lastKey === 'yes' ? 'πŸ’… Designer' : 'πŸ’» Non-designer',
          };
        },
      },
    ],
    [],
  );

  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}
        groupBy={groupBy}
        pivotBy={pivotBy}
        aggregationReducers={columnAggregations}
        defaultGroupRowsState={groupRowsState}
      >
        {({ pivotColumns, pivotColumnGroups }) => {
          return (
            <InfiniteTable<Developer>
              debugId="pivoting-customize-column-example"
              columns={columns}
              hideEmptyGroupColumns
              pivotColumns={pivotColumns}
              pivotColumnGroups={pivotColumnGroups}
              columnDefaultWidth={180}
            />
          );
        }}
      </DataSource>
    </>
  );
}
A warehouse dashboard might look like:
const groupBy = [{ field: 'skuFamily' }, { field: 'sku' }];
const pivotBy = [
  { field: 'warehouse' },
  {
    field: 'status',
    column: ({ column }) => {
      const status =
        column.pivotGroupKeys[column.pivotGroupKeys.length - 1];
      const labels: Record<string, string> = {
        in_stock: 'Available',
        reserved: 'Reserved',
        quarantine: 'Quarantine',
      };

      return {
        header: labels[status] ?? String(status),
        defaultWidth: 120,
      };
    },
  },
];

const aggregationReducers = {
  units: {
    name: 'Units',
    field: 'quantity',
    initialValue: 0,
    reducer: (acc, value) => acc + value,
  },
};
New warehouses in the dataset become new column groups without a frontend schema change.

Scenario 5: SaaS metrics β€” plans and regions at scale#

Product analytics often outgrows client-side reshaping: millions of subscription events, weekly cohorts, plan Γ— region matrices. For that case, keep the same Infinite Table UI model and move the pivot computation to the server.
Enable lazyLoad and provide a function for data that returns already-pivoted groups. The grid still generates pivotColumns / pivotColumnGroups from pivotBy and aggregationReducers, but leaf rows are not loaded β€” pivoting works on aggregated group payloads.
const groupBy = [{ field: 'country' }, { field: 'stack' }];
const pivotBy = [
  { field: 'preferredLanguage' },
  { field: 'canDesign' },
];

const aggregationReducers = {
  salary: { name: 'Salary (avg)', field: 'salary', reducer: 'avg' },
  age: { name: 'Age (avg)', field: 'age', reducer: 'avg' },
};

const dataSource = ({ groupBy, pivotBy, groupKeys, aggregationReducers }) => {
  // Fetch a Promise that resolves to { data, totalCount, pivot, ... }
  // See the pivoting docs for the full response shape.
};

<DataSource lazyLoad data={dataSource} groupBy={groupBy} pivotBy={pivotBy} />;
Typical SaaS mappings:
  • MRR by segment and plan β€” groupBy: segment β†’ accountTier; pivotBy: plan β†’ billingInterval; aggregations: sum(mrr), count(accounts)
  • Activation by acquisition channel β€” groupBy: week β†’ cohort; pivotBy: channel; aggregation: avg(activationRate)
  • Churn by region and product β€” groupBy: region; pivotBy: product β†’ plan; aggregation: sum(churnedMrr)
Grouping and pivot aggregations are loaded remotely. Expand countries to fetch nested groups with pivot values already computed on the server.
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  GroupRowsState,
} from '@infinite-table/infinite-react';
import type {
  InfiniteTablePropColumns,
  InfiniteTablePropColumnPinning,
  DataSourceData,
  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 DATA_SOURCE_SIZE = '10k';

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

  groupKeys = [],
}) => {
  const args = [
    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,
    aggregationReducers
      ? 'reducers=' +
        JSON.stringify(
          Object.keys(aggregationReducers).map((key) => ({
            field: aggregationReducers[key].field,
            id: key,
            name: aggregationReducers[key].reducer,
          })),
        )
      : null,
  ]
    .filter(Boolean)
    .join('&');
  return fetch(
    'https://data.infinite-table.com' +
      `/developers${DATA_SOURCE_SIZE}-sql?` +
      args,
  )
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const aggregationReducers: DataSourcePropAggregationReducers<Developer> = {
  salary: {
    // the aggregation name will be used as the column header
    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' },
};

// make the row labels column (id: 'labels') be pinned
const defaultColumnPinning: InfiniteTablePropColumnPinning = {
  // make the generated group columns pinned to start
  'group-by-country': 'start',
  'group-by-stack': 'start',
};

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

export default function RemotePivotExample() {
  const groupBy: DataSourceGroupBy<Developer>[] = React.useMemo(
    () => [
      {
        field: 'country',
        column: {
          // give the group column for the country prop a custom id
          id: 'group-by-country',
        },
      },
      {
        field: 'stack',
        column: {
          // give the group column for the stack prop a custom id
          id: 'group-by-stack',
        },
      },
    ],
    [],
  );

  const pivotBy: DataSourcePivotBy<Developer>[] = React.useMemo(
    () => [
      { field: 'preferredLanguage' },
      {
        field: 'canDesign',
        // customize the column group
        columnGroup: ({ columnGroup }) => {
          return {
            ...columnGroup,
            header: `${
              columnGroup.pivotGroupKey === 'yes' ? 'Designer' : 'Non-designer'
            }`,
          };
        },
        // customize columns generated under this column group
        column: ({ column }) => ({
          ...column,
          header: `πŸŽ‰ ${column.header}`,
        }),
      },
    ],
    [],
  );

  return (
    <DataSource<Developer>
      primaryKey="id"
      data={dataSource}
      groupBy={groupBy}
      pivotBy={pivotBy}
      aggregationReducers={aggregationReducers}
      defaultGroupRowsState={groupRowsState}
      lazyLoad={true}
    >
      {({ pivotColumns, pivotColumnGroups }) => {
        return (
          <InfiniteTable<Developer>
            debugId="remote-pivoting-example"
            defaultColumnPinning={defaultColumnPinning}
            columns={columns}
            hideEmptyGroupColumns
            pivotColumns={pivotColumns}
            pivotColumnGroups={pivotColumnGroups}
            columnDefaultWidth={220}
          />
        );
      }}
    </DataSource>
  );
}
For very large trees, the same guide covers lazy-load batching so groups stream in pages while the pivot column model stays consistent.

Scenario 6: Let users change the report at runtime#

Many analytics products need more than one fixed pivot. Users want to switch β€œgroup by department, pivot by month” to β€œgroup by region, pivot by product line” without a new screen.
Because groupBy, pivotBy, and aggregationReducers are ordinary React props, you can drive them from UI state. The dynamic pivoting example shows client-side and server-side variants where those dimensions change at runtime, including custom number/currency formatting on the generated columns.
That is the difference between a static export and an interactive report builder: the DataSource recomputes (or re-fetches) the pivot surface; <InfiniteTable /> keeps rendering whatever pivotColumns it receives.

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:
  • Revenue by region and product line (with region/channel totals)
  • Headcount or average salary by department and location
  • Support volume by priority and channel
  • Inventory units by warehouse and stock status
  • SaaS MRR / churn by plan and billing interval
  • Incident count by severity and owning service
In each case the column structure depends on the data. Generated pivot columns are the point: you describe dimensions and aggregations, and Infinite Table creates the report surface.

Go deeper in the docs#