How to handle one to many relationship in TanStack Table (React-Table V8)
Asked Answered
A

1

15

I'm currently trying Tanstack Table, which is a great library!

What I'm trying to do is build up a table that looks like this:

Table that I want

My data come from an API with this type:

type Accounting = {
  category: string;
  total: number; // Sum of all expenses
  expenses: {
    name: string;
    value: number;
  }[];
};

Here is what I've got so far:

const columns = React.useMemo(
    () => [
      columnHelper.accessor("category", {
        header: "Category",
        id: "category",
        cell: (info) => {
          return (
            <>
              {info.row.getCanExpand() && (
                <button
                  {...{
                    onClick: info.row.getToggleExpandedHandler(),
                    style: { cursor: "pointer" },
                    className: "mr-2",
                  }}
                >
                  {info.row.getIsExpanded() ? (
                    <Chevron direction="down" />
                  ) : (
                    <Chevron direction="right" />
                  )}
                </button>
              )}
              {info.getValue()}
            </>
          );
        },
      }),
      columnHelper.accessor("expenses", {
        header: undefined,
        id: "expense-name",
        cell: (info) => {
          // FIXME : this returns a table of objects, I want one row per expense
          return info.getValue();
        },
      }),
      columnHelper.accessor("expenses", {
        aggregationFn: "sum",
        id: "expense-value",
        header: "Expense",
        cell: (info) => {
          // FIXME : this returns a table of objects, I want one row per expense
          return info.getValue();
        },
      }),
    ],
    []
  );

  const grouping = React.useMemo(() => ["category"], []);

  const table = useReactTable({
    data,
    columns,
    state: {
      grouping,
      expanded,
    },
    getExpandedRowModel: getExpandedRowModel(),
    getGroupedRowModel: getGroupedRowModel(),
    getCoreRowModel: getCoreRowModel(),
    onExpandedChange: setExpanded,
    autoResetExpanded: false,
  });

The problem here is that I've got a One-To-Many relationship between one category and multiple expenses for this category.

The above code does not work because tanstack table tries to render a list of objects (a list of expense).

I haven't figure out how Tanstack Table can handle this ? Should I override some sort of render method ? Is Tanstack Table the good choice for this kind of data ?

Respectfully.

Aunt answered 13/9, 2022 at 7:5 Comment(0)
C
23

You can draw a lot from the React Table Expanding Example.

The critical part which you are missing is the getSubRows function on the table. Rather than rendering the individual expenses within the cell of the parent category, we want to create a new row in the table for each expense.

Our two columns are a title and a number. For a top-level Accounting object, it is the category and the total. For an individual expense, it's the name and value. Since those are different properties, we'll need to do some mapping.

Let's start really simple and define those two columns for the top-level objects:

const columns = React.useMemo(
  () => [
    columnHelper.accessor("category", {
      header: "Category",
      id: "category"
    }),
    columnHelper.accessor("total", {
      header: "Expense",
      id: "expense-value"
    })
  ],
  []
);

enter image description here


Now, let's add in the expand functionality. (You do not need the grouping functionality.)

We will store the expanded state in the component and pass it to the table, syncing changes through the onExpandedChange option.

We will add a new getSubRows option to your table. This is a function that takes the parent Accounting object and returns an array of child Accounting objects. As explained earlier, we need to rename some properties to make the interfaces match up. Our columns config expects an object with category and total. I set an empty array of expenses here because the child rows do not have children of their own.

const [expanded, setExpanded] = React.useState<ExpandedState>({});

const table = useReactTable({
  data,
  columns,
  state: {
    expanded
  },
  getExpandedRowModel: getExpandedRowModel(),
  getCoreRowModel: getCoreRowModel(),
  onExpandedChange: setExpanded,
  getSubRows: (originalRow) =>
    originalRow.expenses.map((expense) => ({
      category: expense.name,
      total: expense.value,
      expenses: []
    }))
});

For your columns, the only change that I made from the previous is to display an expand/contract button on the parent rows (which you already had in your attempt).

const columns = React.useMemo(
  () => [
    columnHelper.accessor("category", {
      header: "Category",
      id: "category",
      cell: (info) => {
        return (
          <>
            {info.row.getCanExpand() && (
              <button onClick={info.row.getToggleExpandedHandler()}>
                {info.row.getIsExpanded() ? "-" : "+"}
              </button>
            )}
            {info.getValue()}
          </>
        );
      }
    }),
    columnHelper.accessor("total", {
      header: "Expense",
      id: "expense-value"
    })
  ],
  []
);

enter image description here

Now we have fully-functional row expansion. It's not beautiful but I'll leave it to you to add styling.

CodeSandbox Demo

Cathycathyleen answered 1/1, 2023 at 19:13 Comment(2)
Thank you so much for your help! Exactly what I was looking for, and really well explained :)Frenchy
Can you provide an edited version that expand one and collapse other?Jaco

© 2022 - 2025 — McMap. All rights reserved.