Skip to main content

Introduction

What is a virtualized window?

@resembli/react-virtualized-window provides components that render only what is visible to the user into the DOM. This keeps your website responsive and performant. The Grid below has 100,000 cells, but only the cells visible to the user are actually present in the DOM.

Basic Grid Example
import type { RenderItem } from "@resembli/react-virtualized-window"
import { Grid } from "@resembli/react-virtualized-window"

const data = Array.from({ length: 1000 }, (_, row) => {
return Array.from({ length: 100 }, (_, column) => {
return [row, column]
})
})

const GridCell: RenderItem<number[]> = ({
data,
style,
cellMeta: { row, column, pinnedRow, pinnedColumn },
}) => {
let background = (row + column) % 2 === 1 ? "#f8f8f0" : "white"

if (pinnedRow === "top") background = "#839073"
if (pinnedColumn === "left") background = "#4E4A59"

return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
background,
color: pinnedColumn || pinnedRow ? "rgb(30, 255, 0)" : "black",
...style,
}}
>
{data[0]},{data[1]}
</div>
)
}

export function BasicGridExample() {
return (
<Grid
height={600}
defaultRowHeight={60}
defaultColumnWidth={60}
pinnedTopCount={1}
pinnedLeftCount={1}
data={data}
>
{GridCell}
</Grid>
)
}

What does @resembli/react-virtualized-window support?

Some of the common features supported are:

  • Row virtualization
  • Column virtualization
  • Row and column pinning
  • React 18 concurrent mode ready
  • TypeScript typings
  • Variable width and height data items
  • Automatic window sizing out the box, with resize handling
  • SSR rendering support
  • and much more...

Installation

npm install @resembli/react-virtualized-window

Brief overview of the react-virtualized-window components

ComponentDescription
ListUseful for rendering a list of items. The List component vertically or horizontally virtualizes the view
GridUseful for rendering tabular data. The Grid component horizontally and vertically virtualizes the view

Basic walkthrough

Step 1: Creating a sized container

The virtualized components all work the same way, and share the same base implementation. The first thing you need to do is create a sized container. A sized container must have a width and height even when it is empty. It should also not grow as the contents rendered inside it grows. The simplested and most straight forward way is to create a div with some height and width, e.g:

import { Grid } from "@resembli/react-virtualized-window"

function MyGrid() {
return <div style={{ height: 400, width: 400 }}>{/* To be filled in soon */}</div>
}
info

We are using the <Grid/> component here which virtualizes horizontally and vertically and hence requires both width and height to be sized. In the case of the <List/> only the height or width is required to be sized dependent on the Layout property (which can either be vertical or horizontal).

tip

You can use percentage sizing as well but you need to ensure the parent itself resolves to a size. For example, the following is a valid sized container setup for the window components:

<div style={{ display: "grid", gridTemplateColumns: "400px, 800px", gridTemplateRows: 400 }}>
<Grid width="50%" height="50%" />
</div>

In the above sizing container, the <Grid/> window is rendered in the first column of the first row. The first column is 400px wide and 400px tall. We specify the <Grid/> should take up 50% of the width and height, hence the <Grid/> component will be 200px wide and tall.

Step 2: Creating a render component

Next we need to create a React component that we can use to render our individual items. The window components will use the component to render the items. For each data item, the window component will pass the data for the that item, the styles we need to apply to the item, and some meta data about the item's position in the grid as props to the component. The metadata is not necessary for rendering items. We now have the following:

import { Grid } from "@resembli/react-virtualized-window"

function RenderItem(props) {
const [row, column] = props.data // The piece of data for this item.
const style = props.style // The CSSProperties we need to pass to our item

return (
<div
style={{
border: "1px solid gray",
boxSizing: "border-box",
display: "flex",
justifyContent: "center",
alignItems: "center",
...style,
}}
>
{row},{column}
</div>
)
}

function MyGrid() {
return <div style={{ height: 400, width: 400 }}>{/* To be filled in soon */}</div>
}

Notice this just a regular React component. It's important that we apply the style passed as a prop to our component.

caution

The style prop passed to our RenderItem component contains styles relevant to the sizing of the component. In particular it sets the width, height, and margin CSS values.

It's important these dimensions remain unchanged and items need to have known widths and heights for the virtualization to work correctly.

A common mistake is to apply a border to a RenderItem, e.g.

<div style={{ border: "1px solid black", ...style }}>...</div>

However this changes the dimension of the div if the box-sizing style is content-box, which is the default in browsers. The correct way to give a virtualized item a border to set the box-sizing style to border-box, in addition to setting the border value:

<div style={{ border: "1px solid black", boxSizing: "border-box", ...style }}>...</div>

Step 3: Render the window component with our data

We can now render the componet by passing our data to the grid. It's important we pass our RenderItem as a value, and not as a JSX object. We are rendering the <Grid> component here so the data we pass should be a matrix (i.e. an array of arrays).

import { Grid } from "@resembli/react-virtualized-window"

// Create a 1000x100 matrix
const dummyData = Array.from({ length: 1000 }, (_, row) =>
Array.from({ length: 100 }, (_, column) => [row, column]),
)

function RenderItem(props) {
const [row, column] = props.data // The piece of data for this item.
const style = props.style // The CSSProperties we need to pass to our item

return (
<div
style={{
border: "1px solid gray",
boxSizing: "border-box",
display: "flex",
justifyContent: "center",
alignItems: "center",
...style,
}}
>
{row},{column}
</div>
)
}

function MyGrid() {
return (
<div style={{ height: 400, width: 400 }}>
<Grid defaultRowHeight={50} defaultColumnWidth={100} data={dummyData}>
{RenderItem}
</Grid>
</div>
)
}
tip

It is possible (and okay) to define your RenderItem inline, and in fact you many of our examples do this. For example:

function MyGrid() {
return (
<div style={{ height: 400, width: 400 }}>
<Grid defaultRowHeight={50} defaultColumnWidth={100} data={dummyData}>
{(props) => {
// Our render item component as before
}}
</Grid>
</div>
)
}

Note you should not use any React hooks when defining the component inline, and in fact this is one disadvantage of using an inline function instead of a separate RenderItem.

The end result:

0,0
0,1
0,2
0,3
0,4
0,5
0,6
0,7
0,8
0,9
0,10
0,11
0,12
0,13
0,14
1,0
1,1
1,2
1,3
1,4
1,5
1,6
1,7
1,8
1,9
1,10
1,11
1,12
1,13
1,14
2,0
2,1
2,2
2,3
2,4
2,5
2,6
2,7
2,8
2,9
2,10
2,11
2,12
2,13
2,14
3,0
3,1
3,2
3,3
3,4
3,5
3,6
3,7
3,8
3,9
3,10
3,11
3,12
3,13
3,14
4,0
4,1
4,2
4,3
4,4
4,5
4,6
4,7
4,8
4,9
4,10
4,11
4,12
4,13
4,14
5,0
5,1
5,2
5,3
5,4
5,5
5,6
5,7
5,8
5,9
5,10
5,11
5,12
5,13
5,14
6,0
6,1
6,2
6,3
6,4
6,5
6,6
6,7
6,8
6,9
6,10
6,11
6,12
6,13
6,14
7,0
7,1
7,2
7,3
7,4
7,5
7,6
7,7
7,8
7,9
7,10
7,11
7,12
7,13
7,14
8,0
8,1
8,2
8,3
8,4
8,5
8,6
8,7
8,8
8,9
8,10
8,11
8,12
8,13
8,14
9,0
9,1
9,2
9,3
9,4
9,5
9,6
9,7
9,8
9,9
9,10
9,11
9,12
9,13
9,14
10,0
10,1
10,2
10,3
10,4
10,5
10,6
10,7
10,8
10,9
10,10
10,11
10,12
10,13
10,14
11,0
11,1
11,2
11,3
11,4
11,5
11,6
11,7
11,8
11,9
11,10
11,11
11,12
11,13
11,14
12,0
12,1
12,2
12,3
12,4
12,5
12,6
12,7
12,8
12,9
12,10
12,11
12,12
12,13
12,14
13,0
13,1
13,2
13,3
13,4
13,5
13,6
13,7
13,8
13,9
13,10
13,11
13,12
13,13
13,14
14,0
14,1
14,2
14,3
14,4
14,5
14,6
14,7
14,8
14,9
14,10
14,11
14,12
14,13
14,14

See the Docs for the individual components

Or checkout the recipes

Basic concepts

The NumberOrPercent type

The different window components accept columnWidths and rowHeights properties, as well as their corresponding defaults (defaultColumnWidth and defaultRowHeight). The value provided to these components can either be number or a percent string. For example:

// the default row height is assumed to be 40
<List defaultRowHeight={40}>...</List>

// or the default row height is assumed to be 10% of the window height
<List defaultRowHeight="10%">...</List>

The number value is straight forward, 40 === 40px. However, percent values are not the same as the CSS percent value representation. Row height percent values are based on the window height. Colum width percent values are based on the the window width.

caution

If the component has a gap value set, this will not be included as part of the percentage calcuation, so the items width or height will be the percentage px value + gap px value.

The sticky div

On most browsers scrolling happens on a separate thread. Virtualization calculations happen on the main thread. This means it's possible for the user to scroll before the window has determined what are next items to render and what position they should be in. This leads to content flashing in. This issue is present in all virtualization libaries and inherent to the way browsers work. This is execellent article for those looking to understand more.

Despite the explanation given above, you may notice that when you scroll quickly the contents within the @resembli/react-virtualized-window components do not flash in and out and the checkboarding pattern is not present.

This is due to a sticky positioning div that keeps the window in place and in view even as the user scrolls. For example, if we compare the rendering of the FixedSizedGrid from react-window to a fixed sized grid example from @resembli/react-virtualized-window, we can see checkboarding vs no checkboarding in effect:

info

This is NOT a performance comparison, or even a comparison of react-window to @resembli/react-virtualized-window. This is a demonstration of the checkboarding effect present in other virtualization frameworks, which is not present in @resembli/react-virtualized-window (unless you disable the stick div using the disableSticky prop).

The grids below have the following characteristics:

  • Heigth and Width of 800px
  • Column width of 100px
  • Row height of 30px
  • react-window:

    react window grid example

  • @resembli/react-virtualized-window

    react virtualized window grid example

Tab-index

It's possible to give individual items rendered into the window components a tab index - but this is not recommended. If you do give individual items a tab index be sure to set the overscan property to something greater than 1. This is necessary to allow the browser to tab to items that are out of view.

caution

The window components are intended to render large number of items (10,000+ in many cases). If the individual items have tab indices set, its possible the user can get stuck tabbing through all the items. For this reason we do not recommend adding a tab index to the individual items.

You should add a value to the tabIndex property available on the window componets. This will allow the user to tab to the list and control it with the other keyboard keys (up, down, page-up, page-down, etc).

Searching for text

Browsers provide the ability for users to search for a string of text by pressing Ctrl-F (or Cmd-F on a Mac). This functionality relies on text content being rendered to the dom. Virtualization is the process of rendering only what is visible to the dom. Hence, items that are out of view will not be searchable by the browser. The common workaround for this issue is to build your own search logic - which is able to the scoll to the desired item. You can do this with the imperative VirtualizedWindowApi.