Virtualizing Large Data Lists with react-window
Rendering large dataset in the DOM without the right tools can be expensive as it has some adverse effect on the web page performance; it slows down runtime, network requests, memory performance. In this tutorial, we’ll cover on how render large amount of data, either from an external API or a dummy data file within our application. We’ll use a React library, react-window, to render this large amount of data. Then we’ll build a simple e-commerce app displaying a list of products.
This article will help web developers to build a better web application that displays and handles large amount of data in either a list format or grid format. It will introduce and explore solutions to the shortcomings with infinite scroll or pagination provided in virtualization of data. It will also cover the best UI/UX practices needed when building applications that render large data within it.
What is virtualization?
Virtualization as a scrolling technique is the rendering process that keeps track of the scrolling position of a user, and visually displays only the DOM contents within that current viewport’s position. This technique simply involves handling large dataset in an infinite scroll pattern but incrementally load and render the long lists of data just as the data enters the viewport. This concept presents developers with all the performance advantages of pagination while at the same time providing the UX benefits gained from infinite scrolling.
Using virtualization is a common solution used in rendering large amount of posts in social media applications, delivering large news data in news media applications, and also in bookkeeping (Accounting) software. The endless two dimensional tile scrolling in Map applications also involves virtual scrolling which aids easy display of the constant changing locations of the users when using it. And virtualization supports displaying these large datasets in grid format or list (spreadsheets-like) format.
A simple implementation of virtualization involves a pre-calculation of the aggregate height of the list of data using the Intersection Observer API provided by the Web API and multiplying it with total count of the list items. But for this tutorial, we’ll use another easier approach, by using react-window.
React-window is a simplified version of react-virtualized
package for easily implementing virtualization in React applications through the APIs (components) provided by the library. According to the docs, React window works by only rendering part of a large dataset (just enough to fill the viewport) which reduces the amount of work (and time) required to render the initial view and to process incoming updates to the UI, and also reduces the memory footprint by avoiding over-allocation of DOM nodes.
The Four react-window Components
Let’s go through the four available components that can be used to render lists of items when using react-window
within React applications.
-
FixedSizeList — This component helps render elements of fixed size defined through the
index
prop. Common available props that can be passed into this component are:useIsScrolling
,itemCount
,itemSize
,itemData
,innerElementType
, etc -
VariableSizeList — This component accepts the same props as the
FixedSizeList
component, but with additional props like anestimatedItemSize
, anditemSize
which takes in a function in this case and returns the size of a item in the direction being windowed -
FixedSizeGrid — With this component, we’re rendering using dimensions: vertical (columns) and horizontal (rows) directions. Another difference is that we’ll have to add a data’s count. Common props are
rowCount
,rowWidth
,columnCount
,columnWidth
,height
,width
e.t.c -
VariableSizeGrid — This component is as
FixedSizeGrid
, with additional props:columnWidth
&rowHeight
which accepts functions as their prop values.
For this tutorial, we’ll focus on using the FixedSizeList
component, which gives a beginner-friendly introduction to using react-window
within our React applications (a simple e-commerce application displaying a list of products.).
Implementing react-window In React Apps
Now let’s install react-window
using yarn
in a React application. In this tutorial, react-window
will be installed as a dependency, while the types for it will be installed as a devDependency
. We’ll also install faker.js
as dependency:
yarn add react-window faker
yarn add -D @types/react-window
After the installation has successfully been completed, go ahead to open the ./src/App.js
file and include the following JSX markup:
import { useState } from "react";
import * as faker from "faker";
const App = () => {
const [data, setData] = useState(() =>
Array.from({ length: 1200 }, faker.commerce.product)
);
return (
<main>
<ul style={{ width: "400px", height: "700px", overflowY: "scroll" }}>
{data.map((product, i) => (
<li key={i + product}>{product}</li>
))}
</ul>
</main>
);
};
In the above code, we defined the structural markup for the list of data being rendered into the DOM. Then passed a basic style prop to the ul
tag to add a fixed width and height styles, also with an overflow set to scroll
on it. Also we’ll use the React hook, useState
, to mange our data
state provided by faker.js
’s dummy data. We create an array of size 1200 from it, and populate the array with enough data from faker.js
.
Next, we’ll start implementing virtualization using the react-window
’s component FixedSizeList
. This component is responsible for rendering an individual item of a fixed size specified by the index prop within the list:
import { useState } from "react";
import * as faker from "faker";
import { FixedSizeList as List } from "react-window";
const App = () => {
const [data, setData] = useState(() =>
Array.from({ length: 1200 }, faker.commerce.product)
);
return (
<main>
<List
innerElementType="ul"
itemData={data}
itemCount={data.length}
itemSize={20}
height={700}
width={400}
>
{({data, index, style }) => {
return (
<li style={style}>
{data[index]}
</li>
);
}}
</List>
</main>
);
};
The List
element uses two markup approaches defined by it’s props, the innerElementType
and outerElementType
props. Their default values are divs
but in our use case, we’ll set the outerElementType
to ul
. Then we’ll pass in li
as the children prop to the List
component, providing it with a necessary styles parameters (absolute positioning styles, etc.). Also providing a width and height props to pass the previous styles we earlier defined through the style
prop. Then using the itemCount
prop, we are creating an imaginary array with the same length of our state, data
. Then we explicitly make this data
available to the li
elements through the itemData
prop.
Let’s modify the unique value for the key
prop on the li
elements by the using the itemKey
prop made available on the List
component. We’ll use the faker.datatype.uuid
function to generate random UUID and populate the itemKey
prop with it.
...
const App = () => {
...
return (
<main>
<List
innerElementType="ul"
itemData={data}
itemCount={data.length}
itemKey={faker.datatype.uuid}
itemSize={20}
height={700}
width={400}
>
{({data, index, style }) => {
return (
<li style={style}>
{data[index]}
</li>
);
}}
</List>
</main>
);
};
This code will make sure we’re using React’s lists and key best practices as defined by the docs. The term “key” is used to describe a special string attribute that needs to be included when creating lists of elements in React applications.
With react-window
, we can go further to define more structured UI elements within the children prop passed into the List
component without tampering with the optimised performance.
...
const App = () => {
const [data, setData] = useState(() =>
Array.from({ length: 1200 }, () => ({
productImg: faker.image.business(200, 600, true),
product: faker.commerce.product(),
productPrice: faker.commerce.price(2, 22, 1, "$"),
productDescription: faker.commerce.productAdjective(),
}))
); return (
<main>
<List
innerElementType="ul"
itemData={data}
itemCount={data.length}
itemKey={faker.datatype.uuid}
itemSize={20}
height={700}
width={400}
>
{({data, index, style }) => {
return (
<li className="py-4 border-2 border-indigo-400 flex" {...props}>
<img className="h-10 w-10 rounded-full" src={productImg} alt="" />
<div className="ml-3">
<p className="space-x-1">
<span className="text-base text-gray-900 whitespace-nowrap">
{product}
</span>
<span
className="text-gray-600 text-base font-extrabold"
style={{
backgroundImage:
"linear-gradient(transparent 50%, pink 50%, pink 85%,transparent 85%,transparent 100%)",
}}
>
{productPrice}
</span>
</p>
<p className="text-sm text-gray-500">{productDescription}</p>
</div>
</li>
);
}}
</List>
</main>
);
};
Here, we updated the state of our application with more data fields to help model an online shop that has the following attributes: image (productImg
), name (product
), price (productPrice
) and description (productDescription
) of its products. We also defined more elements to hold this additional data.
(Note: To replicate the UI styles used in this tutorial, make sure to include the below tailwind.css CDN to the /public/index.html
file.)
<link
href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"
rel="stylesheet"
/>
If you inspect the example List
component using browser Devtools, you’ll notice that no matter how lengthy we scroll down the long list of the items within our application, the number of items in its parent div never changes. What does change is how far each one is absolutely positioned from the top. Thus, making sure we see only the UI elements for the items in the viewport.
Open Source Session Replay
OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.
Start enjoying your debugging experience - start using OpenReplay for free.
Conclusion
In this tutorial, we’ve discussed on how to leverage virtualization to optimize the DOM when rending large amount of dataset into it. Then introduced react-window
as a React package that efficiently implements virtaulization behind the hood, to render a large dataset and at the same time speeds up performance targets within our application. It does this by rendering only the data that are within the viewport of the browser. Get the code used in this tutorial on GitHub.