Blog articles

AirNYT: React-Virtualized + Material UI Cards for Fast Lists

November 11, 2018

This tutorial will cover how to use React-virtualized with Material-ui Cards and Grid to make a list of image-heavy cards that loads extremely fast. Doing this not only allows for much faster loading and re-rendering (such as when using client-side filters) but also better user experience in general. This tutorial is part of a broader series on building an AirBnb-like interface for exploring New York Times travel recommendations.

We start with this app as our baseline:

Github: https://github.com/kpennell/nytairbnbbucketlist

Demo: http://nytrecsalaairbnb.surge.sh/

The Problem: Hundreds of Image-heavy Cards

This app is loading 400+ image-heavy cards. For fast wifi, this is generally fine. But for slower connections (and/or mobile), this app is going to feel sluggish. If I add client side filters, it will feel even more sluggish. And if someone were to use this for an app with 40000 instead of 400 cards, it would feel extremely sluggish. No one likes that.

The Solution: React-Virtualized

React-Virtualized is an awesome library written and maintained by Brian Vaughn (he works on the React team at Facebook). Brian describes React-Virtualized as a set of components for efficiently rendering large lists and tabular data. He gives a great explanation of the library and why he invented it here: https://youtu.be/t4tuhg7b50I?t=670.

Windowing is a technique of only rendering what a user actually sees in their browser. In other words, there’s no need to attach a bunch of list, table, grid items to the DOM that the user is not currently using or seeing. So the problem with my list of cards (in the example app, above) is exactly this: the user’s browser is forced to load a bunch of images that the user might not actually see or be using. Let’s fix this with React-Virtualized.

Implementing React-Virtualized

The current implementation of my Grid of Cards is fairly straightforward:

// LocationsGrid.js

class LocationsGrid extends React.Component {
  render() {
    const { locations, classes } = this.props;

    return (
      <div className={classes.root}>
        <Grid container justify="flex-start" spacing={16}>
          {locations.map((location, index) => (
            <Grid key={index} item>
              <LocationCard location={location} />
            </Grid>
          ))}
        </Grid>
      </div>
    );
  }
}

This maps over the props.locations and renders cards in a nice flex-box grid. Here’s the steps I’ll take to implement this same ui using react-virtualized.

First things first:

yarn add react-virtualized

Next, I’m going to use React-Virtualized AutoSizer and List components for this grid ui. Autosizer is a “High-order component that automatically adjusts the width and height of a single child”. Put a bit more simply, Autosizer is a component that goes around (as a parent or HOC) a List or Table component to allow it access to the width and height props. These width and height props are useful for making responsive or dynamic lists or tables.

The next component I will use is the List component. The List component is fairly self-explanatory in that it is what React-virtualized uses for ‘windowed’ or ‘virtualized’ lists.

Source: https://bvaughn.github.io/react-virtualized/#/components/List

Here is some code with inlined comments that show how I use AutoSizer and List together to implement my same Card grid.

<div style={{ marginTop: "10px", height: "80vh" }}>
  <AutoSizer>
    // The Autosizer component goes around the List component and you can see
    here the height // and width props that it will pass to List
    {({ height, width }) => {
      const itemsPerRow = Math.floor(width / CARD_WIDTH) || 1; // A calculation to establish how many cards will go on each row.

      // The || 1 part is a simple hack that makes it work in a really small viewport (if someone totally collapses the window)

      const rowCount = Math.ceil(locations.length / itemsPerRow); // List will need the number of rows in order to be able to properly know what to render and what not to

      return (
        <div>
          <List
            width={width}
            height={height}
            rowCount={rowCount}
            rowHeight={CARD_WIDTH}
            // CARD_WIDTH is a constant of 340

            rowRenderer={({ index, key, style }) => {
              // This is where stuff gets interesting/confusing

              // We are going to constantly update an array of items that our rowRenderer will render

              const items = [];

              // This array will have a start and an end.

              // The start is the top of the window

              // The end is the bottom of the window

              // the for loop below will constantly be updated as the the user scrolls down

              const fromIndex = index * itemsPerRow;

              const toIndex = Math.min(
                fromIndex + itemsPerRow,
                locations.length
              );

              for (let i = fromIndex; i < toIndex; i++) {
                let location = locations[i];

                items.push(
                  <div className={classes.Item} key={i}>
                    <LocationCard location={location} />
                  </div>

                  // Each of these items has the LocationCard in them
                );
              }

              return (
                // They get rendered into the Row

                <div className={classes.Row} key={key} style={style}>
                  {items}
                </div>
              );
            }}
          />
        </div>
      );
    }}
  </AutoSizer>
</div>;

If you happened to get lost in those comments and lines of code, let me try to simplify this:

We have an <AutoSizer> component. In our example, it calculates the (potentially) changing height and width of the user’s browser window.

Ok, then we have a <List> component. It will create our list. But we need to give it props first. If you check the docs for this component, you’ll see that it needs rowCount, height, rowHeight, rowRenderer, and width. rowRenderer (docs here) is the potentially confusing one. This is the function in charge of ‘creating’ or rendering our rows. But it needs to know which rows to render when. In this example, we give it a key and an index. The index tells the rowRenderer where exactly it is in the collection (be it row 2 or row 1,000,002).

From the docs:

index, // Index of row
key, // Unique key within array of rendered rows

Alrighty, so then we have that for loop in there:

for (let i = fromIndex; i < toIndex; i++) {
  let location = locations[i];

  items.push(
    <div className={classes.Item} key={i}>
      <LocationCard location={location} />
    </div>
  );
}

This for loop is creating a smaller array (from the whole big props.locations array) to be rendered within the window. If you’re still not quite getting it, I recommend logging index and items and then scrolling down, like so:

console.log("index " + index);

const toIndex = Math.min(
  fromIndex + itemsPerRow,

  locations.length
);

for (let i = fromIndex; i < toIndex; i++) {
  let location = locations[i];

  items.push(
    <div className={classes.Item} key={i}>
      <LocationCard location={location} />
    </div>
  );
}

console.log("items " + items);

What Did We Win?

What did we achieve with this slightly-confusing code? Let’s check the chrome console again:

The previous implementation had 329 requests and 29MB transferred, which took 8.46s to load:

The React-Virtualized example had 43 requests which transferred 3.6MB and loaded in 3.79s.

Using React-Virtualized allowed us to save a ton of bandwidth and user waiting time. Now if we could just get Soundcloud to do the same!

I hope this helped you understand the key points of using this incredible library. Upcoming tuts will get us back on track to finish up making this AirBnb clone.

Update: Brian Vaughn (the creator of React-Virtualized) submitted a pull request and showed how to do this tutorial using React-Window (a faster version of React-Virtualized): https://github.com/kpennell/nytairbnbbucketlist/commit/101a32bb0555f3a7cc29151de195882b249972e8

That said, here is the code for this tutorial using React- Virtualized: https://github.com/kpennell/nytairbnbwithvirtualized