TanStack Table version
8.21.3
Framework/Library version
19.1.1
Describe the bug and the steps to reproduce it
Hello :),
I'm facing an annoying issue with my implementation of the React-Table.
Everytime a state changes, all the table cells are reset, meaning that the components that are inside and their states are reset too. What can I do to prevent this ? I tried to memoize the columns definition, setting a function getRowId that could help identifying the row to prevent its reset, setting autoResetAll to false but nothing worked.
The code sandbox that I share is a minimal working code of what I pasted under.
import * as React from "react";
import * as Table from "@tanstack/react-table";
export type GridPagination = Table.PaginationState;
export type GridSorting = Table.ColumnSort;
type Formatter = (value?: any, info?: any) => React.JSX.Element;
//TODO: Gérer le cas où -alors que l'on est pas sur la première page- le contenu du Grid change et pageCount passe < page sur laquelle on se trouve.
// =>Exemple: il y a huit pages et on est sur la 7ème, le contenu change: il n’y en a plus que 4 => Bug, on est toujours sur la 7ème page.
type GridProps<T> = React.TableHTMLAttributes<HTMLTableElement> & {
data?: (T & { hasError?: boolean })[];
children: React.ReactNode;
className?: string;
classNames?: {
main?: string;
inner?: string;
};
isFetching?: boolean;
enableFooter?: boolean;
autoSorting?: boolean;
initialSorting?: Table.ColumnSort;
counter?: (showed: number) => string;
enablePagination?: boolean;
autoPagination?: boolean;
pageSize?: number;
pageCount?: number;
borderStyle?: "none" | "horizontal";
headerAlign?: "left" | "center" | "right";
sessionPrefix?: string;
onSortingChange?: (sorting?: Table.ColumnSort) => void;
onPaginationChange?: (pagination: Table.PaginationState) => void;
getRowId?: (originalRow: T, index: number) => string; // Si même id, pas de re-render
onRowClick?: (
e: React.MouseEvent<HTMLTableRowElement, MouseEvent>,
dataRow: any,
) => void;
};
type GridColumnProps = {
enableSorting?: boolean;
header: ((props: any) => React.ReactNode) | string;
footer?: ((props: any) => React.ReactNode) | string;
name: string;
size?: number | "auto";
minSize?: number;
maxSize?: number;
headerAlign?: "left" | "center" | "right";
cell?: (info?: any) => React.ReactNode;
formatter?: Formatter;
};
function getInitialPagination(
pageSize: number,
sessionPrefix?: string,
pageCount?: number,
) {
const init = { pageIndex: 0, pageSize };
if (!sessionPrefix) return init;
const sessionStoragePagination = sessionStorage.getItem(
`${sessionPrefix}-pagination`,
);
const storedPagination = sessionStoragePagination
? JSON.parse(sessionStoragePagination)
: init;
if (pageCount) {
return {
...storedPagination,
pageIndex:
storedPagination.pageIndex > pageCount - 1
? 0
: storedPagination.pageIndex,
};
}
return storedPagination;
}
export default function Grid<T>({
data,
children,
className,
classNames,
isFetching,
enableFooter = false,
autoSorting = false,
initialSorting,
counter,
enablePagination = false,
autoPagination = false,
pageSize = 10,
pageCount,
borderStyle,
headerAlign = "left",
sessionPrefix,
onSortingChange,
onPaginationChange,
getRowId,
onRowClick,
...props
}: GridProps<T>) {
const originalColumns = React.useRef<GridColumnProps[]>([]);
const [pagination, setPagination] = React.useState<Table.PaginationState>(
getInitialPagination(pageSize, sessionPrefix, pageCount),
);
const [sorting, setSorting] = React.useState<Table.SortingState>(
initialSorting ? [initialSorting] : [],
);
const _onSortingChange = (fn: Table.Updater<Table.SortingState>) => {
const updated = typeof fn === "function" ? fn(sorting) : sorting;
if (onSortingChange && updated.length > 0) onSortingChange(updated[0]);
setSorting(updated);
};
const _onPaginationChange = (fn: Table.Updater<Table.PaginationState>) => {
const updated = typeof fn === "function" ? fn(pagination) : pagination;
if (onPaginationChange) onPaginationChange(updated);
if (sessionPrefix) {
sessionStorage.setItem(
`${sessionPrefix}-pagination`,
JSON.stringify(updated),
);
}
setPagination(updated);
};
const columns = React.useMemo(() => {
originalColumns.current = [];
for (const child of React.Children.toArray(children)) {
const element = child as React.ReactElement<GridColumnProps>;
if (element.type === Grid.Column) {
originalColumns.current.push(element.props);
} else {
throw new Error("Grid MUST only contain Grid.Column");
}
}
return originalColumns.current.map(
(column) =>
({
accessorKey: column.name as keyof T,
enableSorting: column.enableSorting === true,
size: column.size === "auto" ? undefined : column.size,
minSize: column.minSize,
maxSize: column.maxSize,
cell:
column.cell ||
((info: any) => {
const cellContent = column.formatter
? column.formatter(info.getValue(), info)
: info.getValue();
return <DefaultCell>{cellContent}</DefaultCell>;
}),
header: (props: any) =>
typeof column.header === "function"
? column.header(props)
: column.header,
footer: (props: any) =>
typeof column.footer === "function"
? column.footer(props)
: column.footer,
}) as Table.ColumnDef<T & { hasError?: boolean }>,
);
}, [children]);
const sortHandler =
<A, B>(header: Table.Header<A, B>) =>
(e: React.MouseEvent | React.KeyboardEvent) => {
e.stopPropagation();
const handler = header.column.getToggleSortingHandler();
if (handler && header.column.getCanSort()) {
handler(e);
}
};
const table = Table.useReactTable<
T & {
hasError?: boolean | undefined;
}
>({
data: data ?? [],
columns,
pageCount: pageCount !== undefined ? pageCount : -1,
state: { sorting, pagination },
manualSorting: !autoSorting,
enableSortingRemoval: false,
manualPagination: !autoPagination,
getPaginationRowModel: autoPagination
? Table.getPaginationRowModel()
: undefined,
getCoreRowModel: Table.getCoreRowModel(),
getSortedRowModel: autoSorting ? Table.getSortedRowModel() : undefined,
onSortingChange: !autoSorting ? _onSortingChange : undefined,
onPaginationChange: _onPaginationChange,
getRowId: getRowId ?? ((originalRow) => JSON.stringify(originalRow)),
});
const currentSorting = sorting.length > 0 ? sorting[0] : undefined;
return (
<div>
<div>
<div>
<table cellSpacing={0} {...props}>
<tbody>
{table.getRowModel().rows.map((row) => {
return (
<tr
key={row.id}
onClick={(e) => {
if (onRowClick) {
onRowClick(e, row);
}
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (onRowClick) {
e.target.dispatchEvent(
new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true,
}),
);
e.stopPropagation();
e.preventDefault();
}
}
}}
tabIndex={onRowClick ? 0 : undefined}
>
{row.getVisibleCells().map((cell) => {
return (
<td key={cell.id}>
{Table.flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</td>
);
})}
</tr>
);
})}
</tbody>
{enableFooter && (
<tfoot>
{table.getFooterGroups().map((footerGroup) => (
<tr key={footerGroup.id}>
{footerGroup.headers.map((header) => (
<th key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder
? null
: Table.flexRender(
header.column.columnDef.footer,
header.getContext(),
)}
</th>
))}
</tr>
))}
</tfoot>
)}
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header, index) => {
const currentColumn = originalColumns.current[index];
const current = currentSorting?.id === header.id;
return (
<th
key={header.id}
colSpan={header.colSpan}
style={{
width:
currentColumn.size === "auto"
? undefined
: `${
currentColumn.size ??
header.column.columnDef.maxSize
}px`,
minWidth:
header.column.columnDef.minSize !== undefined
? `${header.column.columnDef.minSize}px`
: undefined,
maxWidth:
header.column.columnDef.maxSize !== undefined
? `${header.column.columnDef.maxSize}px`
: undefined,
}}
>
<div onClick={sortHandler(header)}>
{header.isPlaceholder ? null : (
<span>
{Table.flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</span>
)}
</div>
</th>
);
})}
</tr>
))}
</thead>
</table>
</div>
</div>
{(counter || enablePagination) && (
<div>
{data !== undefined && counter && (
<div>{counter(table.getRowModel().rows.length)}</div>
)}
</div>
)}
</div>
);
}
Grid.Column = function Column(_: GridColumnProps) {
return null;
};
function DefaultCell(props: React.HTMLAttributes<HTMLDivElement>) {
return <div {...props} />;
}
Grid.DefaultCell = DefaultCell;
Exemple of usage:
import * as React from "react";
import * as Auth from "oauth2";
import * as History from "history";
import * as Query from "react-query-carnet-rouge";
import * as Router from "react-router-dom";
import classnames from "classnames";
import * as Api from "../../../services/api/esf-academy";
import * as Constants from "../../../services/constants";
import Loader from "@valraiso-esf/esf-ui/es/loader-circle";
import Grid, {
GridPagination,
GridSorting,
} from "@valraiso-esf/esf-ui/es/grid";
import Empty from "@valraiso-esf/esf-ui/es/empty";
import Button from "@valraiso-esf/esf-ui/es/button";
import ExpandLess from "@valraiso-esf/esf-icons/es/expand-less";
import ExpandMore from "@valraiso-esf/esf-icons/es/expand-more";
import css from "./desktop.module.css";
const defaultPagination = {
pageIndex: 0,
pageSize: Constants.PAGE_SIZE,
};
type FormationParticipantFormationInfosRequiredFormation = Omit<
FormationParticipantFormationInfos,
"formation"
> & {
formation: Formation;
};
type FormationParticipantHistorique_ = Omit<
FormationParticipantHistorique,
"historique"
> & {
historique: FormationParticipantFormationInfosRequiredFormation[];
};
type LigneHistorique = Moniteur &
FormationParticipantFormationInfosRequiredFormation & {
firstInstanceMoniteur: boolean;
lastInstanceMoniteur: boolean;
oneElementInHistorique: boolean;
historiqueWrapped: boolean;
displayed: boolean;
};
type Props = {
filters: Api.FormationsParticipantsFilters;
sorting: Api.FormationsParticipantsHistoriqueSort;
setSorting: (sorting: Api.FormationsParticipantsHistoriqueSort) => void;
};
export default function Desktop({ filters, sorting, setSorting }: Props) {
const { user } = Auth.useAuth();
const [pagination, setPagination] = History.useState<GridPagination>(
"pagination",
defaultPagination,
);
const [unwrappedMoniteurs, setUnwrappedMoniteurs] = History.useState<
Set<number>
>("unwrappedMoniteurs", new Set());
const queryHistorique = Query.useQuery({
queryKey: [
"formations",
"participants",
"historique",
filters,
{
fetchFormations: true,
},
sorting,
pagination,
],
queryFn: async () => {
return Api.fetchFormationsParticipantsHistorique(
filters,
{
fetchFormations: true,
},
sorting,
pagination,
);
},
placeholderData: Query.keepPreviousData,
});
const historiqueData = React.use(queryHistorique.promise);
const moniteurs = historiqueData?.historique as
| FormationParticipantHistorique_[]
| undefined;
const pageCount = historiqueData?.pageCount;
const count = historiqueData?.count;
const rows = React.useMemo(() => {
if (moniteurs === undefined) return undefined;
const rows: LigneHistorique[] = [];
moniteurs.forEach((moniteur) => {
moniteur.historique.forEach((h, index) => {
const firstInstanceMoniteur = index === 0;
const lastInstanceMoniteur = index === moniteur.historique.length - 1;
const oneElementInHistorique = moniteur.historique.length === 1;
const historiqueWrapped =
!oneElementInHistorique &&
!unwrappedMoniteurs.has(moniteur.noMoniteur);
const displayed = firstInstanceMoniteur || !historiqueWrapped;
rows.push({
...moniteur,
...h,
firstInstanceMoniteur,
lastInstanceMoniteur,
oneElementInHistorique,
historiqueWrapped,
displayed,
});
});
});
return rows;
}, [moniteurs, unwrappedMoniteurs]);
const onSortingChange = (sorting: GridSorting | undefined) => {
if (
sorting !== undefined &&
(sorting.id === "moniteurNom" || sorting.id === "formationsDateBegin")
) {
setSorting(sorting as Api.FormationsParticipantsHistoriqueSort);
}
};
const toggleUnwrap = (noMoniteur: number) => {
setUnwrappedMoniteurs((prev) => {
const newSet = new Set(prev);
if (newSet.has(noMoniteur)) {
newSet.delete(noMoniteur);
} else {
newSet.add(noMoniteur);
}
return newSet;
});
};
if (user === undefined) return null;
return (
<>
{moniteurs === undefined ? (
<Loader />
) : moniteurs?.length === 0 ? (
<div className={css.messageNoParticipant}>
<Empty illustration="file-search">
Aucun historique pour le moment.
</Empty>
</div>
) : (
<Grid<LigneHistorique>
data={rows}
isFetching={queryHistorique.isFetching}
className={css.grid}
enablePagination
pageCount={pageCount}
counter={
count !== undefined
? (showed) =>
`${showed} ${showed > 1 ? "participants" : "participant"} sur ${
count
}`
: undefined
}
tabIndex={pagination.pageIndex}
pageSize={pagination.pageSize}
onPaginationChange={setPagination}
initialSorting={sorting}
onSortingChange={(sorting) =>
onSortingChange(
sorting !== undefined
? ({
...sorting,
id: sorting.id === "id" ? "nom" : sorting.id,
} as GridSorting)
: undefined,
)
}
>
<Grid.Column
name="unwrap"
header=""
cell={(info) => {
const ligneHistorique = info.row.original as LigneHistorique;
const classes = classnames(css.cell, css.unwrap, {
[css.firstInstanceMoniteur]:
ligneHistorique.firstInstanceMoniteur,
[css.lastInstanceMoniteur]:
ligneHistorique.lastInstanceMoniteur,
[css.oneElementInHistorique]:
ligneHistorique.oneElementInHistorique,
[css.historiqueWrapped]: ligneHistorique.historiqueWrapped,
[css.displayed]: ligneHistorique.displayed,
});
return (
<Grid.DefaultCell className={classes}>
<Button
onClick={() => toggleUnwrap(ligneHistorique.noMoniteur)}
color="tertiary"
size="M"
icon
inert={
!ligneHistorique.firstInstanceMoniteur ||
ligneHistorique.oneElementInHistorique
}
className={css.button}
>
{ligneHistorique.historiqueWrapped ? (
<ExpandMore />
) : (
<ExpandLess />
)}
</Button>
</Grid.DefaultCell>
);
}}
size={1}
/>
</Grid>
)}
</>
);
}
What should I do to solve my issue ?
Thank you for reading me.
Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
https://stackblitz.com/edit/vitejs-vite-bmmacmfl?file=src%2FDesktop.tsx
Screenshots or Videos (Optional)
No response
Do you intend to try to help solve this bug with your own PR?
No, because I do not know how
Terms & Code of Conduct
TanStack Table version
8.21.3
Framework/Library version
19.1.1
Describe the bug and the steps to reproduce it
Hello :),
I'm facing an annoying issue with my implementation of the React-Table.
Everytime a state changes, all the table cells are reset, meaning that the components that are inside and their states are reset too. What can I do to prevent this ? I tried to memoize the columns definition, setting a function getRowId that could help identifying the row to prevent its reset, setting autoResetAll to false but nothing worked.
The code sandbox that I share is a minimal working code of what I pasted under.
Exemple of usage:
What should I do to solve my issue ?
Thank you for reading me.
Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
https://stackblitz.com/edit/vitejs-vite-bmmacmfl?file=src%2FDesktop.tsx
Screenshots or Videos (Optional)
No response
Do you intend to try to help solve this bug with your own PR?
No, because I do not know how
Terms & Code of Conduct