Skip to content

How do I prevent my tanstack/react-table from reseting the content of the cells every render ? #6229

@JohnTitour

Description

@JohnTitour

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

  • I agree to follow this project's Code of Conduct
  • I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions