# Master Detail - Collapsing and Expanding Rows

> Learn how to use master-detail and configure the state of the row details as expanded or collapsed

Canonical page: https://infinite-table.com/docs/learn/master-detail/collapsing-and-expanding-rows

You can control the collapsed/expanded state of rows in the master-detail configuration.

By default, all row details are collapsed. You can very easily change this by using the [`defaultRowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowDetailState) prop.

```tsx title="Specyfing the default row detail state" {8}
const defaultRowDetailState = {
  collapsedRows: true,
  expandedRows: [39, 54],
};

<InfiniteTable
  columns={...}
  defaultRowDetailState={defaultRowDetailState}
  rowDetailRenderer={...}
/>

```

**Example: Master detail DataGrid with some row details expanded by default**

Some of the rows in the master DataGrid are expanded by default.

Also, we have a default sort defined, by the `country` and `city` columns.

```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-default-expanded-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-default-expanded-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);
};
```

## Understanding and defining the collapse/expand state for row details

When you want to specify a different collapse/expand state of the row details (since by default they are all collapsed, and you might want to expand some of them), you need to use the [`defaultRowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowDetailState) prop, or its controlled counterpart - the [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) prop.

The [row detail state](https://infinite-table.com/docs/reference/type-definitions/index.md#RowDetailState) can be defined in two ways:

- either specify `collapsedRows: true` (which means all rows are collapsed by default) and specify an array of `expandedRows`, which will contain the ids of the rows that should be rendered as expanded.

```tsx
const defaultRowDetailState = {
  collapsedRows: true,
  expandedRows: ['id-1', 'id-2', 'id-56'],
};
```

- or specify `expandedRows: true` (which means all rows are expanded by default) and specify an array of `collapsedRows`, which will contain the ids of the rows that should be rendered as collapsed.

```tsx
const rowDetailState = {
  expandedRows: true,
  collapsedRows: ['id-1', 'id-2', 'id-56'],
};
```

You can pass these objects into either the [`defaultRowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowDetailState) (uncontrolled) or the [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) (controlled).

If you're using the controlled [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) prop, you'll need to respond to user interaction by listening to [`onRowDetailStateChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onRowDetailStateChange) and updating the value of [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) accordingly.

As an alternative to using the object literals as specified above, you can import the `RowDetailState` class from `@infinite-table/infinite-react` and use it to define the state of the row details. You can pass instances of `RowDetailState` into the [`defaultRowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#defaultRowDetailState) or [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) props.

```tsx title="Passing an instance of RowDetailState to the InfiniteTable"
import { RowDetailState } from '@infinite-table/infinite-react';

const rowDetailState = new RowDetailState({
  collapsedRows: true,
  expandedRows: [2, 3, 4],
});

<InfiniteTable<DATA_TYPE> rowDetailState={rowDetailState} />;
```

```tsx title="Passing an object literal to the InfiniteTable"
<InfiniteTable<DATA_TYPE>
  rowDetailState={{
    collapsedRows: true,
    expandedRows: [2, 3, 4],
  }}
/>
```

See our type definitions for [more details on row detail state](https://infinite-table.com/docs/reference/type-definitions/index.md#RowDetailState).

**Example: Master detail DataGrid with listener for row expand/collapse**

Some of the rows in the master DataGrid are expanded by default.

We use the controlled [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) prop to manage the state of the row details and update it by using [`onRowDetailStateChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onRowDetailStateChange).

```ts file=master-detail-controlled-expanded-enhanced-example.page.tsx

```

## Listening to row detail state changes

In order to be notified when the collapse/expand state of row details changes, you can use the [`onRowDetailStateChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onRowDetailStateChange) prop.

This function is called with only one argument - the new [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState). Please note this is an instance of [`RowDetailState`](https://infinite-table.com/docs/reference/type-definitions/index.md#RowDetailState). If you want to use the object literal, make sure you call `getState()` on the instance of `RowDetailState`.

```tsx title="Using the onRowDetailStateChange listener" {11}
function App() {
  const [rowDetailState, setRowDetailState] = React.useState<RowDetailStateObject>({
    collapsedRows: true as const,
    expandedRows: [39, 54],
  });

  return <DataSource<DATA_TYPE> {...}>
    <InfiniteTable<DATA_TYPE>
      rowDetailState={rowDetailState}
      onRowDetailStateChange={(rowDetailStateInstance) => {
        setRowDetailState(rowDetailStateInstance.getState());
      }}
      columnMinWidth={50}
      columns={masterColumns}
      rowDetailHeight={200}
      rowDetailRenderer={renderDetail}
    />
  </DataSource>
}
```

When using the controlled [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState), you'll need to respond to the user interaction by using the [`onRowDetailStateChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onRowDetailStateChange) listener, in order to update the controlled [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState).

This allows you to manage the state of the row details yourself - making it easy to expand/collapse all rows, or to expand/collapse a specific row by simply updating the value of the [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) prop.

```tsx
const [rowDetailState, setRowDetailState] =
  React.useState<RowDetailStateObject>({
    collapsedRows: true,
    expandedRows: [39, 54],
  });

const expandAll = () => {
  setRowDetailState({
    collapsedRows: [],
    expandedRows: true,
  });
};
const collapseAll = () => {
  setRowDetailState({
    collapsedRows: true,
    expandedRows: [],
  });
};

return (
  <>
    <button onClick={expandAll}>Expand All</button>
    <button onClick={collapseAll}>Collapse All</button>
    <InfiniteTable<DATA_TYPE> rowDetailState={rowDetailState} />
  </>
);
```

If you prefer the more imperative approach, you can still use the [Row Detail API](https://infinite-table.com/docs/reference/row-detail-api/index.md) to [expand](https://infinite-table.com/docs/reference/row-detail-api/index.md#expandRowDetail) or [collapse](https://infinite-table.com/docs/reference/row-detail-api/index.md#collapseRowDetail) details for rows.

## Single row expand

Using the controlled [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) prop is very powerful - it allows you to configure the expand state to only allow one row to be expanded at a time, if that's something you need.

This means that if any other row(s) are expanded and you expand a new row, the previously expanded rows will all be collapsed.

**Example: Master detail only one row expanded at a time**

In this demo we allow only one row to be expanded at any given time.

We use the controlled [`rowDetailState`](https://infinite-table.com/docs/reference/infinite-table-props.md#rowDetailState) prop to manage the state of the row details and update it by using [`onRowDetailStateChange`](https://infinite-table.com/docs/reference/infinite-table-props.md#onRowDetailStateChange).

```ts file=master-detail-one-expanded-row-example.page.tsx

```
