KevinVandy/material-react-table

Sub rows are not rendered even though they are in the model

soudis opened this issue · 1 comments

material-react-table version

v2.13.0

react & react-dom versions

v18.3.1

Describe the bug and the steps to reproduce it

I created this generic table component that also allows to have a tree view. The sub rows are fetched on clicking the expand button and the table data is copied in order to trigger are rerender. Looking the the table model the subrows are then correctly assigned, but there is not render of the sub rows. Just the expanded icon changes.

Sorry for not providing a more minimal example, but I thing the problem lies somewhere in the details. I struggled for many many hours already and studied multiple other solutions, so I recon there may be a bug.

Please also look at the screenshot where you can see the getSubRow calls and the table model output in the logs. The content of the subrows is not shown, but it is in the correct format.

Minimal, Reproducible Example - (Optional, but Recommended)

"use client";

import { useEffect, useMemo, useState } from "react";
import {
  type MRT_RowSelectionState,
  MaterialReactTable,
  useMaterialReactTable,
  type MRT_ColumnDef,
  type MRT_ColumnFiltersState,
  type MRT_PaginationState,
  type MRT_SortingState,
  type MRT_RowData,
  type MRT_Row,
  type MRT_TableOptions,
  type MRT_ExpandedState,
} from "material-react-table";
import { IconButton, Tooltip } from "@mui/material";
import RefreshIcon from "@mui/icons-material/Refresh";
import { MRT_Localization_DE } from "material-react-table/locales/de";

import { cloneDeep, isEqual } from "lodash";
import { type ProcedureUseQuery } from "node_modules/@trpc/react-query/dist/createTRPCReact";
import { type IdNameSchema, type ListSchema } from "~/schemas/generic";

type ResolverDef<TRowData extends TRowDataBase> = {
  input: ListSchema;
  output: { total: number; data: TRowData[] };
  transformer: true;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  errorShape: any;
};

type TRowDataBase = MRT_RowData & IdNameSchema;

type Props<TRowData extends TRowDataBase> = {
  columns: MRT_ColumnDef<TRowData>[];
  selected?: IdNameSchema[];
  query?: Partial<ListSchema>;
  onSelectionChange?: (selection: IdNameSchema[]) => void;
  useQuery: ProcedureUseQuery<ResolverDef<TRowData>>;
  onClick?: (row: MRT_Row<TRowData>) => void;
  options?: Partial<MRT_TableOptions<TRowData>>;
  getChildren?: (row: TRowData) => IdNameSchema[];
};

export default function GenericTable<TRowData extends TRowDataBase>({
  columns,
  selected,
  onSelectionChange,
  useQuery,
  onClick,
  query,
  options,
  getChildren,
}: Props<TRowData>) {
  const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(
    []
  );
  const [rowSelection, setRowSelection] = useState<MRT_RowSelectionState>(
    (selected ?? []).reduce<MRT_RowSelectionState>((agg, selection) => {
      agg[selection.id] = true;
      return agg;
    }, {})
  );
  const [globalFilter, setGlobalFilter] = useState("");
  const [sorting, setSorting] = useState<MRT_SortingState>([]);
  const [pagination, setPagination] = useState<MRT_PaginationState>({
    pageIndex: 0,
    pageSize: 10,
  });
  const [expanded, setExpanded] = useState<MRT_ExpandedState>({}); //Record<string, boolean> | true

  //consider storing this code in a custom hook (i.e useFetchUsers)
  const {
    data: subRowData = { total: 0, data: [] },
    isRefetching: isRefetchingSubRows,
    isLoading: isLoadingSubRows,
  } = useQuery(
    {
      skip: 0,
      take: 1000,
      parentIds: Object.keys(expanded),
      ...query,
    },
    { enabled: !!getChildren && !!expanded }
  );

  //consider storing this code in a custom hook (i.e useFetchUsers)
  const {
    data = { total: 0, data: [] },
    isError,
    isRefetching,
    isLoading,
    refetch,
  } = useQuery({
    skip: pagination.pageIndex * pagination.pageSize,
    take: pagination.pageSize,
    filter: globalFilter,
    columnFilter: columnFilters.reduce<Record<string, string>>(
      (agg, filter) => {
        agg[filter.id] = filter.value as string;
        return agg;
      },
      {}
    ),
    ...query,
  });

  const rootData = useMemo(() => cloneDeep(data.data), [data, subRowData]);

  const handleRowClick = (row: MRT_Row<TRowData>) => {
    if (onSelectionChange) {
      if (selected?.find((item) => item.id === row.id)) {
        onSelectionChange(selected.filter((item) => item.id !== row.id));
      } else {
        onSelectionChange(
          (selected ?? []).concat({
            id: row.id,
            name: row.getValue<string>("name"),
          })
        );
      }
    }
    onClick?.(row);
  };

  const subRowOptions: Partial<MRT_TableOptions<TRowData>> = getChildren
    ? {
        enableExpandAll: false,
        enableExpanding: true,
        getSubRows: (row) => {
          const childs = getChildren
            ? subRowData.data.filter((subRow) =>
                getChildren(row)
                  .map((child) => child.id)
                  .includes(subRow.id)
              )
            : [];
          console.log("getSubRows", row.id, childs.length);
          return childs;
        },
        manualExpanding: true,
        // paginateExpandedRows: false,
        getRowCanExpand: (row) => getChildren(row.original).length > 0,
      }
    : {};

  const table = useMaterialReactTable({
    localization: MRT_Localization_DE,
    getRowId: (row) => row.id,
    columns,
    data: rootData,
    enableRowSelection: Boolean(onSelectionChange),
    enableMultiRowSelection: Boolean(onSelectionChange),
    manualFiltering: true, //turn off built-in client-side filtering
    manualPagination: true, //turn off built-in client-side pagination
    manualSorting: true, //turn off built-in client-side sorting
    ...subRowOptions,
    positionToolbarAlertBanner: "none",
    muiTablePaperProps: {
      sx: { boxShadow: "none" },
    },
    muiToolbarAlertBannerProps: isError
      ? {
          color: "error",
          children: "Error loading data",
        }
      : undefined,
    muiTableBodyRowProps: ({ row }) => ({
      //add onClick to row to select upon clicking anywhere in the row
      onClick: (event) => {
        if (!event.defaultPrevented) {
          handleRowClick(row);
        }
      },

      sx: { cursor: "pointer" },
    }),
    muiSelectCheckboxProps: ({ row }) => ({
      onClick: () => handleRowClick(row),
    }),
    onColumnFiltersChange: setColumnFilters,
    onGlobalFilterChange: setGlobalFilter,
    onPaginationChange: setPagination,
    onExpandedChange: setExpanded,
    onSortingChange: setSorting,
    onRowSelectionChange: setRowSelection,
    renderTopToolbarCustomActions: () => (
      <>
        <Tooltip arrow title="Refresh Data">
          <IconButton onClick={() => refetch()}>
            <RefreshIcon />
          </IconButton>
        </Tooltip>
      </>
    ),
    rowCount: data.total ?? 0,
    ...options,
    initialState: {
      showColumnFilters: false,
      showGlobalFilter: true,
      ...options?.initialState,
    },
    state: {
      expanded,
      rowSelection,
      columnFilters,
      globalFilter,
      isLoading: isLoading,
      pagination,
      showAlertBanner: isError,
      showProgressBars: isRefetching || isRefetchingSubRows,
      sorting,
      ...options?.state,
    },
  });

  useEffect(() => {
    setTimeout(() => {
      table
        .getRowModel()
        .rows.forEach((row) =>
          console.log("row model", row.id, "subrow length", row.subRows?.length)
        );
    }, 1000);
  }, [subRowData]);

  useEffect(() => {
    const newSelection = (selected ?? []).reduce<MRT_RowSelectionState>(
      (agg, selection) => {
        agg[selection.id] = true;
        return agg;
      },
      {}
    );
    if (!isEqual(Object.keys(newSelection), Object.keys(rowSelection))) {
      setRowSelection(newSelection);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [selected]);

  return <MaterialReactTable table={table} />;
}

Screenshots or Videos (Optional)

screenshot

Do you intend to try to help solve this bug with your own PR?

No, because I do not know how

Terms

  • I understand that if my bug cannot be reliably reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.

If you set manualExpanding to true, then MRT/TanStack Table will disable its internal logic to expand rows and assume that the data you pass in already has the rows that are expanded in the root level.