JSPM

  • Created
  • Published
  • Downloads 10332
  • Score
    100M100P100Q133928F
  • License MIT

A tiny React hook for rendering large datasets like a breeze.

Package Exports

  • react-cool-virtual

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (react-cool-virtual) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

🚧 Work in progress, most APIs are done. Not production ready yet but soon!



â™ŧïļ
react-cool-virtual


A tiny React hook for rendering large datasets like a breeze.

npm version npm downloads gzip size All Contributors

Features

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
# or
$ 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>
<!-- react-cool-virtual comes here -->
<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, // Provide the total number for the list items
    itemSize: 50, // The size of each item (default = 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.

Edit RCV - Fixed Size

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.

Edit RCV - Variable Size

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.

Edit RCV - Dynamic Size

import useVirtual from "react-cool-virtual";

const List = () => {
  const { outerRef, innerRef, items } = useVirtual({
    itemCount: 1000,
    itemSize: 75, // The unmeasured item sizes will refer to this value (default = 50)
  });

  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.

Edit RCV - Real-time Resize

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.

Edit RCV - RWD

import useVirtual from "react-cool-virtual";

const List = () => {
  const { outerRef, innerRef, items } = useVirtual({
    itemCount: 1000,
    // Use the outer's width (2nd parameter) to adjust the item's size
    itemSize: (_, width) => (width > 400 ? 50 : 100),
    // The event will be triggered on outer's size is being changed
    onResize: (rect) => console.log("Outer's rect: ", rect),
  });

  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>
  );
};

Scroll to Offset/Items

You can imperatively scroll to offset or items as follows:

Edit RCV - Scroll-to Controls

const { scrollTo, scrollToItem } = useVirtual();

const scrollToOffset = () => {
  // Scroll to 500px
  scrollTo(500, () => {
    // ðŸĪ™ðŸž Do whatever you want through the callback
  });
};

const scrollToItem = () => {
  // Scroll to the 500th item
  scrollToItem(500, () => {
    // ðŸĪ™ðŸž Do whatever you want through the callback
  });

  // Control the alignment of the item with the `align` option
  // Available values: "auto" (default) | "start" | "center" | "end"
  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.

Edit RCV - Smooth Scrolling

const { scrollTo, scrollToItem } = useVirtual();

// Smoothly scroll to 500px
const scrollToOffset = () => scrollTo({ offset: 500, smooth: true });

// Smoothly scroll to the 500th item
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({
  // For 500 milliseconds (default = 500ms)
  scrollDuration: 500,
  // Using "easeInOutBack" effect (default = easeInOutCubic), see: https://easings.net/#easeInOutBack
  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 ðŸĪ”.

Edit RCV - Infinite Scroll

Working with Skeleton Screens

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) => {
  // Set the state of a batch items as `true`
  // to avoid the callback from being invoked repeatedly
  isItemLoadedArr[loadIndex] = true;

  try {
    const { data: comments } = await axios(`/comments?postId=${loadIndex + 1}`);

    setComments((prevComments) => [...prevComments, ...comments]);
  } catch (err) {
    // If there's an error set the state back to `false`
    isItemLoadedArr[loadIndex] = false;
    // Then try again
    loadData({ loadIndex }, setComments);
  }
};

const List = () => {
  const [comments, setComments] = useState([]);
  const { outerRef, innerRef, items } = useVirtual({
    itemCount: TOTAL_COMMENTS,
    // Estimated item size (with padding)
    itemSize: 122,
    // The number of items that you want to load/or pre-load, it will trigger the `loadMore` callback
    // when the user scrolls within every items, e.g. 1 - 5, 6 - 10, and so on (default = 15)
    loadMoreCount: BATCH_COMMENTS,
    // Provide the loaded state of a batch items to the callback for telling the hook
    // whether the `loadMore` should be triggered or not
    isItemLoaded: (loadIndex) => isItemLoadedArr[loadIndex],
    // We can fetch the data through the callback, it's invoked when more items need to be loaded
    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={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;
// We only have 50 (500 / 5) batches of items, so set the 51th (index = 50) batch as `true`
// to avoid the `loadMore` callback from being invoked, yep it's a trick 😉
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, // Provide the number of comments
    loadMoreCount: 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 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>
  );
};

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. The

const List = () => {
  const { outerRef, innerRef, items } = useVirtual({
    itemCount: 1000,
    ssrItemCount: 30, // Renders 0th - 30th items on SSR
    // or
    ssrItemCount: [50, 80], // Renders 50th - 80th items on SSR
  });

  return (
    <div
      style={{ width: "300px", height: "300px", overflow: "auto" }}
      ref={outerRef}
    >
      <div ref={innerRef}>
        {/* The items will be rendered both on SSR and CSR, depends 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.

Performance Optimization

Items are re-rendered whenever the user scrolls. If your item is a heavy data component, there're two strategies for performance optimizing.

Use React.memo

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 }) => {
  // Some many heavy computing here... ðŸĪŠ

  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) => {
  // Some many heavy computing here... ðŸĪŠ

  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, // Just use it (default = false)
    // or
    useIsScrolling: (speed) => speed > 50, // Use it based on the scroll speed (more user friendly)
  });

  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
      }}
    />
  );
};

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 = () => {
  // 1st is the `outerRef`, 2nd is the `innerRef`
  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

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 props = 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.

  • For number use case, please refer to the fixed size.
  • For index callback use case, please refer to the variable size.
  • For width callback use case, please refer to the RWD.

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 offset.

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.

scrollDuration

number

The duration of smooth scrolling, the unit is milliseconds (default = 500ms).

scrollEasingFunction

(time: number) => number

A function that allows us to customize the easing effect of smooth scrolling (default = easeInOutCubic).

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 props = useVirtual({
  onScroll: ({
    startIndex, // (number) The start index of the batch item
    stopIndex, // (number) The stop index of the batch item
    loadIndex, // (number) The index of a batch items (e.g. 1 - 15 as 0, 16 - 30 as 1, and so on)
    scrollOffset, // (number) The scroll offset from top/left, depends on the `horizontal` option
    userScroll, // (boolean) Tells you the scrolling is through the user or not
  }) => {
    // Fetch data...
  },
});

onScroll

(event: Object) => void

This event will be triggered when scroll position is being changed by the user scrolls or scrollTo/scrollToItem methods.

const props = useVirtual({
  onScroll: ({
    overscanStartIndex, // (number) The start index of the overscan item
    overscanStopIndex, // (number) The stop index of the overscan item
    visibleStartIndex, // (number) The start index of the visible item
    visibleStopIndex, // (number) The stop index of the visible item
    scrollOffset, // (number) The scroll offset from top/left, depends on the `horizontal` option
    scrollForward, // (boolean) The scroll direction of up/down or left/right, depends on the `horizontal` option
    userScroll, // (boolean) Tells you the scrolling is through the user or not
  }) => {
    // Do something...
  },
});

onResize

(event: Object) => void

This event will be triggered when the size of the outer container element changes.

Props

An object with the following properties:

outerRef

React.useRef<HTMLElement>

A ref to attach to the outer container element.

innerRef

React.useRef<HTMLElement>

A ref to attach to the inner container element.

items

Object[]

Coming soon...

scrollTo

(offsetOrOptions: number | Object, callback?: () => void) => void

Coming soon...

scrollToItem

(indexOrOptions: number | Object, callback?: () => void) => void

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
# or
$ 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...

  • Unit testing
  • Reverse scrolling
  • Infinite loop
  • scrollBy method

Contributors âœĻ

Thanks goes to these wonderful people (emoji key):


Welly

ðŸĪ” ðŸ’ŧ 📖 🚇 🚧

This project follows the all-contributors specification. Contributions of any kind welcome!