Infinite loading with react-table + react-virtualized / react-window

6.2k Views Asked by At

I have the following requirements to my table:

  1. Table should have fixed header
  2. It needs to autosize: for infinite scroll to work the first fetch should get sufficient amount of data for scroll to appear at all.
  3. table body should work as infinite loader: when scrolled to the end of the list table body should show loading indicator and load more rows

My assumptions are as follows:

  1. as user will scroll through possibly large sets of data I should virtualize lists (react-virtualized seems to be the only good option for me)
  2. as we currently have react-table I want to keep it (it has great mechanism of declaring table rows, columns, accessing data and filtering + sorting)
  3. As we use material ui I need to use material ui react components
  4. Because react-virtualized has own Table component I could use it, but react-table has different way of rendering rows and columns, therefore I have to use List component. (react-table separates rows and columns while react-virtualized uses columns directly as children of Table component)
  5. I saw that react-virtualized works with HOC component called InfiniteLoader, so I should use that as well
  6. Finally I need my columns to not be messed up just because it has more text (i.e. have dynamic height). So I tried to use CellMeasurer for this.

What I was able to achieve can be seen in this sandbox https://codesandbox.io/s/react-table-infinite-mzkkp?file=/src/MuiTable.js (I cannot provide code here because it is quite large)

So, in general I could make Autosizer, CellMeasurer and List components from react-virtualized to work. I am stuck at inifinite scroll part. I saw the example on official docs, but it seems to be a little bit anti pattern (mutation state directly is not a good thing at all)

So I tried to achieve similar result, however if you could see, my loadMore function fires too early for some reason. It leads to requests being sent on nearly every scroll event.

Any help is much appreciated.

What I tried already:

  1. Using react-window instead of react-virtualized It works only for simple use cases, and fails with dynamic size of cells.
  2. Using react-inifnite-scrollcomponent (https://www.npmjs.com/package/react-infinite-scroll-component) It is working for entire page (unable to make "sticky" header, unable to render loading indicators as part of table body, it odes not have any optimization for long lists)
  3. Using Table component from react-virtualized. I was unable to make it work with react-table (as Table component from react-virtualized seems to render Cells directly as children of Table component. I know it has renderRow function, but it means two separate places while react-table has
<TableRow
  {...row.getRowProps({
     style
  })}
  component="div"
  >
  {row.cells.map((cell) => {
     return (
       <TableCell {...cell.getCellProps()} component="div">
         {cell.render("Cell")}
       </TableCell>
      );
   })}
</TableRow>

Also, it is not clear how to render custom filters this way.

3

There are 3 best solutions below

0
On

I've found the Virtuso package to be a great help for this. Works better out the box for dynamically sized list items.

0
On

I know this is not the answer you were looking for, but I recommend using an IntersectionObserver instead of the virtualize library. react-virtualized is not great for dynamic/responsive sized elements. IntersectionObserver is a native browser api that can detect when an element enters the viewport, without providing it with element sizes.

You can easily use it with a react library like react-intersection-observer which has an InView component and a useInView hook, though that would require wrapping every table cell with InView. The performance of an IntersectionObserver tends to be better than a scroll event-based solution like react-virtualized, though that might degrade if you have 1000s of observers.

You could also use something like intersection-observer-admin, which will pool all your observers into one instance similar to the react SyntheticEvent. You'd want to integrate that into something like React.useContext or redux.

0
On

I don't have a complete solution for you, but here are a few things that might help you in your quest:

  1. Consider a static header instead of using position: sticky to keep the header row at the top.

    (If you could get everything else in react-inifnite-scrollcomponent to work, this would solve the sticky header problem.)

    A method I've used is to adjust for the size of the scrollbar and always include it (even if not needed) - this may or may not work in all cases:

    // Header div
    <div
        style={{ width: 'calc(100% - 0.9em)' }}  // accomodate for the width of the scrollbar
    >
        // header stuff
    </div>
    // list (simplified for clarity) - here I used react-window
    <div>
        <AutoSizer>
            {({ height, width }) => (
                <List
                    style={{
                        overflowX: 'hidden',
                        overflowY: 'scroll',  // always display scrollbar
                    }}
                >
                    {RenderRow} // function to return a complete react-table row
                </List>
            )}
        </AutoSizer>
    </div>
    

    And here are some other ideas if that one doesn't work for you: https://stackoverflow.com/a/63412885/6451307.

    If you need horizontal scrolling, you might consider the above plus a wrapper to provide the horizontal scrolling separately.

  2. Consider the table as a list of rows for virtualization purposes.

    It sounds like you've already tried this some, but I definitely found it easiest to use the list virtualization tools and then render a row of cells for each list item.

    You can see an example of that in my code above, too.

  3. Consider an overlay for the loading indicator.

    If it works with your UI needs, create a separate element for the loading indicator that overlays the table body (using absolute positioning and turning it on only when it's needed). It could be solid to hide the table data completely or have a translucent background. This would prevent it from causing problems with whatever is going on in the table/list itself.

    (Some accessibility actions might need to be taken, too. Like hiding the table data from screen readers when the overlay is up, giving the overlay role="alert" or similar, etc. so consider that if you go this route.)

  4. If you're not already doing it, consider using div's instead of trying to use table elements to allow for easier styling and more flexible element structure.

    react-table adds the correct table roles so you can use div's and your table should still have the correct semantics as long as you apply the react-table functions properly - like {...getTableBodyProps()} and similar.

Hopefully, one or more of those will help you get closer to your goal. Good luck!