A tiny React hook for rendering large datasets like a breeze.
Features
- β»οΈ Renders millions of items with highly performant way, using DOM recycling.
- π£ Easy to use, based on React hook.
- π
πΌ Apply styles without hassle, just few setups.
- 𧱠Supports fixed, variable, dynamic, and real-time heights/widths.
- π₯ Supports responsive web design (RWD) for better UX.
- π Supports sticky headers for building on-trend lists.
- π Built-ins load more callback for you to deal with infinite scroll + skeleton screens.
- π± Imperative scroll-to methods for offset, items, and alignment.
- πΉ Out-of-the-box smooth scrolling and the effect is DIY-able.
- π¬ It's possible to implement stick to bottom and pre-pending items for chat, feeds, etc.
- β³ Provides
isScrolling
indicator to you for UI placeholders or performance optimization. - ποΈ Supports server-side rendering (SSR) for a fast FP + FCP and better SEO.
- π Supports TypeScript type definition.
- π Super flexible API design, built with DX in mind.
- π¦ Tiny size (~ 3kB gzipped). No external dependencies, aside for the
react
.
Why?
When rendering a large set of data (e.g. list, table, etc.) in React, we all face performance/memory troubles. There're some great libraries already available but most of them are component-based solutions that provide well-defineded way of using but increase a lot of bundle size. However, a library comes out as a hook-based solution that is flexible and headless
but using and styling it can be verbose (because it's a low-level hook). Furthermore, it lacks many of the useful features.
React Cool Virtual is a tiny React hook that gives you a better DX and modern way for virtualizing a large amount of data without struggle π€―.
Docs
Getting Started
Requirement
To use React Cool Virtual, you must use react@16.8.0
or greater which includes hooks.
Installation
This package is distributed via npm.
$ yarn add react-cool-virtual
$ npm install --save react-cool-virtual
β οΈ This package using ResizeObserver API under the hook. Most modern browsers support it natively, you can also add polyfill for full browser support.
CDN
If you're not using a module bundler or package manager. We also provide a UMD build which is available over the unpkg.com CDN. Simply use a <script>
tag to add it after React CDN links as below:
<script crossorigin src="https://unpkg.com/react/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-cool-virtual/dist/index.umd.production.min.js"></script>
Once you've added this you will have access to the window.ReactCoolVirtual.useVirtual
variable.
Basic Usage
Here's the basic concept of how it rocks:
import useVirtual from "react-cool-virtual";
const List = () => {
const { outerRef, innerRef, items } = useVirtual({
itemCount: 10000,
itemSize: 50,
});
return (
<div
ref={outerRef} // Attach the `outerRef` to the scroll container
style={{ width: "300px", height: "500px", overflow: "auto" }}
>
{/* Attach the `innerRef` to the wrapper of the items */}
<div ref={innerRef}>
{items.map(({ index, size }) => (
// You can set the item's height with the `size` property
<div key={index} style={{ height: `${size}px` }}>
βοΈ {index}
</div>
))}
</div>
</div>
);
};
β¨ Pretty easy right? React Cool Virtual is more powerful than you think. Let's explore more use cases through the examples!
Examples
Fixed Size
This example demonstrates how to create a fixed size row. For column or grid, please refer to CodeSandbox.
import useVirtual from "react-cool-virtual";
const List = () => {
const { outerRef, innerRef, items } = useVirtual({
itemCount: 1000,
});
return (
<div
style={{ width: "300px", height: "300px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{items.map(({ index, size }) => (
<div key={index} style={{ height: `${size}px` }}>
βοΈ {index}
</div>
))}
</div>
</div>
);
};
Variable Size
This example demonstrates how to create a variable size row. For column or grid, please refer to CodeSandbox.
import useVirtual from "react-cool-virtual";
const List = () => {
const { outerRef, innerRef, items } = useVirtual({
itemCount: 1000,
itemSize: (idx) => (idx % 2 ? 100 : 50),
});
return (
<div
style={{ width: "300px", height: "300px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{items.map(({ index, size }) => (
<div key={index} style={{ height: `${size}px` }}>
βοΈ {index}
</div>
))}
</div>
</div>
);
};
Dynamic Size
This example demonstrates how to create a dynamic (unknown) size row. For column or grid, please refer to CodeSandbox.
import useVirtual from "react-cool-virtual";
const List = () => {
const { outerRef, innerRef, items } = useVirtual({
itemCount: 1000,
itemSize: 75,
});
return (
<div
style={{ width: "300px", height: "300px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{items.map(({ index, measureRef }) => (
// Use the `measureRef` to measure the item size
<div key={index} ref={measureRef}>
{/* Some data... */}
</div>
))}
</div>
</div>
);
};
π‘ The scrollbar is jumping (or unexpected position)? It's because the total size of the items is gradually corrected along with an item that has been measured. You can tweak the itemSize
to reduce the phenomenon.
Real-time Resize
This example demonstrates how to create a real-time resize row (e.g. accordion, collapse, etc.). For column or grid, please refer to CodeSandbox.
import { useState, forwardRef } from "react";
import useVirtual from "react-cool-virtual";
const AccordionItem = forwardRef(({ children, height, ...rest }, ref) => {
const [h, setH] = useState(height);
return (
<div
{...rest}
style={{ height: `${h}px` }}
ref={ref}
onClick={() => setH((prevH) => (prevH === 50 ? 100 : 50))}
>
{children}
</div>
);
});
const List = () => {
const { outerRef, innerRef, items } = useVirtual({
itemCount: 50,
});
return (
<div
style={{ width: "300px", height: "300px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{items.map(({ index, size, measureRef }) => (
// Use the `measureRef` to measure the item size
<AccordionItem key={index} height={size} ref={measureRef}>
ππ» Click Me
</AccordionItem>
))}
</div>
</div>
);
};
Responsive Web Design (RWD)
This example demonstrates how to create a list with RWD to provide a better UX for the user.
import useVirtual from "react-cool-virtual";
const List = () => {
const { outerRef, innerRef, items } = useVirtual({
itemCount: 1000,
itemSize: (_, width) => (width > 400 ? 50 : 100),
onResize: (size) => console.log("Outer's size: ", size),
});
return (
<div
style={{ width: "100%", height: "400px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{/* We can also access the outer's width here */}
{items.map(({ index, size, width }) => (
<div key={index} style={{ height: `${size}px` }}>
βοΈ {index} ({width})
</div>
))}
</div>
</div>
);
};
π‘ If the item size is specified through the function of itemSize
, please ensure there's no the measureRef on the item element. Otherwise, the hook will use the measured (cached) size for the item. When working with RWD, we can only use either of the two.
This example demonstrates how to make sticky headers with React Cool Virtual.
import useVirtual from "react-cool-virtual";
const List = () => {
const { outerRef, innerRef, items } = useVirtual({
itemCount: 1000,
itemSize: 75,
stickyIndices: [0, 10, 20, 30, 40, 50],
});
return (
<div
style={{ width: "300px", height: "300px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{items.map(({ index, size, isSticky }) => {
let style = { height: `${size}px` };
// Use the `isSticky` property to style the sticky item, that's it β¨
style = isSticky ? { ...style, position: "sticky", top: "0" } : style;
return (
<div key={someData[index].id} style={style}>
{someData[index].content}
</div>
);
})}
</div>
</div>
);
};
π‘ The scrollbar disappears when using Chrome in Mac? If you encounter this issue, you can add will-change:transform
to the outer element to workaround this problem.
Scroll to Offset / Items
You can imperatively scroll to offset or items as follows:
const { scrollTo, scrollToItem } = useVirtual();
const scrollToOffset = () => {
scrollTo(500, () => {
});
};
const scrollToItem = () => {
scrollToItem(500, () => {
});
scrollToItem({ index: 10, align: "auto" });
};
Smooth Scrolling
React Cool Virtual provides the smooth scrolling feature out of the box, all you need to do is turn the smooth
option on.
const { scrollTo, scrollToItem } = useVirtual();
const scrollToOffset = () => scrollTo({ offset: 500, smooth: true });
const scrollToItem = () => scrollToItem({ index: 10, smooth: true });
π‘ When working with dynamic size, the scroll position will be automatically corrected along with the items are measured. To optimize it, we can provide an estimated item size to the itemSize option.
The default easing effect is easeInOutSine, and the duration is 100ms <= distance * 0.075 <= 500ms
. You can easily customize your own effect as follows:
const { scrollTo } = useVirtual({
scrollDuration: 500,
scrollDuration: (distance) => distance * 0.05,
scrollEasingFunction: (t) => {
const c1 = 1.70158;
const c2 = c1 * 1.525;
return t < 0.5
? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
: (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
},
});
const scrollToOffset = () => scrollTo({ offset: 500, smooth: true });
π‘ For more cool easing effects, please check it out.
Infinite Scroll
It's possible to make a complicated infinite scroll logic simple by just using a hook, no kidding! Let's see how possible π€.
import { useState } from "react";
import useVirtual from "react-cool-virtual";
import axios from "axios";
const TOTAL_COMMENTS = 500;
const BATCH_COMMENTS = 5;
const isItemLoadedArr = [];
const loadData = async ({ loadIndex }, setComments) => {
isItemLoadedArr[loadIndex] = true;
try {
const { data: comments } = await axios(`/comments?postId=${loadIndex + 1}`);
setComments((prevComments) => {
const nextComments = [...prevComments];
comments.forEach((comment) => {
nextComments[comment.id - 1] = comment;
});
return nextComments;
});
} catch (err) {
isItemLoadedArr[loadIndex] = false;
loadData({ loadIndex }, setComments);
}
};
const List = () => {
const [comments, setComments] = useState([]);
const { outerRef, innerRef, items } = useVirtual({
itemCount: TOTAL_COMMENTS,
itemSize: 122,
loadMoreCount: BATCH_COMMENTS,
isItemLoaded: (loadIndex) => isItemLoadedArr[loadIndex],
loadMore: (e) => loadData(e, setComments),
});
return (
<div
style={{ width: "300px", height: "500px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{items.map(({ index, measureRef }) => (
<div
key={comments[index]?.id || `fb-${index}`}
style={{ padding: "16px", minHeight: "122px" }}
ref={measureRef} // Used to measure the unknown item size
>
{comments[index]?.body || "β³ Loading..."}
</div>
))}
</div>
</div>
);
};
Working with A Loading Indicator
import { Fragment, useState } from "react";
import useVirtual from "react-cool-virtual";
import axios from "axios";
const TOTAL_COMMENTS = 500;
const BATCH_COMMENTS = 5;
const isItemLoadedArr = [];
isItemLoadedArr[50] = true;
const loadData = async ({ loadIndex }, setComments) => {
isItemLoadedArr[loadIndex] = true;
try {
const { data: comments } = await axios(`/comments?postId=${loadIndex + 1}`);
setComments((prevComments) => [...prevComments, ...comments]);
} catch (err) {
isItemLoadedArr[loadIndex] = false;
loadData({ loadIndex }, setComments);
}
};
const Loading = () => <div>β³ Loading...</div>;
const List = () => {
const [comments, setComments] = useState([]);
const { outerRef, innerRef, items } = useVirtual({
itemCount: comments.length,
loadMoreCount: BATCH_COMMENTS,
isItemLoaded: (loadIndex) => isItemLoadedArr[loadIndex],
loadMore: (e) => loadData(e, setComments),
});
return (
<div
style={{ width: "300px", height: "500px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{items.length ? (
items.map(({ index, measureRef }) => {
const showLoading =
index === comments.length - 1 && comments.length < TOTAL_COMMENTS;
return (
<Fragment key={comments[index].id}>
<div ref={measureRef}>{comments[index].body}</div>
{showLoading && <Loading />}
</Fragment>
);
})
) : (
<Loading />
)}
</div>
</div>
);
};
Pre-pending Items
This example demonstrates how to pre-pend items and maintain scroll position for the user.
import { useEffect, useState } from "react";
import useVirtual from "react-cool-virtual";
import axios from "axios";
const TOTAL_COMMENTS = 500;
const BATCH_COMMENTS = 5;
let shouldFetchData = true;
let postId = 100;
const fetchData = async (postId, setComments) => {
try {
const { data: comments } = await axios(`/comments?postId=${postId}`);
setComments((prevComments) => [...comments, ...prevComments]);
} catch (err) {
fetchData(postId, setComments);
}
};
const List = () => {
const [comments, setComments] = useState([]);
const { outerRef, innerRef, items, scrollToItem } = useVirtual({
itemCount: comments.length,
onScroll: ({ scrollOffset }) => {
if (scrollOffset < 50 && shouldFetchData) {
fetchData(--postId, setComments);
shouldFetchData = false;
}
},
});
useEffect(() => fetchData(postId, setComments), []);
useEffect(() => {
requestAnimationFrame(() => {
scrollToItem({ index: BATCH_COMMENTS, align: "start" }, () => {
if (comments.length < TOTAL_COMMENTS) shouldFetchData = true;
});
});
}, [comments.length, scrollToItem]);
return (
<div
style={{ width: "300px", height: "500px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{items.length ? (
items.map(({ index, measureRef }) => (
// Used to measure the unknown item size
<div key={comments[index].id} ref={measureRef}>
{comments[index].body}
</div>
))
) : (
<div className="item">β³ Loading...</div>
)}
</div>
</div>
);
};
Filtering Items
When working with filtering items, we can reset the scroll position when the itemCount
is changed by enabling the resetScroll option.
import { useState } from "react";
import useVirtual from "react-cool-virtual";
const List = () => {
const [itemCount, setItemCount] = useState(100);
const { outerRef, innerRef, items } = useVirtual({
itemCount,
resetScroll: true,
});
return (
<div
style={{ width: "300px", height: "300px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{items.map(({ index, size }) => (
<div key={index} style={{ height: `${size}px` }}>
βοΈ {index}
</div>
))}
</div>
</div>
);
};
Sticking to Bottom
This example demonstrates the scenario of sticking/unsticking the scroll position to the bottom for a chatroom.
import { useState, useEffect } from "react";
import useVirtual from "react-cool-virtual";
import axios from "axios";
const TOTAL_MESSAGES = 200;
let isScrolling = false;
let id = 0;
const loadData = async (id, setMessages) => {
try {
const { data: messages } = await axios(`/messages/${id}`);
setMessages((prevMessages) => [...prevMessages, messages]);
} catch (err) {
loadData(id, setMessages);
}
};
const Chatroom = () => {
const [shouldSticky, setShouldSticky] = useState(true);
const [messages, setMessages] = useState([]);
const { outerRef, innerRef, items, scrollToItem } = useVirtual({
itemCount: messages.length,
scrollDuration: 50,
onScroll: ({ userScroll }) => {
if (userScroll && !isScrolling) setShouldSticky(false);
},
});
useEffect(() => {
if (id <= TOTAL_MESSAGES)
setTimeout(
() => loadData(++id, setMessages),
Math.floor(500 + Math.random() * 2000)
);
}, [messages.length]);
useEffect(() => {
if (shouldSticky) {
isScrolling = true;
scrollToItem({ index: messages.length - 1, smooth: true }, () => {
isScrolling = false;
});
}
}, [messages.length, shouldSticky, scrollToItem]);
return (
<div>
<div
style={{ width: "300px", height: "400px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{items.map(({ index, measureRef }) => (
// Used to measure the unknown item size
<div key={`${messages[index].id}`} ref={measureRef}>
<div>{messages[index].content}</div>
</div>
))}
</div>
</div>
{!shouldSticky && (
<button onClick={() => setShouldSticky(true)}>Stick to Bottom</button>
)}
</div>
);
};
Working with Input Elements
This example demonstrates how to handle input elements (or form fields) in a virtualized list.
import { useState } from "react";
import useVirtual from "react-cool-virtual";
const defaultValues = new Array(20).fill(false);
const Form = () => {
const [formData, setFormData] = useState({ todo: defaultValues });
const { outerRef, innerRef, items } = useVirtual({
itemCount: defaultValues.length,
});
const handleInputChange = ({ target }, index) => {
setFormData((prevData) => {
const todo = [...prevData.todo];
todo[index] = target.checked;
return { todo };
});
};
const handleSubmit = (e) => {
e.preventDefault();
alert(JSON.stringify(formData, undefined, 2));
};
return (
<form onSubmit={handleSubmit}>
<div
style={{ width: "300px", height: "300px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{items.map(({ index, size }) => (
<div key={index} style={{ height: `${size}px` }}>
<input
id={`todo-${index}`}
type="checkbox"
// Populate the corresponding state to the default value
defaultChecked={formData.todo[index]}
onChange={(e) => handleInputChange(e, index)}
/>
<label htmlFor={`todo-${index}`}>{index}. I'd like to...</label>
</div>
))}
</div>
</div>
<input type="submit" />
</form>
);
};
When dealing with forms, we can use React Cool Form to handle the form state and boost performance for use.
import useVirtual from "react-cool-virtual";
import { useForm } from "react-cool-form";
const defaultValues = new Array(20).fill(false);
const Form = () => {
const { outerRef, innerRef, items } = useVirtual({
itemCount: defaultValues.length,
});
const { form } = useForm({
defaultValues: { todo: defaultValues },
removeOnUnmounted: false,
onSubmit: (formData) => alert(JSON.stringify(formData, undefined, 2)),
});
return (
<form ref={form}>
<div
style={{ width: "300px", height: "300px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{items.map(({ index, size }) => (
<div key={index} style={{ height: `${size}px` }}>
<input
id={`todo-${index}`}
name={`todo[${index}]`}
type="checkbox"
/>
<label htmlFor={`todo-${index}`}>{index}. I'd like to...</label>
</div>
))}
</div>
</div>
<input type="submit" />
</form>
);
};
Dealing with Dynamic Items
React requires keys for array items. I'd recommend using an unique id as the key as possible as we can, especially when working with reordering, filtering, etc. Refer to this article to learn more.
const List = () => {
const { outerRef, innerRef, items } = useVirtual();
return (
<div
ref={outerRef}
style={{ width: "300px", height: "300px", overflow: "auto" }}
>
<div ref={innerRef}>
{items.map(({ index, size }) => (
// Use IDs from your data as keys
<div key={someData[index].id} style={{ height: `${size}px` }}>
{someData[index].content}
</div>
))}
</div>
</div>
);
};
Server-side Rendering (SSR)
Server-side rendering allows us to provide a fast FP and FCP, it also benefits for SEO. React Cool Virtual supplies you a seamless DX between SSR and CSR.
const List = () => {
const { outerRef, innerRef, items } = useVirtual({
itemCount: 1000,
ssrItemCount: 30,
ssrItemCount: [50, 80],
});
return (
<div
style={{ width: "300px", height: "300px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{/* The items will be rendered both on SSR and CSR, depending on our settings */}
{items.map(({ index, size }) => (
<div key={someData[index].id} style={{ height: `${size}px` }}>
{someData[index].content}
</div>
))}
</div>
</div>
);
};
π‘ Please note, when using the ssrItemCount
, the initial items will be the SSR items but it has no impact to the UX. In addition, you might notice that some styles (i.e. width, start) of the SSR items are 0
. It's by design, because there's no way to know the outer's size on SSR. However, you can make up these styles based on the environments if you need.
API
React Cool Virtual is a custom React hook that supplies you with all the features for building highly performant virtualized datasets easily π. It takes options
parameters and returns useful methods as follows.
const returnValues = useVirtual(options);
Options
An object
with the following options:
itemCount (Required)
number
The total number of items. It can be an arbitrary number if actual number is unknown, see the example to learn more.
ssrItemCount
number | [number, number]
The number of items that are rendered on server-side, see the example to learn more.
itemSize
number | (index: number, width: number) => number
The size of an item (default = 50). When working with dynamic size, it will be the default/or estimated size of the unmeasured items.
horizontal
boolean
The layout/orientation of the list (default = false). When true
means left/right scrolling, so the hook will use width
as the item size and use the left
as the start position.
resetScroll
boolean
It's used to tell the hook to reset the scroll position when the itemCount is changed (default = false). It's useful for filtering items.
overscanCount
number
The number of items to render behind and ahead of the visible area (default = 1). That can be used for two reasons:
- To slightly reduce/prevent a flash of empty screen while the user is scrolling. Please note, too many can negatively impact performance.
- To allow the tab key to focus on the next (invisible) item for better accessibility.
useIsScrolling
boolean
To enable/disable the isScrolling indicator of an item (default = false). It's useful for UI placeholders or performance optimization when the list is being scrolled. Please note, using it will result in an additional render after scrolling has stopped.
stickyIndices
number[]
An array of indexes to make certain items in the list sticky. See the example to learn more.
- The values must be provided in ascending order, i.e.
[0, 10, 20, 30, ...]
.
scrollDuration
number | (distance: number) => number
The duration of smooth scrolling, the unit is milliseconds (default = 100ms <= distance * 0.075 <= 500ms
).
scrollEasingFunction
(time: number) => number
A function that allows us to customize the easing effect of smooth scrolling (default = easeInOutSine).
loadMoreCount
number
How many number of items that you want to load/or pre-load (default = 15), it's used for infinite scroll. A number 15 means the loadMore callback will be invoked when the user scrolls within every 15 items, e.g. 1 - 15, 16 - 30, and so on.
isItemLoaded
(index: number) => boolean
A callback for us to provide the loaded state of a batch items, it's used for infinite scroll. It tells the hook whether the loadMore should be triggered or not.
loadMore
(event: Object) => void
A callback for us to fetch (more) data, it's used for infinite scroll. It's invoked when more items need to be loaded, which based on the mechanism of loadMoreCount and isItemLoaded.
const loadMore = ({
startIndex, // (number) The index of the first batch item
stopIndex, // (number) The index of the last batch item
loadIndex, // (number) The index of the current batch items (e.g. 1 - 15 as `0`, 16 - 30 as `1`, and so on)
scrollOffset, // (number) The scroll offset from top/left, depending on the `horizontal` option
userScroll, // (boolean) Tells you the scrolling is through the user or not
}) => {
};
const props = useVirtual({ loadMore });
onScroll
(event: Object) => void
This event will be triggered when scroll position is being changed by the user scrolls or scrollTo/scrollToItem methods.
const onScroll = ({
overscanStartIndex, // (number) The index of the first overscan item
overscanStopIndex, // (number) The index of the last overscan item
visibleStartIndex, // (number) The index of the first visible item
visibleStopIndex, // (number) The index of the last visible item
scrollOffset, // (number) The scroll offset from top/left, depending on the `horizontal` option
scrollForward, // (boolean) The scroll direction of up/down or left/right, depending on the `horizontal` option
userScroll, // (boolean) Tells you the scrolling is through the user or not
}) => {
};
const props = useVirtual({ onScroll });
onResize
(event: Object) => void
This event will be triggered when the size of the outer element changes.
const onResize = ({
width, // (number) The content width of the outer element
height, // (number) The content height of the outer element
}) => {
};
const props = useVirtual({ onResize });
Return Values
An object
with the following properties:
outerRef
React.useRef<HTMLElement>
A ref to attach to the outer element. We must apply it for using this hook.
innerRef
React.useRef<HTMLElement>
A ref to attach to the inner element. We must apply it for using this hook.
items
Object[]
The virtualized items for rendering rows/columns. Each item is an object
that contains the following properties:
Name | Type | Description |
---|
index | number | The index of the item. |
size | number | The fixed/variable/measured size of the item. |
width | number | The current content width of the outer element. It's useful for a RWD row/column. |
start | number | The starting position of the item. We might only need this when working with grids. |
isScrolling | true | undefined | An indicator to show a placeholder or optimize performance for the item. |
isSticky | true | undefined | An indicator to make certain items become sticky in the list. |
measureRef | Function | It's used to measure an item with dynamic or real-time heights/widths. |
scrollTo
(offsetOrOptions: number | Object, callback?: () => void) => void
This method allows us to scroll to the specified offset from top/left, depending on the horizontal option.
scrollTo(500);
scrollTo({
offset: 500,
smooth: true,
});
π‘ It's possible to customize the easing effect of the smoothly scrolling, see the example to learn more.
scrollToItem
(indexOrOptions: number | Object, callback?: () => void) => void
This method allows us to scroll to the specified item.
scrollToItem(10);
scrollTo({
index: 10,
align: "auto",
smooth: true,
});
π‘ It's possible to customize the easing effect of the smoothly scrolling, see the example to learn more.
Others
Performance Optimization
Items are re-rendered whenever the user scrolls. If your item is a heavy data component, there're two strategies for performance optimization.
When working with non-dynamic size, we can extract the item to it's own component and wrap it with React.memo
. It shallowly compares the current props and the next props to avoid unnecessary re-renders.
import { memo } from "react";
import useVirtual from "react-cool-virtual";
const MemoizedItem = memo(({ height, ...rest }) => {
return (
<div {...rest} style={{ height: `${height}px` }}>
π³ Am I heavy?
</div>
);
});
const List = () => {
const { outerRef, innerRef, items } = useVirtual({
itemCount: 1000,
itemSize: 75,
});
return (
<div
style={{ width: "300px", height: "300px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{items.map(({ index, size }) => (
<MemoizedItem key={index} height={size} />
))}
</div>
</div>
);
};
Use isScrolling
Indicator
If the above solution can't meet your case or you're working with dynamic size. React Cool Virtual supplies you an isScrolling
indicator that allows you to replace the heavy component with a light one while the user is scrolling.
import { forwardRef } from "react";
import useVirtual from "react-cool-virtual";
const HeavyItem = forwardRef((props, ref) => {
return (
<div {...props} ref={ref}>
π³ Am I heavy?
</div>
);
});
const LightItem = (props) => <div {...props}>π¦ I believe I can fly...</div>;
const List = () => {
const { outerRef, innerRef, items } = useVirtual({
itemCount: 1000,
useIsScrolling: true,
useIsScrolling: (speed) => speed > 50,
});
return (
<div
style={{ width: "300px", height: "300px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{items.map(({ index, isScrolling, measureRef }) =>
isScrolling ? (
<LightItem key={index} />
) : (
<HeavyItem key={index} ref={measureRef} />
)
)}
</div>
</div>
);
};
π‘ Well... the isScrolling
can also be used in many other ways, please use your imagination π€.
How to Share A ref
?
You can share a ref
as follows, here we take the outerRef
as the example:
import { useRef } from "react";
import useVirtual from "react-cool-virtual";
const App = () => {
const ref = useRef();
const { outerRef } = useVirtual();
return (
<div
ref={(el) => {
outerRef.current = el; // Set the element to the `outerRef`
ref.current = el; // Share the element for other purposes
}}
/>
);
};
Layout Items
React Cool Virtual is designed to simplify the styling and keep all the items in the document flow for rows/columns. However, when working with grids, we need to layout the items in two-dimensional. For that reason, we also provide the start property for you to achieve it.
import { Fragment } from "react";
import useVirtual from "react-cool-virtual";
const Grid = () => {
const row = useVirtual({
itemCount: 1000,
});
const col = useVirtual({
horizontal: true,
itemCount: 1000,
itemSize: 100,
});
return (
<div
style={{ width: "400px", height: "400px", overflow: "auto" }}
ref={(el) => {
row.outerRef.current = el;
col.outerRef.current = el;
}}
>
<div
style={{ position: "relative" }}
ref={(el) => {
row.innerRef.current = el;
col.innerRef.current = el;
}}
>
{row.items.map((rowItem) => (
<Fragment key={rowItem.index}>
{col.items.map((colItem) => (
<div
key={colItem.index}
style={{
position: "absolute",
height: `${rowItem.size}px`,
width: `${colItem.size}px`,
// The `start` property can be used for positioning the items
transform: `translateX(${colItem.start}px) translateY(${rowItem.start}px)`,
}}
>
βοΈ {rowItem.index}, {colItem.index}
</div>
))}
</Fragment>
))}
</div>
</div>
);
};
Working in TypeScript
React Cool Virtual is built with TypeScript, you can tell the hook what type of your outer and inner elements are as follows.
If the outer element and inner element are the different types:
const App = () => {
const { outerRef, innerRef } = useVirtual<HTMLDivElement, HTMLUListElement>();
return (
<div ref={outerRef}>
<ul ref={innerRef}>{/* Rendering items... */}</ul>
</div>
);
};
If the outer element and inner element are the same types:
const App = () => {
const { outerRef, innerRef } = useVirtual<HTMLDivElement>();
return (
<div ref={outerRef}>
<div ref={innerRef}>{/* Rendering items... */}</div>
</div>
);
};
π‘ For more available types, please check it out.
ResizeObserver Polyfill
ResizeObserver has good support amongst browsers, but it's not universal. You'll need to use polyfill for browsers that don't support it. Polyfills is something you should do consciously at the application level. Therefore React Cool Virtual doesn't include it.
We recommend using @juggle/resize-observer:
$ yarn add @juggle/resize-observer
$ npm install --save @juggle/resize-observer
Then pollute the window
object:
import { ResizeObserver } from "@juggle/resize-observer";
if (!("ResizeObserver" in window)) window.ResizeObserver = ResizeObserver;
You could use dynamic imports to only load the file when the polyfill is required:
(async () => {
if (!("ResizeObserver" in window)) {
const module = await import("@juggle/resize-observer");
window.ResizeObserver = module.ResizeObserver;
}
})();
To Do...
Articles / Blog Posts
π‘ If you have written any blog post or article about React Cool Virtual, please open a PR to add it here.
Contributors β¨
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!