Scrollable Grid with Just-in-Time Data Loading – Part 1: Using React Window

Say you have thousands (or even just hundreds) of rows of data to render. To provide a good experience for the user, you might not want to fetch all the data at once. Instead, you might want to fetch the first hundred, show those, then fetch the next hundred.

Alternatively, you could fetch all the data at once but not render all the rows—just the elements that the user would see. This is, essentially, “windowing” or “virtualizing” your data.

React Window’s InfiniteLoader provides a loader to fetch data in batches, and react-window supplies UI components (such as list and grid) that render a windowed chunk of rows.

Although rendering complex grids still requires adding some functionality, the react window library makes getting started with batch fetching and rendering rows pretty straight forward.

1. InfiniteLoader: Fetching the Data

InfiniteLoader lets you fetch row data in chunks that can then be just-in-time rendered when the rows scroll into view.

It requires these props:

Property Description
isItemLoaded A function that, given an index, returns whether or not the item at that index is loaded.
loadMoreItems A function to be invoked when more data needs to be loaded. It takes a start and stop index and fetches the rows between the start and the stop.
rowCount The number of rows loaded.
children A render props that takes 2 arguments (onItemsRendered and ref). The render prop should call the onItemsRendered argument whenever the rendered section changes.

The child component should call onItemsRendered with information about the new section whenever the user scrolls to a new section of the screen. InfiniteLoader will then use the isItemLoaded method to determine if all the rows in the new section are already loaded.

If the isItemLoaded function returns false, InfiniteLoader calls the function to load more items. Whether or not an item is loaded for a specific index could be determined by, say, checking if the array of data fetched contains a row at that index.

If the data is not yet loaded, InfiniteLoader fetches the new data. The loadMoreItems function decides how the new data is fetched. In my current project, InfiniteLoader fetches data from the server in batches. The loader function provides an offset parameter corresponding to the start index with each request to the server.  In the demo project below, the data is “fetched” from a random data generator and then stored in state.

2. Grid or List: Displaying the Data

You have the data; now you need to display it. The main API react-window options to render data are List and Grid.

  • List renders a windowed list of elements in rows, but only the rows necessary to fill itself based on its horizontal scroll positions.
  • Grid renders a windowed grid of elements. It renders the cells to fill itself based on the horizontal and vertical scroll position.

Grid allows you to window both vertically and horizontally, meaning you can just-in time-render cells in both directions. List only allows you to window vertically.

List and Grid both take:

Property Description
onItemsRendered Callback invoked with information about the section of the list/grid that was rendered. It gets called whenever the user scrolls around the list or the grid. If you are using InfiniteLoader, this is where you should call the loader’s onItemsRendered function. That way, InfiniteLoader can check and see if it needs to fetch more data for the newly-rendered section.
children A render prop that, given a row index (in the case of a list) or both a row index and column index (in the case of a grid), determines what to render.

If you are using InfiniteLoader, you should call its onItemsRendered function whenever the user scrolls to a new section of the grid. To do this, call InfiniteLoader’s onItemRendered function from the grid’s onItemsRendered function:


onItemsRendered={({
            visibleRowStartIndex,
            visibleRowStopIndex,
            overscanRowStopIndex,
            overscanRowStartIndex,
          }) => {
            onItemsRendered({
              overscanStartIndex: overscanRowStartIndex,
              overscanStopIndex: overscanRowStopIndex,
              visibleStartIndex: visibleRowStartIndex,
              visibleStopIndex: visibleRowStopIndex,
            });
          }}

That way, InfiniteLoader can check and see if it needs to fetch more data for the newly-rendered section.

What you decide to render at a given row index and column index of a Grid is completely up to you. For instance, nothing would stop you from ignoring the row and column index parameters in the render prop and just rendering static content in every cell.

In my current project, we are fetching data with InfiniteLoader and then passing that data to react-table, a completely separate library. React-table transforms the data to easily indexable row and column data. Our cell component then uses the row and column index to determine which react-table row and column to render.

In the demo below, the row index is used to index into the row of fetched data, and the column index determines which part of the row to render.

Demo

This demo illustrates data fetching with InfiniteLoader and windowing with react window’s Grid component. When “fetching” data, the demo simulates fetching data from a server by generating fake data with an arbitrary delay.

The demo uses the Grid component to render the data; it takes an optional itemData prop that takes contextual data to be passed to the child render prop (in this case, the gird cell) as a data prop.  Basically, if there is data you need when rendering the grid cells, passing that data into itemData lets you use that data when rendering an individual grid cell. Most likely, the data you want to pass down is the data fetched by InfiniteLoader.

The Grid’s child component determines what to render based on the row index, column index, and the data prop from the Grid. The component uses the data prop to access the loaded rows. It then uses the row index to index into the rows and the column index to determine which attribute of the data to render.

Checkout the demo on code sandbox or github.


This is the first post in a series on creating a scrollable grid with just-in-time data loading:

  1. Using React Window
  2. Storing and Restoring Scroll Position with React Window
  3. Using React Table with React Window
  4. Back-End Implementation (Coming Soon)
 
Conversation
  • Mike says:

    Hello!

    I liked your article, thank you!

    I’m a junior developer, and i have a little, simple question:
    How to change the value of stopIndex in loadMoreItems function ? =)
    I am making an application with a large list of users (more than 2000 users).
    It’s my first project with React&Redux, I have little experience and i need some help in this question.

    Your article is the best I could find this week !

    Thank you in advance.

    • Lydia Cupery Lydia Cupery says:

      I’m glad you found the article helpful!

      If I understand correctly, you are asking how to affect the stop index the loadMoreItems’function provides you.
      The loadMoreItems function is a callback function provided by react window infinite loader whenever the table needs to load more items. It’s up to you to return a Promise that resolves once you have finished loading the necessary data. And then up to you to use the data you have loaded to render a Grid cell, row cell etc.

      Because loadMoreItems is a callback function provided by react window, you can not directly affect the stop index. However, if you want to fetch data is larger or smaller chunks (indirectly affecting the stop index) you could play around with the ‘minimumBatchSize’ and ‘threshold’ arguments to infinite loader. Minimum batch size is the minimum number of rows to fetch at a time. The threshold is how close to a row you need to be to pre-fetch the data.

      I hope that was helpful!

      • Mike says:

        Hello, Lydia !

        Thanks for the detailed answer !

        I understand how to use ‘loadMoreItems’ function, ‘minimumBatchSize’ and ‘threshold’ arguments. I can’t understand how react-window sets values for arguments ‘startIndex’ and ‘stopIndex’ inside ‘loadMoreItems’…

        Before the first rendering of the list application request 50 users from server. Then React renders a list with 50 user, and then i scroll down the list, react-window invokes ‘loadMoreItems’ function with arguments ‘startIndex’=50, ‘stopIndex’ = 50, sends request to server and React renders list. If i scroll down the list again, react-window invokes ‘loadMoreItems’ function with arguments ‘startIndex’=100, ‘stopIndex’ = 100.

        I can’t understand why the values of both arguments doubled?

        • Lydia Cupery Lydia Cupery says:

          Hello Mike!

          I’m using the defaults (minimum batch size of 10, threshold of 15). I set the ‘itemCount’ prop on the infinite loader to be the number of rows I know I will have in the list (100). Previously, because I didn’t really care that the start and stop index were being set correctly (since I’m currently ignoring the start and stop index in the loadMoreItems callback), so I just had it set to the number of items currently being rendered. However, the itemCount should actually be set to the “number of rows in list” according to the documentation.

          After setting the itemCount correctly, I printed out the start and stop index in the loadMoreItems callback. My first few values looked like this:
          startIndex: 0, stopIndex: 15
          startIndex: 10, stopIndex: 21
          startIndex: 20, stopIndex: 29
          startIndex: 39, stopIndex: 39

          The load more items gets called whenever I do not have the data I need to render a row within the load threshold (i.e. isItemLoaded returns back false for that index). This means I need to get the data for that row. LoadMoreItems is invoked with the current index for which I don’t have data as the start index.

          Because the threshold is set to 15, react window is checking to see if the item has been loaded 15 rows ahead of the user scroll location. So say when I am on index 10, react window would be checking to see if index 25 has been loaded.

          Because the minimum batch size is 10, load more items always sets the stop index at least 10 ahead of the start index. That’s why the above numbers increment by 10. And my best guess your minimum batch size is set to 50, so that’s why your numbers are incrementing by 50?

  • Monique says:

    This is exactly what I have been searching for. Your demo here isn’t working though.
    https://codesandbox.io/s/github/lydiacupery/infinite-scroll-example

    I get the following error:

    DependencyNotFoundError
    Could not find dependency: ‘@material-ui/core’ relative to ‘/src/App.tsx’

  • Ashish says:

    Hey Lydia!
    Thanks for the article :)

    Might be a little irrelevant but do you know how can I fetch data in VariableSizeGrid on horizontal scroll. I am using react-window. Can this also be done using react-window-infinite-scroll

  • Comments are closed.