TanStack/table

React Infinite render loop , when passing data as undefined when table is using useSortBy and usePagination Hooks.

rayanfer32 opened this issue · 9 comments

Describe the bug

I have reproduced the problem in this repl , please check it out.
https://stackblitz.com/edit/react-xutun7?file=src%2FApp.js,src%2FTable.js

Table.js

import React from 'react';
import { useTable, usePagination, useSortBy } from 'react-table';

export default function Table({ data = [], columns = [] }) {
  // bug: when using useSortBy and usePagination hooks we cannot pass the data as undefined
  // The sad part is i cannot handle the error inside the table component by using  data || []
  // After starring at my peice of code for 17hrs now i finally found the fix.
//  it was the empty array initialization done in the parameters of Table component, 
// which confused react somehow to go into infinte render loop

  // To get rid of the render loop error just remove the empty array passed to the data parameter

  // * this is a workaround, seems working bcz the data here has changed to [] somehow after couple of render tries
  if (!data) {
    console.log('Catching undefined data');
    return;
  }

  let tableInstance = useTable(
    { data: data || [], columns },
    useSortBy,
    usePagination
  );

  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

App.js

import React from 'react';
import Table from './Table';
import './style.css';

export default function App() {
  const columns = [
    {
      Header: 'Supply',
      accessor: 'currentsupply',
    },
    {
      Header: 'Max Supply',
      accessor: 'maxsupply',
    },
  ];
  return (
    <div>
      <Table columns={columns} data={undefined} />
    </div>
  );
}
Error in /turbo_modules/react-dom@18.2.0/cjs/react-dom.development.js (27292:11)
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.

Your minimal, reproducible example

https://stackblitz.com/edit/react-xutun7?file=src%2FApp.js,src%2FTable.js

Steps to reproduce

https://stackblitz.com/edit/react-xutun7?file=src%2FApp.js,src%2FTable.js

Expected behavior

Should not cause render loop

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

Windows / Linux

react-table version

7.8.0

TypeScript version

4.6.2

Additional context

No response

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.

Removing the empty array passed to data solves the problem , but I don't understand why

Before

export default function Table({ data = [], columns = [] }) { ... }

After

export default function Table({ data, columns = [] }) { ... }
vemoo commented

Removing the empty array passed to data solves the problem , but I don't understand why

Before

export default function Table({ data = [], columns = [] }) { ... }

After

export default function Table({ data, columns = [] }) { ... }

Because with data = [] data will never be undefined inside Table it will always be an empty array.

The infinite render is caused because you are creating a new empty array on every render and [] !== [].

You can fix it by having a constant empty array:

import React from 'react';
import { useTable, usePagination, useSortBy } from 'react-table';

const emptyArray = [];

export default function Table({ data, columns }) {

  let tableInstance = useTable(
    { data: data || emptyArray, columns },
    useSortBy,
    usePagination
  );

  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

Another thing, columns also has the same problem in this example, it should be memoized.

Removing the empty array passed to data solves the problem , but I don't understand why
Before

export default function Table({ data = [], columns = [] }) { ... }

After

export default function Table({ data, columns = [] }) { ... }

Because with data = [] data will never be undefined inside Table it will always be an empty array.

The infinite render is caused because you are creating a new empty array on every render and [] !== [].

You can fix it by having a constant empty array:

import React from 'react';
import { useTable, usePagination, useSortBy } from 'react-table';

const emptyArray = [];

export default function Table({ data, columns }) {

  let tableInstance = useTable(
    { data: data || emptyArray, columns },
    useSortBy,
    usePagination
  );

  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

Another thing, columns also has the same problem in this example, it should be memoized.

Thanks that explains why my code was going haywire! Have a nice day 😄

The infinite render is caused because you are creating a new empty array on every render and [] !== [].

This is nonsense, passing a new object to a hook should absolutely not cause an infinite render

Take this simple hook as an example

const useTest(arg: []) => {
  return 'test'
}
...
const test = useTest([])
...

The example code does not cause an infinite render

I've looked at the source code of this library and while I cannot pinpoint the exact problem it definitely looks suspect

^ A state update on every render 🤔

You can break react in general by creating a new object on every render and passing that to anything that does Object.is() equality comparisons from render to render, e.g. useEffect

I agree this is nonsense, but that’s just React and technically how most immutable systems handle change detection.

The expectation that Table is going to deep diff everything you pass it is even more absurd, as this would grind most tables to a halt just to determine on every render if it should recompute or not, let alone rerender.

Has anyone successfully resolved this problem? After reviewing all the comments suggesting that columns and data be encapsulated within useMemo, I implemented this approach. However, my application still falls into an infinite loop, and intriguingly, this occurs even when the columns and data are initialized as empty arrays. Incorporating the useSortBy hook further exacerbates the issue, leading to a crash.

Has anyone successfully resolved this problem? After reviewing all the comments suggesting that columns and data be encapsulated within useMemo, I implemented this approach. However, my application still falls into an infinite loop, and intriguingly, this occurs even when the columns and data are initialized as empty arrays. Incorporating the useSortBy hook further exacerbates the issue, leading to a crash.

The render loop was caused by the empty array [] initialization in the parameters, which makes react create a new array every single time it re-renders and as we know in javascript [] !== [] this render loop issue arises. Check if you are doing something similar thing with an object too.

Has anyone successfully resolved this problem? After reviewing all the comments suggesting that columns and data be encapsulated within useMemo, I implemented this approach. However, my application still falls into an infinite loop, and intriguingly, this occurs even when the columns and data are initialized as empty arrays. Incorporating the useSortBy hook further exacerbates the issue, leading to a crash.

Memoizing data, columns, declaring EMPTY_ARRAY outside the react component and then using it inside as a fallback worked for me:

const EMPTY_ARRAY = [];

const Component = ()=> {
...
const table = useTable({
    data: memoizedData || EMPTY_ARRAY,
    columns: memoizedColumns,
...
aau8 commented

Removing the empty array passed to data solves the problem , but I don't understand why
Before

export default function Table({ data = [], columns = [] }) { ... }

After

export default function Table({ data, columns = [] }) { ... }

Because with data = [] data will never be undefined inside Table it will always be an empty array.

The infinite render is caused because you are creating a new empty array on every render and [] !== [].

You can fix it by having a constant empty array:

import React from 'react';
import { useTable, usePagination, useSortBy } from 'react-table';

const emptyArray = [];

export default function Table({ data, columns }) {

  let tableInstance = useTable(
    { data: data || emptyArray, columns },
    useSortBy,
    usePagination
  );

  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

Another thing, columns also has the same problem in this example, it should be memoized.

i try solve this problem 2 days. Thank you, man