🚧 Work in progress, most APIs are done. Not production ready yet, but you can try it!
♻️
react-cool-virtual
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 resize heights/widths.
- 🖥 Supports RWD (responsive web design) for better UX.
- 🚚 Built-ins load more callback for you to deal with infinite scroll + skeleton screens.
- 🖱 Imperative scroll-to controls for offset, items, and alignment.
- 🛹 Out of the box smooth scrolling and the effect is DIY-able.
- ⛳ Provides
isScrolling
indicator to you for performance optimization or other purposes. - 🗄️ Supports server-side rendering (SSR) for faster FCP and better SEO.
- 📜 Supports TypeScript type definition.
- 🎛 Super flexible API design, built with DX in mind.
- 🦔 Tiny size (~ 2.8kB 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 applying styles for using it can be verbose. 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
Frequently viewed docs:
Getting Started
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 CND 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} // Set the scroll container with the `outerRef`
style={{ width: "300px", height: "500px", overflow: "auto" }}
>
{/* Set the inner element with the `innerRef` */}
<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
Some of the common use cases that React Cool Virtual can help you out.
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>
);
};
💡 Scrollbar thumb is jumping? It's because the total size of the items is gradually corrected along with an item 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 Item = 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: (rect) => console.log("Outer's rect: ", rect),
});
return (
<div
style={{ width: "300px", 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>
);
};
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: 500, align: "center" });
};
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: 500, smooth: true });
The default easing effect is easeInOutCubic, and the duration is 500 milliseconds. You can easily customize your own effect as follows:
const { scrollTo } = useVirtual({
scrollDuration: 500,
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) => [...prevComments, ...comments]);
} catch (err) {
isItemLoadedArr[loadIndex] = false;
loadData({ loadIndex }, setComments);
}
};
const List = () => {
const [comments, setComments] = useState([]);
const { outerRef, innerRef, items } = useVirtual({
itemCount: TOTAL_COMMENTS,
itemSize: 122,
loadMoreThreshold: BATCH_COMMENTS,
isItemLoaded: (loadIndex) => isItemLoadedArr[loadIndex],
loadMore: (e) => loadData(e, setComments),
});
return (
<div
style={{ width: "300px", height: "300px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{items.map(({ index, measureRef }) => (
<div
key={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 = [];
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,
itemSize: 122,
loadMoreThreshold: BATCH_COMMENTS,
isItemLoaded: (loadIndex) => isItemLoadedArr[loadIndex],
loadMore: (e) => loadData(e, setComments),
});
return (
<div
style={{ width: "300px", height: "300px", overflow: "auto" }}
ref={outerRef}
>
<div ref={innerRef}>
{items.length ? (
items.map(({ index, measureRef }) => {
const len = comments.length;
const showLoading = index === len - 1 && len < TOTAL_COMMENTS;
return (
<Fragment key={index}>
<div
style={{ padding: "16px", minHeight: "122px" }}
ref={measureRef}
>
{comments[index].body}
</div>
{showLoading && <Loading />}
</Fragment>
);
})
) : (
<Loading />
)}
</div>
</div>
);
};
Dealing with Dynamic Items
Coming soon...
Server-side Rendering (SSR)
Coming soon...
Performance Optimization
Coming soon...
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
}}
/>
);
};
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:
const App = () => {
const { outerRef, innerRef } = useVirtual<HTMLDivElement, HTMLDivElement>();
return (
<div ref={outerRef}>
<div ref={innerRef}>{/* Rendering items... */}</div>
</div>
);
};
💡 For more available types, please check it out.
API
Coming soon...
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...
Contributors ✨
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!