refinedev/refine

[BUG] `useTable` from `@refinedev/react-table` causes infinite rendering

Opened this issue ยท 9 comments

Describe the bug

I was trying to create a list view following up the example of useTable. However, when I got to the page, the page was rendering infinitely as the following:

github-issue-reproduction

It seems like the tableQuery's status remains its fetchStatus as "fetching", but there is no calling to the data provider's getList client action, and the data remains undefined, so no data can be displayed.

image

Steps To Reproduce

My code is mostly the same with the usage example. The difference is it is built on top of Next.js's app routing.

Here is my Refine's layout:

<Refine
  routerProvider={routerProvider}
  dataProvider={DataProviderClient}
  authProvider={authProvider}
  notificationProvider={useNotificationProvider}
  resources={[
    {
       name: "users",
       list: "users",
       create: "users/create",
       edit: "users/:id/edit",
       show: "users/:id",
    },
  ]}
  options={{
    syncWithLocation: true,
    warnWhenUnsavedChanges: true,
    useNewQueryKeys: true,
  }}
>
  {children}
</Refine>

Here is my page at /src/app/users/page.tsx

"use client";

import { HStack, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react";
import { ColumnFilter } from "@components/DataDisplay/ColumnFilter";
import { Pagination } from "@components/Pagination";
import { capitalize } from "@lib/helpers/string.helper";
import { DateField, EditButton, List, ShowButton, TagField, TextField } from "@refinedev/chakra-ui";
import { useTable } from "@refinedev/react-table";
import { ColumnDef, flexRender } from "@tanstack/react-table";
import { useMemo } from "react";
import { UserProps } from "./schema/user.schema";

const LIST_PROPS_AS_TEXT = ["name", "email"];

export default function UsersPage() {
  const columns = useMemo<ColumnDef<UserProps>[]>(
    () => [
      {
        id: "_id",
        header: "ID",
        accessorKey: "_id",
        meta: {
          filterOperator: "eq",
        },
      },
      ...LIST_PROPS_AS_TEXT.map((field) => ({
        id: field,
        header: capitalize(field),
        accessorKey: field,
      })),
      {
        id: "roles",
        header: "Roles",
        accessorKey: "roles",
        cell: (props) => {
          return (
            <HStack>
              {(props.getValue() as string[]).map((role: string, index) => (
                <TagField key={index} value={role} />
              ))}
            </HStack>
          );
        },
      },
      {
        id: "createdAt",
        header: "Created At",
        accessorKey: "createdAt",
        cell: (props) => <DateField value={props.getValue() as string} format="HH:mm DD/MM/YYYY" />,
      },
      {
        id: "actions",
        header: "Actions",
        accessorKey: "_id",
        cell: (props) => {
          return (
            <HStack>
              <EditButton recordItemId={props.getValue() as string} />
              <ShowButton recordItemId={props.getValue() as string} />
            </HStack>
          );
        },
      },
    ],
    [],
  );

  const {
    getHeaderGroups,
    getRowModel,
    refineCore: { tableQuery, pageCount, current, setCurrent },
  } = useTable<UserProps>({ columns });

  console.log("๐Ÿš€ ~ UsersPage ~ Render", tableQuery);
  const total = tableQuery?.data?.total ?? 0;

  return (
    <List>
      <p>Total: {total}</p>
      <TableContainer marginTop={12}>
        <Table variant="simple">
          <Thead>
            {getHeaderGroups().map((headerGroup) => {
              return (
                <Tr key={headerGroup.id}>
                  {headerGroup.headers.map((header) => {
                    return (
                      <Th key={header.id}>
                        {!header.isPlaceholder && (
                          <HStack spacing={2}>
                            <TextField
                              value={flexRender(
                                header.column.columnDef.header,
                                header.getContext(),
                              )}
                            />
                            <ColumnFilter<UserProps> column={header.column} />
                          </HStack>
                        )}
                      </Th>
                    );
                  })}
                </Tr>
              );
            })}
          </Thead>
          <Tbody>
            {getRowModel().rows.map((row) => {
              return (
                <Tr key={row.id}>
                  {row.getVisibleCells().map((cell) => {
                    return (
                      <Td key={cell.id}>
                        {flexRender(cell.column.columnDef.cell, cell.getContext())}
                      </Td>
                    );
                  })}
                </Tr>
              );
            })}
          </Tbody>
        </Table>
      </TableContainer>
      <Pagination current={current} pageCount={pageCount} setCurrent={setCurrent} />
    </List>
  );
}

Expected behavior

  • I expect to have the data rendered when visiting the page.

Packages

โ”œโ”€โ”€ next@14.2.5
โ”œโ”€โ”€ react@18.3.1
โ”œโ”€โ”€ react-dom@18.3.1
โ”œโ”€โ”€ @refinedev/react-table@5.6.13
โ”œโ”€โ”€ @tanstack/react-table@8.20.1
โ”œโ”€โ”€ @refinedev/core@4.54.0
โ”œโ”€โ”€ @refinedev/nextjs-router@6.1.0
โ”œโ”€โ”€ @refinedev/chakra-ui@2.32.0
โ”œโ”€โ”€ @chakra-ui/react@2.8.2

Additional Context

Btw, I saw in the source code that it may pass an empty array [] to the TanStack's reactTable here.

Wondering if it has any effect on the infinite rendering of useTable. Because there was a TanStack's issue that is quite similar to it.

Hey @khoaxuantu, I couldn't reproduce the issue from the code you've provided. Could you provide a repository with a minimal repro? That would be great! Thanks.

@BatuhanW Sure! It's quite complicated to reproduce minimally, but I just have created a branch of my project here to reproduce it. I have mocked the login and the data so that we can visit the /users page and look into the issue directly.
Would you mind checking it out?
Thanks

Just checked out the codebase you've provided @khoaxuantu. I was able to reproduce the same rendering issue with the initial setup. Then it resolved when I removed the onClick prop of the prev button in the <Pagination /> component. Can you check if this works? Instead of using current - 1, I've updated it to pagination.prev, not sure if this is something related with the caches but it started working on every test I make ๐Ÿ˜…

@aliemir It doesn't work 100% correct, sadly. I have tried both current - 1 and pagination.nev in the <Pagination /> component as you said. The issue in both occurred as follows:

  • From the root path /, I navigated to the /users (via navigation sidebar), I got the issue.
  • Then I clicked on the pagination buttons, and it works. It did call the getList action, fetch the data successfully, and stop rendering infinitely.
  • Then I navigated to /, then reloaded the whole site, and clicked on the Users navigation sidebar again, the issue occurred again

Another thing that I noticed is that if I visit the /users path by entering the url directly, the page is renderred successfully.
So I don't think the problem is from the pagination component

Hey @khoaxuantu you are right! My bad, I only tested out in the same page and hot reloading fixed the issue when I made changes in the <Pagination /> ๐Ÿคฆ I investigated a bit more into the issue, it looks like its only related to the useTable of @refinedev/react-table, when I switch to using useTable from @refinedev/core the issue did not occur.

I've found that the issue happens when syncing sorters and filters from React Table to Refine. We did not had a check if the current states are equal or not and left with repeated calls to setFilters. This caused queries to stuck at loading without properly calling the API. As a workaround I saw that setting syncWithLocation to false resolves the issue. For a fix, we need to add equality checks for filters and sorters in their effects.

When the issue is stuck at loading, Refine's overtime interval keeps running, this was causing your console.log to run repeatedly every second. It looks like an infinite rendering issue but its really logging due to overtime.elapsedTime getting updated ๐Ÿ˜…

In my local, I've tested out using lodash/isEqual before calling setFilters in a useEffect of useTable and it seems to resolve the issue ๐Ÿค”

Let us know if syncWithLocation: false workaround is working for you, then we can discuss about the fix. Also let us know if you want to work on the fix, we'll be happy to see your contribution ๐Ÿ™

Hey @aliemir, I just tried setting syncWithLocation to false and it worked as expected. I think I will go with this workaround for now.
Because of my limited time, I think it's better to let you fix the issue and I hope to be able to turn on the option in the future releases soon.
Thank you very much for the support ๐Ÿ™‡

Hey @aliemir, were you suggesting something like this?

if (!isEqual(crudFilters, filtersCore)) {
      setFilters(crudFilters);
}

Hey @Anonymous961 yeah this is what I tried, would love to hear from you if you can spare some time to work on this ๐Ÿ™

Hey @aliemir, were you suggesting something like this?

if (!isEqual(crudFilters, filtersCore)) {
      setFilters(crudFilters);
}

Ye something like that!