reactjsreact-table-v7

React Table - setting up a collapsible row of data


I am currently trying to implement React-Table, with a data structure which matches this typescript definition.

export type VendorContent = {
  id: number;
  name: string;
  user_name: string;
  dob: string;
};

export type VendorData = {
  vendor: string;
  rows: VendorContent[];
};


<DataTable defaultData={vendorData} />

Structurally, the design I have looks like this:

enter image description here

Within the DataTable itself, I have something like this:

const columns = [
  columnHelper.display({
    id: 'actions',
    cell: (props) => <p>test</p>,
  }),
  columnHelper.accessor('name', {
    header: () => 'Name',
    cell: (info) => info.renderValue(),
  }),
  columnHelper.accessor('user_name', {
    header: () => 'User name',
    cell: (info) => info.renderValue(),
  }),
  columnHelper.accessor('dob', {
    header: () => 'DOB',
    cell: (info) => info.renderValue(),
  }),
];

    const DataTable = (props: DataTableProps) => {
      const { defaultData } = props;
      const [data, setData] = React.useState(() =>
        defaultData.flatMap((item) => item.rows)
      );
    
      const table = useReactTable({
        data,
        columns,
        getCoreRowModel: getCoreRowModel(),
      });

Now, here's the kicker. Vendor1, and Vendor2 are collapsible rows, and need to be somehow passed into the table, but the defaultData.flatMap((item) => item.rows) which sets up each row, is obviously removing this information / structure. Ergo, I've nothing to hook into to try and render that in the table.

Things I've tried:

  const [data, setData] = React.useState(() =>
        defaultData
      );

Once I try and pass the full Data object in, the column definition complains. (Data passed is no longer an array).

getSubRows within the React Table hook seems to require a full definition of all the columns (all I want is the vendor name there).

Header groups seem to be rendered before the headings, but what I actually want is almost a 'row group' that is expandable / collapsible?

How would I achieve a design similar to the below, with a data structure as illustrated, such that there are row 'headings' which designate the vendor?

I've setup a codesandbox here that sort of illustrates the problem: https://codesandbox.io/s/sad-morning-g5is0e?file=/src/App.js


Solution

  • First steps

    Starting from this docs and this example from docs we can create a colapsable row like this (click on the vendor to expand/collapse next rows).

    Steps to do it:

    1. Import useExpanded and add it as the second argument of useTable (after the object containing { columns, data })
    2. Replace defaultData.flatMap((item) => item.rows) with myData.map((row) => ({ ...row, subRows: row.rows })) (or if you can just rename rows to subRows and you can just send defaultData (without any mapping or altering of the data).
    3. Add at the beginning of const columns = React.useMemo(() => [ the following snippet:

    {
      id: "vendor",
      Header: ({ getToggleAllRowsExpandedProps }) => (
        <span {...getToggleAllRowsExpandedProps()}>VENDOR</span> // this can be changed
      ),
      Cell: ({ row }) =>
        row.original.vendor ? <span {...row.getToggleRowExpandedProps({})}>row.vendor</span> : null, // render null if the row is not expandable
    },
    
    4. Add the DOB to the columns

    Formatting the rows

    With some reverse engineering from this question (using colspan) we can render only one value per row (reverse because we want the main row to use all 4 cells).

    This will also make the first header part very small and lead to something like this for example.

    How we got here from First steps:

    1. We rendered the first cell if the original row has any value for vendor key and
    2. We expanded the cell (in this case) for a span of 4 rows

    Main difference in a snippet:

    {
      row.original.vendor ? (
        <td {...row.cells[0].getCellProps()} colSpan={4}>
          {row.cells[0].render("Cell")}
        </td>
      ) : (
        row.cells.map((cell) => {
          return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
        })
      );
    }

    Unfortunately I don't think there is another (easier / more straight forward) way to do it (I mean I don't think this is bad, but I think it can be confusing especially if you try to figure it out searching trough so many pages of docs and there is no guide in this direction as far as I know).

    Also please note I tried to highlight and explain the process. There might be some small extra adjustments needed in the code.