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)
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.