02.02.2021

Denis Yakovenko

15 min read

Efficient rendering of really large lists with react-select

Introduction

Using React as the UI framework of a web application always leaves developers with many choices through the development process. One of the most popular decisions to make is which Select component to use. React-select is the first choice when selecting component (with many other features) is required - it has 20.5k stars on GitHub and 2 million weekly downloads on npm. 

Like any common open-source component, react-select solves the common use cases very well. However, at the edges, react-select might disable, falls short sometimes and calls for customizations.

The problem we want to tackle with react-select is quite common: we would like to allow selection from a large list of options. How large? While working on some of FTL projects, we've seen the component start to break at around 1,000 options. 

So, keep reading and we'll show you how it breaks, when react-select desabled, and also demonstrate to you how we improved the performance by using another open-source library - react-window. Finally, we'll dive into the react-window implementation to understand how it is achieved.

By the way - the entire code used in the post is public.

Challenges of setting React-select for long lists

To demonstrate the problem, we've created a new project with create-react-app. We added the react-select component by running `yarn add react-select`.

The most simple code for displaying a select component would be:

import React, { useState } from "react";
import Select from "react-select";
import "./App.css";
 
const options = [...Array(100).keys()].map(idx => ({
 value: `value-${idx}`,
 label: `label-${idx}`,
}));
 
function App() {
 const [inputValue, setInputValue] = useState("");
 
 return (
   
     
       React-select With Large Lists
     
     
        setInputValue(val)}
         options={options}
       />
     
   
 );
}
 
export default App;


This would result in a high-performance select component, with 100 options. We'll use the same code to generate a large and long list of items to include in the select.

The following two gifs compare the performance of a React-select dropdown with 100 options to that of 10,000 options:

React select 100 options React select 10,000 options

React select 100 options

React select 10,000 options

 

The performance degradation is significant. With 10,000 options, the react-select component takes about 5 seconds to open its options menu. Why should it take so long? In both cases, only the first 8 or so options are visible. This is the first hint to how react-window approaches this problem - let's go into details.

React-window comes to the rescue

The key to improving the React-select performance is limiting the rendering of the long list. It's enough to only render 8 items while keeping the same usability as rendering all 10,000 items. 

React-window is another prevalent open-source library, with 8.5k stars on Github and 300,000 npm downloads. React-window is used to render large lists in a virtual way: only the visible elements of the list are actually rendered.

How do we combine the two? Lucky for us, react-select exposes an API that allows us to replace any of its core building blocks. In this case, we would like to replace the MenuList component with our own, custom, MenuList. The new declaration of react-select would look like so:

...
         inputValue={inputValue}
         onInputChange={val => setInputValue(val)}
         options={options}
         components={{
           MenuList: CustomMenuList,
         }}
/>
...

And now in our own CustomMenuList component, we utilize react-window to virtually render the list of options. To enable the virtualization, we must provide the virtualized list with some basic data about the it: the list's height, and the height of each item in the list. That way the component knows exactly how many and which items to render. 

The list's height is passed to the component as the props of the MenuList component, as provided by the react-select library. We determine the item heights ourselves, per our usability needs.

This is how the CustomMenuList ends up looking:

import { FixedSizeList as List } from "react-window";
 
const CustomMenuList = props => {
 const itemHeight = 35;
 const { options, children, maxHeight, getValue } = props;
 const [value] = getValue();
 const initialOffset = options.indexOf(value) * itemHeight;
 
 return (
   
     
       {({ index, style }) => {children[index]} }
     
   
 );
};

The first thing we would like to see is performance improvement. This is the normal react-select rendering alongside the virtualized list one:

 

react native select dropdown react-native-select-dropdown

React select 100 options

React select 10,000 options

 

The time to render with the virtualized list is almost the same as the list with 100 options. Also - the rest of the react-select functionality is enhanced: filtering options by value search, selecting an option or scrolling down the list. These were almost unusable before virtualizing the list.

React-window Shallow Dive


React-window solves the performance problem by virtualizing the list, and only rendering the visible items. At this point, it's important to note that mounting elements to the real (i.e non-virtual) DOM is the most expensive operation for the browser to perform. In order to avoid performance degradation, we will try to minimize the number of such operations. What does it look like in the real DOM? 

This is the DOM of the normal react-select options list:

React-select with large lists

And this is the new DOM with the virtual list:

React-select with long list

The new DOM only has 9 list items. Of course, when scrolling down the list the items that are rendered change, but the amount of items remains 9 all the same.

To better understand how this is achieved, we can inspect the react-window code. The key is a couple of methods called getStartIndexForOffset and getStopIndexForStartIndex:

getStartIndexForOffset: (
   { itemCount, itemSize }: Props,
   offset: number
 ): number =>
   Math.max(
     0,
     Math.min(itemCount - 1, Math.floor(offset / ((itemSize: any): number)))
   ),
 
getStopIndexForStartIndex: (
   { direction, height, itemCount, itemSize, layout, width }: Props,
   startIndex: number,
   scrollOffset: number
 ): number => {
   const isHorizontal = direction === 'horizontal' || layout === 'horizontal';
   const offset = startIndex * ((itemSize: any): number);
   const size = (((isHorizontal ? width : height): any): number);
   const numVisibleItems = Math.ceil(
     (size + scrollOffset - offset) / ((itemSize: any): number)
   );
   return Math.max(
     0,
     Math.min(
       itemCount - 1,
       startIndex + numVisibleItems - 1 // -1 is because stop index is inclusive
     )
   );
 },

The start index is calculated by the scroll offset, and according to the item's height. The stop index is fitted to the start index and is calculated by the provided height and item height. The visible items are inferred by the start and stop indexes, and rendered to the screen accordingly.

 

Conclusion: Choosing React-select dropdown for long lists or not?

React-select is a great choice for dropdown components in React applications. We analized react-select options, and how the react-select component might break for a large list use case, while also discussed how to handle it. React-select dropdown is smart to provide a strong customization API, which should be a consideration when choosing an open-source component.

Moreover, we explained how to use react-window, as a very straightforward solution to handling large amounts of data, and rendering long lists efficiently.

The virtualization of list rendering can be used in many cases. Any time we would want to render a long list, we can utilize virtual rendering to enhance performance. Even such seemingly tiny change in list perfromance might be a good start for improving your app's efficiency overall. So, give it a try!