# Master Detail

> Learn how to use master-detail rendering with the React DataGrid

Canonical page: https://infinite-table.com/docs/learn/master-detail/overview

The React DataGrid that Infinite Table offers has native support for master-detail rows.

The single most important property for the master-detail DataGrid configuration is the [`rowDetailRenderer`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailRenderer) function prop - which makes the DataGrid be considered master-detail.

In addition, make sure you have a column with the `renderRowDetailIcon: true` flag set. [`columns.renderRowDetailIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderRowDetailIcon) on a column makes the column display the row detail expand icon.

The row detail in the DataGrid can contain another DataGrid or any other custom content.

It's very imporant that the [`rowDetailRenderer`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailRenderer) function prop you pass into `<InfiniteTable />` is stable and doesn't change on every render. So make sure you pass a reference to the same function every time - except of course if you want the row detail to change based on some other state.

**Example: Basic master detail DataGrid example**

This example shows a master DataGrid with cities & countries.

The details for each city shows a DataGrid with developers in that city.

The detail DataGrid is configured with remote sorting.

```ts
import * as React from 'react';

import {
  DataSourceData,
  InfiniteTable,
  InfiniteTablePropColumns,
  DataSource,
  InfiniteTableRowInfo,
} from '@infinite-table/infinite-react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;

  city: string;
  currency: string;
  country: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';

  salary: number;
};

type City = {
  id: number;
  name: string;
  country: string;
};

const masterColumns: InfiniteTablePropColumns<City> = {
  id: {
    field: 'id',
    header: 'ID',
    defaultWidth: 70,
    renderRowDetailIcon: true,
  },
  country: { field: 'country', header: 'Country' },
  city: { field: 'name', header: 'City', defaultFlex: 1 },
};

const detailColumns: InfiniteTablePropColumns<Developer> = {
  firstName: {
    field: 'firstName',
    header: 'First Name',
  },
  salary: {
    field: 'salary',
    type: 'number',
  },

  stack: { field: 'stack' },
  currency: { field: 'currency' },
  city: { field: 'city' },
};

const domProps = {
  style: {
    height: '100%',
  },
};
const shouldReloadData = {
  sortInfo: true,
  filterValue: true,
};
function renderDetail(rowInfo: InfiniteTableRowInfo<City>) {
  console.log('rendering detail for master row', rowInfo.id);
  return (
    <DataSource<Developer>
      data={detailDataSource}
      primaryKey="id"
      shouldReloadData={shouldReloadData}
    >
      <InfiniteTable<Developer>
        debugId="master-detail-example"
        columnDefaultWidth={150}
        columnMinWidth={50}
        columns={detailColumns}
      />
    </DataSource>
  );
}

export default () => {
  return (
    <>
      <DataSource<City>
        data={citiesDataSource}
        primaryKey="id"
        defaultSortInfo={[
          {
            field: 'country',
            dir: 1,
          },
          {
            field: 'name',
            dir: 1,
          },
        ]}
      >
        <InfiniteTable<City>
          debugId="master-detail-example-2"
          domProps={domProps}
          columnDefaultWidth={150}
          columnMinWidth={50}
          columns={masterColumns}
          rowDetailRenderer={renderDetail}
        />
      </DataSource>
    </>
  );
};

// fetch an array of cities from the server
const citiesDataSource: DataSourceData<City> = () => {
  const cityNames = new Set<string>();
  const result: City[] = [];
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`)
    .then((response) => response.json())
    .then((response) => {
      response.data.forEach((data: Developer) => {
        if (cityNames.has(data.city)) {
          return;
        }
        cityNames.add(data.city);
        result.push({
          name: data.city,
          country: data.country,
          id: result.length,
        });
      });

      return result;
    });
};

const detailDataSource: DataSourceData<Developer> = ({
  filterValue,
  sortInfo,
  masterRowInfo,
}) => {
  if (sortInfo && !Array.isArray(sortInfo)) {
    sortInfo = [sortInfo];
  }

  if (!filterValue) {
    filterValue = [];
  }
  if (masterRowInfo) {
    // filter by master country and city
    filterValue = [
      {
        field: 'city',
        filter: {
          operator: 'eq',
          type: 'string',
          value: masterRowInfo.data.name,
        },
      },
      {
        field: 'country',
        filter: {
          operator: 'eq',
          type: 'string',
          value: masterRowInfo.data.country,
        },
      },
      ...filterValue,
    ];
  }
  const args = [
    sortInfo
      ? 'sortInfo=' +
        JSON.stringify(
          sortInfo.map((s) => ({
            field: s.field,
            dir: s.dir,
          })),
        )
      : null,

    filterValue
      ? 'filterBy=' +
        JSON.stringify(
          filterValue.map(({ field, filter }) => {
            return {
              field: field,
              operator: filter.operator,
              value:
                filter.type === 'number' ? Number(filter.value) : filter.value,
            };
          }),
        )
      : null,
  ]
    .filter(Boolean)
    .join('&');

  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};
```

If you want to use a component instead of the [`rowDetailRenderer`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailRenderer) function, you can use the [`components.RowDetail`](https://infinite-table.com/docs/reference/infinite-table-props.md#components.RowDetail) property. This works similarly and makes the DataGrid be considered master-detail. Inside the component, you can use the [`useMasterRowInfo`](https://infinite-table.com/docs/reference/hooks/index.md#useMasterRowInfo) hook to get the master row information.

## Loading the Detail DataSource

When master-detail is configured and the row detail renders a DataGrid, the [`data`](https://infinite-table.com/docs/reference/datasource-props/index.md#data) function for the detail `<DataSource />` will be called with the `masterRowInfo` as a property available in the object passed as argument.

```tsx title="Loading the detail DataGrid data" {2}
const detailDataFn: DataSourceData<Developer> = ({
  masterRowInfo,
  sortInfo,
  ...
}) => {

  return Promise.resolve([...])
}

<DataSource<Developer> data={detailDataFn}>
  {...}
</DataSource>
```

You can see the live example above for more details.

## Rendering a detail DataGrid

Using the [`rowDetailRenderer`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailRenderer) prop, you can render any custom content for the row details.

The content doesn't need to include Infinite Table.

You can, however, render an Infinite Table React DataGrid, at any level of nesting inside the row detail content.

**Example: Master detail with custom content & DataGrid**

In this example, the row detail contains custom content, along with another Infinite Table DataGrid. You can nest a child DataGrid inside the row details at any level of nesting.

```ts
import * as React from 'react';

import {
  DataSourceData,
  InfiniteTable,
  InfiniteTablePropColumns,
  DataSource,
  InfiniteTableRowInfo,
} from '@infinite-table/infinite-react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;

  city: string;
  currency: string;
  country: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';

  salary: number;
};

type City = {
  id: number;
  name: string;
  country: string;
};

const masterColumns: InfiniteTablePropColumns<City> = {
  id: {
    field: 'id',
    header: 'ID',
    defaultWidth: 70,
    renderRowDetailIcon: true,
  },
  country: { field: 'country', header: 'Country' },
  city: { field: 'name', header: 'City', defaultFlex: 1 },
};

const detailColumns: InfiniteTablePropColumns<Developer> = {
  firstName: {
    field: 'firstName',
    header: 'First Name',
  },
  salary: {
    field: 'salary',
    type: 'number',
  },

  stack: { field: 'stack' },
  currency: { field: 'currency' },
  city: { field: 'city' },
};

const domProps = {
  style: {
    height: '100%',
  },
};

const shouldReloadData = {
  sortInfo: true,
  filterValue: true,
};
function renderDetail(rowInfo: InfiniteTableRowInfo<City>) {
  return (
    <div
      style={{
        padding: 10,
        color: 'var(--infinite-cell-color)',
        background: 'var(--infinite-background)',
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
      }}
    >
      <h3>
        Developers in {rowInfo.data?.name}, {rowInfo.data?.country}
      </h3>

      <DataSource<Developer>
        data={detailDataSource}
        primaryKey="id"
        shouldReloadData={shouldReloadData}
      >
        <InfiniteTable<Developer>
          debugId="master-detail-custom-datagrid-example"
          columnDefaultWidth={150}
          columnMinWidth={50}
          columns={detailColumns}
        />
      </DataSource>
    </div>
  );
}

const defaultRowDetailState = {
  collapsedRows: true as const,
  expandedRows: [39, 54],
};

export default () => {
  return (
    <>
      <DataSource<City>
        data={citiesDataSource}
        primaryKey="id"
        defaultSortInfo={[
          {
            field: 'country',
            dir: 1,
          },
          {
            field: 'name',
            dir: 1,
          },
        ]}
      >
        <InfiniteTable<City>
          debugId="master-detail-custom-datagrid-example-2"
          domProps={domProps}
          columnDefaultWidth={150}
          defaultRowDetailState={defaultRowDetailState}
          columnMinWidth={50}
          columns={masterColumns}
          rowDetailHeight={320}
          rowDetailRenderer={renderDetail}
        />
      </DataSource>
    </>
  );
};

// fetch an array of cities from the server
const citiesDataSource: DataSourceData<City> = () => {
  const cityNames = new Set<string>();
  const result: City[] = [];
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`)
    .then((response) => response.json())
    .then((response) => {
      response.data.forEach((data: Developer) => {
        if (cityNames.has(data.city)) {
          return;
        }
        cityNames.add(data.city);
        result.push({
          name: data.city,
          country: data.country,
          id: result.length,
        });
      });

      return result;
    });
};

const detailDataSource: DataSourceData<Developer> = ({
  filterValue,
  sortInfo,
  masterRowInfo,
}) => {
  if (sortInfo && !Array.isArray(sortInfo)) {
    sortInfo = [sortInfo];
  }

  if (!filterValue) {
    filterValue = [];
  }
  if (masterRowInfo) {
    // filter by master country and city
    filterValue = [
      {
        field: 'city',
        filter: {
          operator: 'eq',
          type: 'string',
          value: masterRowInfo.data.name,
        },
      },
      {
        field: 'country',
        filter: {
          operator: 'eq',
          type: 'string',
          value: masterRowInfo.data.country,
        },
      },
      ...filterValue,
    ];
  }
  const args = [
    sortInfo
      ? 'sortInfo=' +
        JSON.stringify(
          sortInfo.map((s) => ({
            field: s.field,
            dir: s.dir,
          })),
        )
      : null,

    filterValue
      ? 'filterBy=' +
        JSON.stringify(
          filterValue.map(({ field, filter }) => {
            return {
              field: field,
              operator: filter.operator,
              value:
                filter.type === 'number' ? Number(filter.value) : filter.value,
            };
          }),
        )
      : null,
  ]
    .filter(Boolean)
    .join('&');

  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};
```

## Configuring the master-detail height

In order to configure the height of the row details, you can use the [`rowDetailHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailHeight) prop.

```tsx title="Configuring the row detail height" {3}
<InfiniteTable<City>
  columns={masterColumns}
  rowDetailHeight={500}
  rowDetailRenderer={renderDetail}
/>
```

The default value for the [`rowDetailHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailHeight) is `300` px.

[`rowDetailHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailHeight) can be one of the following:

- `number` - the height in pixels
- `string` - the name of a CSS variable that configures the height - eg: `--master-detail-height`
- `(rowInfo) => number` - a function that can return a different height for each row. The sole argument is the [rowInfo object](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo).

**Example: Master detail DataGrid with custom height for row details**

This master-detail DataGrid is configured with a custom [`rowDetailHeight`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailHeight) of `200px`.

```ts
import * as React from 'react';

import {
  DataSourceData,
  InfiniteTable,
  InfiniteTablePropColumns,
  DataSource,
  InfiniteTableRowInfo,
} from '@infinite-table/infinite-react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;

  city: string;
  currency: string;
  country: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';

  salary: number;
};

type City = {
  id: number;
  name: string;
  country: string;
};

const masterColumns: InfiniteTablePropColumns<City> = {
  id: {
    field: 'id',
    header: 'ID',
    defaultWidth: 70,
    renderRowDetailIcon: true,
  },
  country: { field: 'country', header: 'Country' },
  city: { field: 'name', header: 'City', defaultFlex: 1 },
};

const detailColumns: InfiniteTablePropColumns<Developer> = {
  firstName: {
    field: 'firstName',
    header: 'First Name',
  },
  salary: {
    field: 'salary',
    type: 'number',
  },

  stack: { field: 'stack' },
  currency: { field: 'currency' },
  city: { field: 'city' },
};

const domProps = {
  style: {
    height: '100%',
  },
};
const shouldReloadData = {
  sortInfo: true,
  filterValue: true,
};
function renderDetail(rowInfo: InfiniteTableRowInfo<City>) {
  console.log('rendering detail for master row', rowInfo.id);
  return (
    <DataSource<Developer>
      data={detailDataSource}
      primaryKey="id"
      shouldReloadData={shouldReloadData}
    >
      <InfiniteTable<Developer>
        debugId="master-detail-custom-detail-height-example"
        columnDefaultWidth={150}
        columnMinWidth={50}
        columns={detailColumns}
      />
    </DataSource>
  );
}

const defaultRowDetailState = {
  collapsedRows: true as const,
  expandedRows: [39, 54],
};

export default () => {
  return (
    <>
      <DataSource<City>
        data={citiesDataSource}
        primaryKey="id"
        defaultSortInfo={[
          {
            field: 'country',
            dir: 1,
          },
          {
            field: 'name',
            dir: 1,
          },
        ]}
      >
        <InfiniteTable<City>
          debugId="master-detail-custom-detail-height-example-2"
          domProps={domProps}
          columnDefaultWidth={150}
          defaultRowDetailState={defaultRowDetailState}
          columnMinWidth={50}
          columns={masterColumns}
          rowDetailHeight={200}
          rowDetailRenderer={renderDetail}
        />
      </DataSource>
    </>
  );
};

// fetch an array of cities from the server
const citiesDataSource: DataSourceData<City> = () => {
  const cityNames = new Set<string>();
  const result: City[] = [];
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`)
    .then((response) => response.json())
    .then((response) => {
      response.data.forEach((data: Developer) => {
        if (cityNames.has(data.city)) {
          return;
        }
        cityNames.add(data.city);
        result.push({
          name: data.city,
          country: data.country,
          id: result.length,
        });
      });

      return result;
    });
};

const detailDataSource: DataSourceData<Developer> = ({
  filterValue,
  sortInfo,
  masterRowInfo,
}) => {
  if (sortInfo && !Array.isArray(sortInfo)) {
    sortInfo = [sortInfo];
  }

  if (!filterValue) {
    filterValue = [];
  }
  if (masterRowInfo) {
    // filter by master country and city
    filterValue = [
      {
        field: 'city',
        filter: {
          operator: 'eq',
          type: 'string',
          value: masterRowInfo.data.name,
        },
      },
      {
        field: 'country',
        filter: {
          operator: 'eq',
          type: 'string',
          value: masterRowInfo.data.country,
        },
      },
      ...filterValue,
    ];
  }
  const args = [
    sortInfo
      ? 'sortInfo=' +
        JSON.stringify(
          sortInfo.map((s) => ({
            field: s.field,
            dir: s.dir,
          })),
        )
      : null,

    filterValue
      ? 'filterBy=' +
        JSON.stringify(
          filterValue.map(({ field, filter }) => {
            return {
              field: field,
              operator: filter.operator,
              value:
                filter.type === 'number' ? Number(filter.value) : filter.value,
            };
          }),
        )
      : null,
  ]
    .filter(Boolean)
    .join('&');

  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};
```

## Conditional row details

Not all rows in a DataGrid need to have details. To configure which rows have details, you can use the [`isRowDetailEnabled`](https://infinite-table.com/docs/reference/infinite-table-props.md#isRowDetailEnabled) function prop.

```tsx title="Using conditional row details" {5}
<InfiniteTable<City>
  columns={masterColumns}
  rowDetailHeight={500}
  rowDetailRenderer={renderDetail}
  isRowDetailEnabled={(rowInfo) => rowInfo.data.cityName.contains('i')}
/>
```

The [`isRowDetailEnabled`](https://infinite-table.com/docs/reference/infinite-table-props.md#isRowDetailEnabled) function prop is called with the [rowInfo object](https://infinite-table.com/docs/reference/type-definitions/index.md#InfiniteTableRowInfo) and is expected to return a boolean value.

**Example: Master detail DataGrid with conditional details**

This example shows a master DataGrid with cities & countries.

Not all rows have details - every other row is configured without details via the [`isRowDetailEnabled`](https://infinite-table.com/docs/reference/infinite-table-props.md#isRowDetailEnabled) function prop.

```ts
import * as React from 'react';

import {
  DataSourceData,
  InfiniteTable,
  InfiniteTablePropColumns,
  DataSource,
  InfiniteTableRowInfo,
} from '@infinite-table/infinite-react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;

  city: string;
  currency: string;
  country: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';

  salary: number;
};

type City = {
  id: number;
  name: string;
  country: string;
};

const masterColumns: InfiniteTablePropColumns<City> = {
  id: {
    field: 'id',
    header: 'ID',
    defaultWidth: 70,
    renderRowDetailIcon: true,
  },
  country: { field: 'country', header: 'Country' },
  city: { field: 'name', header: 'City', defaultFlex: 1 },
};

const detailColumns: InfiniteTablePropColumns<Developer> = {
  firstName: {
    field: 'firstName',
    header: 'First Name',
  },
  salary: {
    field: 'salary',
    type: 'number',
  },

  stack: { field: 'stack' },
  currency: { field: 'currency' },
  city: { field: 'city' },
};

const domProps = {
  style: {
    height: '100%',
  },
};
const shouldReloadData = {
  sortInfo: true,
  filterValue: true,
};
function renderDetail(rowInfo: InfiniteTableRowInfo<City>) {
  console.log('rendering detail for master row', rowInfo.id);
  return (
    <DataSource<Developer>
      data={detailDataSource}
      primaryKey="id"
      shouldReloadData={shouldReloadData}
    >
      <InfiniteTable<Developer>
        debugId="master-detail-per-row-example"
        columnDefaultWidth={150}
        columnMinWidth={50}
        columns={detailColumns}
      />
    </DataSource>
  );
}

const isRowDetailEnabled = (rowInfo: InfiniteTableRowInfo<City>) => {
  return rowInfo.indexInAll % 2 === 0;
};

export default () => {
  return (
    <>
      <DataSource<City>
        data={citiesDataSource}
        primaryKey="id"
        defaultSortInfo={[
          {
            field: 'country',
            dir: 1,
          },
          {
            field: 'name',
            dir: 1,
          },
        ]}
      >
        <InfiniteTable<City>
          debugId="master-detail-per-row-example-2"
          domProps={domProps}
          columnDefaultWidth={150}
          columnMinWidth={50}
          columns={masterColumns}
          isRowDetailEnabled={isRowDetailEnabled}
          rowDetailRenderer={renderDetail}
        />
      </DataSource>
    </>
  );
};

// fetch an array of cities from the server
const citiesDataSource: DataSourceData<City> = () => {
  const cityNames = new Set<string>();
  const result: City[] = [];
  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql`)
    .then((response) => response.json())
    .then((response) => {
      response.data.forEach((data: Developer) => {
        if (cityNames.has(data.city)) {
          return;
        }
        cityNames.add(data.city);
        result.push({
          name: data.city,
          country: data.country,
          id: result.length,
        });
      });

      return result;
    });
};

const detailDataSource: DataSourceData<Developer> = ({
  filterValue,
  sortInfo,
  masterRowInfo,
}) => {
  if (sortInfo && !Array.isArray(sortInfo)) {
    sortInfo = [sortInfo];
  }

  if (!filterValue) {
    filterValue = [];
  }
  if (masterRowInfo) {
    // filter by master country and city
    filterValue = [
      {
        field: 'city',
        filter: {
          operator: 'eq',
          type: 'string',
          value: masterRowInfo.data.name,
        },
      },
      {
        field: 'country',
        filter: {
          operator: 'eq',
          type: 'string',
          value: masterRowInfo.data.country,
        },
      },
      ...filterValue,
    ];
  }
  const args = [
    sortInfo
      ? 'sortInfo=' +
        JSON.stringify(
          sortInfo.map((s) => ({
            field: s.field,
            dir: s.dir,
          })),
        )
      : null,

    filterValue
      ? 'filterBy=' +
        JSON.stringify(
          filterValue.map(({ field, filter }) => {
            return {
              field: field,
              operator: filter.operator,
              value:
                filter.type === 'number' ? Number(filter.value) : filter.value,
            };
          }),
        )
      : null,
  ]
    .filter(Boolean)
    .join('&');

  return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?` + args)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};
```
