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

Overview

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

Getting Started

Requirement

To use React Cool Virtual, you must use [email protected] 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 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:

{ 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 (
{/* Attach the `innerRef` to the wrapper of the items */}
{items.map(({ index, size }) => ( // You can set the item's height with the `size` property
⭐️ {index}
))}
); }; ">
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} // 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.

Edit RCV - Fixed Size

{ const { outerRef, innerRef, items } = useVirtual({ itemCount: 1000, }); return (
{items.map(({ index, size }) => (
⭐️ {index}
))}
); }; ">
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

{ const { outerRef, innerRef, items } = useVirtual({ itemCount: 1000, itemSize: (idx) => (idx % 2 ? 100 : 50), }); return (
{items.map(({ index, size }) => (
⭐️ {index}
))}
); }; ">
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

{ const { outerRef, innerRef, items } = useVirtual({ itemCount: 1000, itemSize: 75, // The unmeasured item sizes will refer to this value (default = 50) }); return (
{items.map(({ index, measureRef }) => ( // Use the `measureRef` to measure the item size
{/* Some data... */}
))}
); }; ">
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

{ const [h, setH] = useState(height); return (
setH((prevH) => (prevH === 50 ? 100 : 50))} > {children}
); }); const List = () => { const { outerRef, innerRef, items } = useVirtual({ itemCount: 50, }); return (
{items.map(({ index, size, measureRef }) => ( // Use the `measureRef` to measure the item size 👋🏻 Click Me ))}
); }; ">
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.

Edit RCV - RWD

{ 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 changes onResize: (size) => console.log("Outer's size: ", size), }); return (
{/* We can also access the outer's width here */} {items.map(({ index, size, width }) => (
⭐️ {index} ({width})
))}
); }; ">
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 changes
    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.

Sticky Headers

This example demonstrates how to make sticky headers with React Cool Virtual.

Edit RCV - Sticky Headers

{ const { outerRef, innerRef, items } = useVirtual({ itemCount: 1000, itemSize: 75, stickyIndices: [0, 10, 20, 30, 40, 50], // The values must be provided in ascending order }); return (
{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 (
{someData[index].content}
); })}
); }; ">
import useVirtual from "react-cool-virtual";

const List = () => {
  const { outerRef, innerRef, items } = useVirtual({
    itemCount: 1000,
    itemSize: 75,
    stickyIndices: [0, 10, 20, 30, 40, 50], // The values must be provided in ascending order
  });

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

💡 Scrollbars disappear 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:

Edit RCV - Scroll-to Methods

const { scrollTo, scrollToItem } = useVirtual();

const scrollToOffset = () => {
  // Scrolls to 500px
  scrollTo(500, () => {
    // 🤙🏼 Do whatever you want through the callback
  });
};

const scrollToItem = () => {
  // Scrolls to the 500th item
  scrollToItem(500, () => {
    // 🤙🏼 Do whatever you want through the callback
  });

  // We can control the alignment of the item with the `align` option
  // Acceptable values are: "auto" (default) | "start" | "center" | "end"
  // Using "auto" will scroll the item into the view at the start or end, depending on which is closer
  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.

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: 10, smooth: true });

The default easing effect is easeInOutCubic, and the duration is 500 milliseconds. You can easily customize your own effect as follows:

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

{ // 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 (
{items.map(({ index, measureRef }) => (
{comments[index]?.body || "⏳ Loading..."}
))}
); }; ">
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

{ 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 = () =>
⏳ Loading...
; 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 (
{items.length ? ( items.map(({ index, measureRef }) => { const showLoading = index === comments.length - 1 && comments.length < TOTAL_COMMENTS; return (
{comments[index].body}
{showLoading && }
); }) ) : ( )}
); }; ">
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>
  );
};

Working with Input Elements

This example demonstrates how to handle input elements (or form fields) in a virtualized list.

Edit RCV - Input Elements

{ const [formData, setFormData] = useState({ todo: defaultValues }); const { outerRef, innerRef, items } = useVirtual({ itemCount: defaultValues.length, }); const handleInputChange = ({ target }, index) => { // Store the input values in React state setFormData((prevData) => { const todo = [...prevData.todo]; todo[index] = target.checked; return { todo }; }); }; const handleSubmit = (e) => { e.preventDefault(); alert(JSON.stringify(formData, undefined, 2)); }; return (
{items.map(({ index, size }) => (
handleInputChange(e, index)} />
))}
); }; ">
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) => {
    // Store the input values in React state
    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.

{ const { outerRef, innerRef, items } = useVirtual({ itemCount: defaultValues.length, }); const { form } = useForm({ defaultValues: { todo: defaultValues }, removeOnUnmounted: false, // To keep the value of unmounted fields onSubmit: (formData) => alert(JSON.stringify(formData, undefined, 2)), }); return (
{items.map(({ index, size }) => (
))}
); }; ">
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, // To keep the value of unmounted fields
    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.

{items.map(({ index, size }) => ( // Use IDs from your data as keys
{someData[index].content}
))}
); }; ">
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 items will be rendered both on SSR and CSR, depending on our settings */} {items.map(({ index, size }) => (
{someData[index].content}
))}
); }; ">
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, 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.

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

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 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
  }) => {
    // 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 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
  }) => {
    // Do something...
  },
});

onResize

(event: Object) => void

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

const props = useVirtual({
  onResize: ({
    width, // (number) The content width of the outer element
    height, // (number) The content height of the outer element
  }) => {
    // Do something...
  },
});

Return Values

An object with the following properties:

outerRef

React.useRef

A ref to attach to the outer element. We must apply it for using this hook.

innerRef

React.useRef

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.

// Basic usage
scrollTo(500);

// Using options
scrollTo({
  offset: 500,
  smooth: true, // Enable/disable smooth scrolling (default = false)
});

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

// Basic usage
scrollToItem(10);

// Using options
scrollTo({
  index: 10,
  // Control the alignment of the item, acceptable values are: "auto" (default) | "start" | "center" | "end"
  // Using "auto" will scroll the item into the view at the start or end, depending on which is closer
  align: "auto",
  // Enable/disable smooth scrolling (default = false)
  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.

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.

{ // A lot of heavy computing here... 🤪 return (
🐳 Am I heavy?
); }); const List = () => { const { outerRef, innerRef, items } = useVirtual({ itemCount: 1000, itemSize: 75, }); return (
{items.map(({ index, size }) => ( ))}
); }; ">
import { memo } from "react";
import useVirtual from "react-cool-virtual";

const MemoizedItem = memo(({ height, ...rest }) => {
  // A lot of 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.

{ // A lot of heavy computing here... 🤪 return (
🐳 Am I heavy?
); }); const LightItem = (props) =>
🦐 I believe I can fly...
; 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 (
{items.map(({ index, isScrolling, measureRef }) => isScrolling ? ( ) : ( ) )}
); }; ">
import { forwardRef } from "react";
import useVirtual from "react-cool-virtual";

const HeavyItem = forwardRef((props, ref) => {
  // A lot of 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:

{ const ref = useRef(); const { outerRef } = useVirtual(); return (
{ outerRef.current = el; // Set the element to the `outerRef` ref.current = el; // Share the element for other purposes }} /> ); }; ">
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.

{ const row = useVirtual({ itemCount: 1000, }); const col = useVirtual({ horizontal: true, itemCount: 1000, itemSize: 100, }); return (
{ row.outerRef.current = el; col.outerRef.current = el; }} >
{ row.innerRef.current = el; col.innerRef.current = el; }} > {row.items.map((rowItem) => ( {col.items.map((colItem) => (
⭐️ {rowItem.index}, {colItem.index}
))}
))}
); }; ">
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:

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.

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
  • Supports chat
  • scrollBy method

Contributors

Thanks goes to these wonderful people (emoji key):


Welly

🤔 💻 📖 🚇 🚧

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

Issues
  • Items is [] when the component refreshes

    Items is [] when the component refreshes

    It seems that when you mount/unmount the component and the itemCount does not change the items is [] (after the first time).

    A hacky way out of it is to do something like itemCount: myCount + Math.round(Math.random() * 100) and filter later.

    In other words, some kind of check takes place based on the itemCount that causes cache-related (I presume) issues.

    question 
    opened by phaistonian 36
  • Several issues when filtering list from long to shorter (or short to longer)

    Several issues when filtering list from long to shorter (or short to longer)

    Bug Report

    Describe the Bug

    Displaying a short list (81 entries) after a long list (830 entries) skips all but two (or a few) entries of shorter list and introduces excess free space before items displayed. resetScroll is enabled and itemCount managed as local state. This only happens when first (longer) list was scrolled down for a few pages.

    How to Reproduce

    Usage:

      const [itemCount, setItemCount] = React.useState(entries.length)
      const { outerRef, innerRef, items, scrollToItem } = useVirtual({
        itemCount,
        resetScroll: true
      })
    
      React.useEffect(() => {
        setItemCount(entries.length)
      }, [entries])
    
    

    CodeSandbox Link

    I'm working on a CodeSandbox which reproduces this behavior. This will take a few days.

    Expected Behavior

    Second (shorter) list is displayed with first entry at the top (provided resetScroll is working as intended).

    Screenshots

    Screenshot 2021-09-08 at 19 53 05

    Your Environment

    • MacBook Pro (15-inch, 2017)
    • macOS 11.5.2 (Big Sur)
    • Electron 13.1.6
    process.versions = {
      "node": "14.16.0",
      "v8": "9.1.269.36-electron.0",
      "uv": "1.40.0",
      "zlib": "1.2.11",
      "brotli": "1.0.9",
      "ares": "1.16.1",
      "modules": "89",
      "nghttp2": "1.41.0",
      "napi": "7",
      "llhttp": "2.1.3",
      "openssl": "1.1.1",
      "icu": "68.1",
      "unicode": "13.0",
      "electron": "13.1.6",
      "chrome": "91.0.4472.124"
    }
    

    Additional Information

    N/A

    bug 
    opened by dehmer 20
  • TypeError: Cannot read property 'start' of undefined

    TypeError: Cannot read property 'start' of undefined

    Bug Report

    Describe the Bug

    Abstract list components receives rows as props. useVirtual() is used in this component. Filtering (i.e. truncating) rows in parent component leads to TypeError: Cannot read property 'start' of undefined, but only if original/longer list was scrolled down a page or so before filtering.

    How to Reproduce

    Steps to reproduce the behavior, please provide code snippets or a repository:

    Usage

    The error show a little less frequent, when entries are managed as local state (commented out below). resetScroll seems to have no impact on error condition.

      const entries = props.entries
      // const [entries, setEntries] = React.useState(props.entries)
      // React.useEffect(() => {
      //   setEntries(props.entries)
      // }, [props.entries])
    
      const { outerRef, innerRef, items, scrollToItem } = useVirtual({
        itemCount: entries.length,
        resetScroll: true
      })
    
    

    Steps to reproduce in CodeSandbox (provided below):

    1. Display 800 list rows
    2. Scroll down a page or more
    3. Shorten list to 2 rows (combo box) 4.TypeError: Cannot read property 'start' of undefined @ var currStart = msData[vStop].start;

    In case entries are managed as state in 4. Repeat steps 1, 2 and 3 5. TypeError: Cannot read property 'start' of undefined @ var currStart = msData[vStop].start;

    The primary problems seems to be that items array is often 'overshooting', i.e. not in sync with rows/entries, items length exceeds rows length. In this case I can only return null as a result:

      const card = ({ index, measureRef }) => {
      
        // Why can this happen?
        if (index >= entries.length) {
          console.warn('<List/> overshooting', `${index}/${entries.length}`)
          return null
        }
    
        const entry = entries[index]
        return child({
          entry,
          id: entry.id,
          focused: focusId === entry.id,
          selected: selected.includes(entry.id),
          ref: measureRef
        })
      }
    
    

    CodeSandbox Link

    https://codesandbox.io/s/keen-thunder-e0f5m

    Expected Behavior

    Don't crash and burn, when list is filtered in parent.

    Your Environment

    • MacBook Pro (15-inch, 2017)
    • macOS 11.5.2 (Big Sur)
    • Electron 13.1.6
    process.versions = {
      "node": "14.16.0",
      "v8": "9.1.269.36-electron.0",
      "uv": "1.40.0",
      "zlib": "1.2.11",
      "brotli": "1.0.9",
      "ares": "1.16.1",
      "modules": "89",
      "nghttp2": "1.41.0",
      "napi": "7",
      "llhttp": "2.1.3",
      "openssl": "1.1.1",
      "icu": "68.1",
      "unicode": "13.0",
      "electron": "13.1.6",
      "chrome": "91.0.4472.124"
    }
    
    

    Additional Information

    Scrolling down the 800 row list from CodeSandbox in Safari frequently show the following error message in console (without stack trace): [Error] ResizeObserver loop completed with undelivered notifications. (x17) But Safari is NOT the target environment! Might be helpful anyway.

    bug 
    opened by dehmer 10
  • App crashes when scrolling with drag bar

    App crashes when scrolling with drag bar

    In this example when you try to scroll with the mouse, the page freezes and crashes.

    Maybe this is mine alone?

    bug help wanted 
    opened by fourteenmeister 10
  • Problem with relative height (100%) container (outerRef)

    Problem with relative height (100%) container (outerRef)

    First things first: Congratulations on the well designed interface! It was easy as pie to integrate useVirtual() with my existing list. Well done!

    Bug Report

    Describe the Bug

    • List contains items with variable (but fixed) heights.
    • Container (outerRef) has relative height of 100%. Necessary to scale list height with window height (Electron Application).

    How to Reproduce

    When scrolling down, at some point the list/container outgrows its allotted size towards to bottom of the screen (see screenshots below). Exactly when list (innerRef) margin-topexceeds container (outerRef) clientHeight. Also, from this point on, the number of items grows with each step down.

    const List = props => {
      const { child, entries } = props
      const { outerRef, innerRef, items, scrollToItem } = useVirtual({
        itemCount: entries.length,
        itemSize: 140, // average/estimate [~100px; ~200px]
        overscanCount: 10 // has no effect on observed issue
      })
    
      React.useEffect(() => {
        if (props.scroll === 'none') return
        if (props.focusIndex === -1) return
        scrollToItem(props.focusIndex)
      }, [scrollToItem, props.focusIndex, props.scroll])
    
      const card = ({ index, measureRef }) => {
        const entry = entries[index]
        return child({
          entry,
          id: entry.id, // key
          ref: measureRef,
          focused: props.focusId === entry.id,
          selected: props.selected.includes(entry.id)
        })
      }
    
      return (
        <div className='list-container' ref={outerRef}>
          <div
            ref={innerRef}
            className='list'
          >
            { entries.length ? items.map(card) : null }
          </div>
        </div>
      )
    }
    
    

    CodeSandbox Link

    I can try to isolate the problem, if it would be helpful. I'm holding back until your initial feedback.

    Expected Behavior

    • Main issue: List/container should keep its size while scrolling through the complete list.
    • (Minor) issue: Focused item should always be completely visible (scrollToItem(index)).

    Screenshots

    • A: first item is focused
    • B: last item is focused before list starts to grow. Focused item is not visible though (right below item 1OSC).
    • C: list has outgrown its allotted height. This should not happen.
    Screenshot 2021-08-30 at 14 01 39

    Your Environment

    • MacBook Pro (15-inch, 2017)
    • macOS 11.5.2 (Big Sur)
    • Electron 13.1.6
    process.versions = {
      "node": "14.16.0",
      "v8": "9.1.269.36-electron.0",
      "uv": "1.40.0",
      "zlib": "1.2.11",
      "brotli": "1.0.9",
      "ares": "1.16.1",
      "modules": "89",
      "nghttp2": "1.41.0",
      "napi": "7",
      "llhttp": "2.1.3",
      "openssl": "1.1.1",
      "icu": "68.1",
      "unicode": "13.0",
      "electron": "13.1.6",
      "chrome": "91.0.4472.124"
    }
    
    
    opened by dehmer 8
  • stickyIndices can be dynamic

    stickyIndices can be dynamic

    stickyIndices must be dynamic. A good example of a chat room where the date should always be updated is ( #239 )

    opened by nikitapilgrim 6
  • New option: scrollToTopOnChange

    New option: scrollToTopOnChange

    The title explains it :)

    When the itemCount changes, i.e. when new items are passed, the optionally (or by default) scrollTo(0) should be invoked.

    Ps: what I do now is a useEffect.

    enhancement 
    opened by phaistonian 4
  • chore(deps-dev): bump eslint-config-welly from 1.11.1 to 1.11.2

    chore(deps-dev): bump eslint-config-welly from 1.11.1 to 1.11.2

    Bumps eslint-config-welly from 1.11.1 to 1.11.2.

    Release notes

    Sourced from eslint-config-welly's releases.

    v1.11.2

    Upgrade the follows dependencies:

    Commits
    • 2ea79d5 Release 1.11.2
    • 076a6de upgrade: typescript ^4.3.2 → ^4.3.4 & typescript-eslint ^4.26.1 → ^4.27.0
    • 8bdd4e3 docs(readme): update packages table
    • See full diff in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    dependencies 
    opened by dependabot[bot] 4
  • chore(deps-dev): bump jest from 27.0.3 to 27.0.4

    chore(deps-dev): bump jest from 27.0.3 to 27.0.4

    Bumps jest from 27.0.3 to 27.0.4.

    Release notes

    Sourced from jest's releases.

    27.0.4

    Fixes

    • [jest-config, jest-resolve] Pass in require.resolve to resolvers to resolve from correct base (#11493)
    Changelog

    Sourced from jest's changelog.

    27.0.4

    Fixes

    • [jest-config, jest-resolve] Pass in require.resolve to resolvers to resolve from correct base (#11493)
    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    dependencies 
    opened by dependabot[bot] 4
  • Fix: real-time resize jumping

    Fix: real-time resize jumping

    • Fix: real-time resize jumping
    opened by wellyshen 4
  • Fix: correct the event properties of `loadMore` when scrolling backward

    Fix: correct the event properties of `loadMore` when scrolling backward

    • Fix: correct the event properties of loadMore when scrolling backward ( #284 )
    opened by wellyshen 2
  • Wrong indices passed to loadMore

    Wrong indices passed to loadMore

    Bug Report

    First of all, this library is great and really easy to use. I was making a demo with lazy loading + filtering and I think I found a bug:

    Describe the Bug

    The indices passed to loadMore are not always correct. For example:

    • 50 items, page size 5
    • Scroll down to the very bottom
    • Now, the loadMore method receives e.startIndex=50 and e.stopIndex=54.

    This is wrong because there are only 50 items. When the user scrolls down quickly to the bottom, the loadMore method is never called with indices 45-49. Then the items are never loaded and the user keeps seeing the loading animation.

    Also, even if you don't scroll to the end of the list, scrolling to certain positions results in the data not being loaded.

    CodeSandbox Link

    https://codesandbox.io/s/rcv-infinite-scroll-forked-g48o7

    This is pretty much just the Infinite Scroll demo (https://github.com/wellyshen/react-cool-virtual#infinite-scroll). That demo itself contains a bug when loading the data -- it just appends the new comments to the existing array, but when the user scroll quickly the indices are not necessarily continuous. I fixed this in the demo by setting the item to the appropriate index.

    Other than that, I only added a console log to loadMore.

    This is a screenshot of what can happen when quickly scrolling down to the last item. loadMore should load items 500-504, which don't even exist. Note that the last call to loadMore was with the indices 430-434. This results in the visible items not getting loaded at all and the user keeps seeing the loading animation.

    image

    Expected Behavior

    The correct indices for the visible range are passed to loadMore and isItemLoaded.

    I've seen that the index is computed as Math.floor((vStop + 1) / loadMoreCount) (https://github.com/wellyshen/react-cool-virtual/blob/master/src/useVirtual.ts#L445). With +1 it may skip to the next page / batch. It also does not take into account the vStart value, when the user scrolls down quickly those items may not be loaded either.

    I'm not quite sure, but I suppose the computation should use the vStart and vStop values, quantize those to the corresponding page / batch number, then use that and take care of the case when two pages / batches are visible on the screen and need to be loaded?

    bug 
    opened by blutorange 1
  • chore(deps-dev): bump @types/react from 17.0.20 to 17.0.29

    chore(deps-dev): bump @types/react from 17.0.20 to 17.0.29

    Bumps @types/react from 17.0.20 to 17.0.29.

    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    dependencies 
    opened by dependabot[bot] 1
  • chore(deps-dev): bump lint-staged from 11.1.2 to 11.2.3

    chore(deps-dev): bump lint-staged from 11.1.2 to 11.2.3

    Bumps lint-staged from 11.1.2 to 11.2.3.

    Release notes

    Sourced from lint-staged's releases.

    v11.2.3

    11.2.3 (2021-10-10)

    Bug Fixes

    • unbreak windows by correctly normalizing cwd (#1029) (f861d8d)

    v11.2.2

    11.2.2 (2021-10-09)

    Bug Fixes

    v11.2.1

    11.2.1 (2021-10-09)

    Bug Fixes

    v11.2.0

    11.2.0 (2021-10-04)

    Features

    v11.2.0-beta.1

    11.2.0-beta.1 (2021-10-02)

    Bug Fixes

    • add --no-stash as hidden option for backwards-compatibility (73db492)
    • do not apply empty patch (a7c1c0b)
    • do not use fs/promises for Node.js 12 compatibility (c99a6a1)
    • restore original state when preventing an empty commit (f7ef8ef)
    • restore previous order of jobs (ba62b22)

    Features

    • do not use a git stash for better performance (ff0cc0d)

    ... (truncated)

    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    dependencies 
    opened by dependabot[bot] 1
  • chore(deps-dev): bump jest from 27.1.1 to 27.2.5

    chore(deps-dev): bump jest from 27.1.1 to 27.2.5

    Bumps jest from 27.1.1 to 27.2.5.

    Release notes

    Sourced from jest's releases.

    v27.2.5

    Features

    • [jest-config] Warn when multiple Jest configs are located (#11922)

    Fixes

    • [expect] Pass matcher context to asymmetric matchers (#11926 & #11930)
    • [expect] Improve TypeScript types (#11931)
    • [expect] Improve typings of toThrow() and toThrowError() matchers (#11929)
    • [jest-cli] Improve --help printout by removing defunct --browser option (#11914)
    • [jest-haste-map] Use distinct cache paths for different values of computeDependencies (#11916)
    • [@jest/reporters] Do not buffer console.logs when using verbose reporter (#11054)

    Chore & Maintenance

    • [expect] Export default matchers (#11932)
    • [@jest/types] Mark deprecated configuration options as @deprecated (#11913)

    New Contributors

    Full Changelog: https://github.com/facebook/jest/compare/v27.2.4...v27.2.5

    27.2.4

    Features

    • [expect] Add equality checks for Array Buffers in expect.ToStrictEqual() (#11805)

    Fixes

    • [jest-snapshot] Correctly indent inline snapshots (#11560)

    27.2.3

    Features

    • [@jest/fake-timers] Update @sinonjs/fake-timers to v8 (#11879)

    Fixes

    • [jest-config] Parse testEnvironmentOptions if received from CLI (#11902)
    • [jest-reporters] Call destroy on v8-to-istanbul converters to free memory (#11896)

    27.2.2

    Fixes

    • [jest-runtime] Correct wrapperLength value for ESM modules. (#11893)

    ... (truncated)

    Changelog

    Sourced from jest's changelog.

    27.2.5

    Features

    • [jest-config] Warn when multiple Jest configs are located (#11922)

    Fixes

    • [expect] Pass matcher context to asymmetric matchers (#11926 & #11930)
    • [expect] Improve TypeScript types (#11931)
    • [expect] Improve typings of toThrow() and toThrowError() matchers (#11929)
    • [jest-cli] Improve --help printout by removing defunct --browser option (#11914)
    • [jest-haste-map] Use distinct cache paths for different values of computeDependencies (#11916)
    • [@jest/reporters] Do not buffer console.logs when using verbose reporter (#11054)

    Chore & Maintenance

    • [expect] Export default matchers (#11932)
    • [@jest/types] Mark deprecated configuration options as @deprecated (#11913)

    Performance

    27.2.4

    Features

    • [expect] Add equality checks for Array Buffers in expect.ToStrictEqual() (#11805)

    Fixes

    • [jest-snapshot] Correctly indent inline snapshots (#11560)

    27.2.3

    Features

    • [@jest/fake-timers] Update @sinonjs/fake-timers to v8 (#11879)

    Fixes

    • [jest-config] Parse testEnvironmentOptions if received from CLI (#11902)
    • [jest-reporters] Call destroy on v8-to-istanbul converters to free memory (#11896)

    27.2.2

    Fixes

    • [jest-runtime] Correct wrapperLength value for ESM modules. (#11893)

    27.2.1

    ... (truncated)

    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    dependencies 
    opened by dependabot[bot] 1
  • chore(deps-dev): bump @testing-library/react from 12.1.0 to 12.1.2

    chore(deps-dev): bump @testing-library/react from 12.1.0 to 12.1.2

    Bumps @testing-library/react from 12.1.0 to 12.1.2.

    Release notes

    Sourced from @​testing-library/react's releases.

    v12.1.2

    12.1.2 (2021-10-03)

    Bug Fixes

    • render: Don't reject wrapper types based on statics (#973) (7f53b56)

    v12.1.1

    12.1.1 (2021-09-27)

    Bug Fixes

    • TS: make wrapper allow a simple function comp (#966) (cde904c)
    Commits
    • 7f53b56 fix(render): Don't reject wrapper types based on statics (#973)
    • cde904c fix(TS): make wrapper allow a simple function comp (#966)
    • a218b63 docs: add akashshyamdev as a contributor for bug (#967)
    • 84851dc test: Backport tests using the full timer matrix (#962)
    • See full diff in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    dependencies 
    opened by dependabot[bot] 1
  • chore(deps-dev): bump rollup from 2.56.3 to 2.58.0

    chore(deps-dev): bump rollup from 2.56.3 to 2.58.0

    Bumps rollup from 2.56.3 to 2.58.0.

    Release notes

    Sourced from rollup's releases.

    v2.58.0

    2021-10-01

    Features

    • Add a flag to more reliably identify entry points in the resolveId hook (#4230)

    Pull Requests

    v2.57.0

    2021-09-22

    Features

    • Add generatedCode option to allow Rollup to use es2015 features for smaller output and more efficient helpers (#4215)
    • Improve AMD and SystemJS parsing performance by wrapping relevant functions in parentheses (#4215)
    • Using preferConst will now show a warning with strictDeprecations: true (#4215)

    Bug Fixes

    • Improve ES3 syntax compatibility by more consequently quoting reserved words as props in generated code (#4215)
    • Do not use Object.assign in generated code to ensure ES5 compatibility without the need for polyfills (#4215)
    • Support live-bindings in dynamic namespace objects that contain reexported external or synthetic namespaces (#4215)
    • Use correct "this" binding in dynamic import expressions for CommonJS and AMD (#4215)
    • Properly handle shimMissingExports for exports that are only used internally (#4215)
    • Prevent unhandled rejection for failed module parsing (#4228)

    Pull Requests

    Changelog

    Sourced from rollup's changelog.

    2.58.0

    2021-10-01

    Features

    • Add a flag to more reliably identify entry points in the resolveId hook (#4230)

    Pull Requests

    2.57.0

    2021-09-22

    Features

    • Add generatedCode option to allow Rollup to use es2015 features for smaller output and more efficient helpers (#4215)
    • Improve AMD and SystemJS parsing performance by wrapping relevant functions in parentheses (#4215)
    • Using preferConst will now show a warning with strictDeprecations: true (#4215)

    Bug Fixes

    • Improve ES3 syntax compatibility by more consequently quoting reserved words as props in generated code (#4215)
    • Do not use Object.assign in generated code to ensure ES5 compatibility without the need for polyfills (#4215)
    • Support live-bindings in dynamic namespace objects that contain reexported external or synthetic namespaces (#4215)
    • Use correct "this" binding in dynamic import expressions for CommonJS and AMD (#4215)
    • Properly handle shimMissingExports for exports that are only used internally (#4215)
    • Prevent unhandled rejection for failed module parsing (#4228)

    Pull Requests

    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    dependencies 
    opened by dependabot[bot] 1
  • chore(deps-dev): bump @rollup/plugin-commonjs from 20.0.0 to 21.0.0

    chore(deps-dev): bump @rollup/plugin-commonjs from 20.0.0 to 21.0.0

    Bumps @rollup/plugin-commonjs from 20.0.0 to 21.0.0.

    Changelog

    Sourced from @​rollup/plugin-commonjs's changelog.

    v21.0.0

    2021-10-01

    Breaking Changes

    • fix: use safe default value for ignoreTryCatch (#1005)
    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    dependencies 
    opened by dependabot[bot] 1
  • chore(deps-dev): bump @rollup/plugin-node-resolve from 13.0.4 to 13.0.5

    chore(deps-dev): bump @rollup/plugin-node-resolve from 13.0.4 to 13.0.5

    Bumps @rollup/plugin-node-resolve from 13.0.4 to 13.0.5.

    Changelog

    Sourced from @​rollup/plugin-node-resolve's changelog.

    v13.0.5

    2021-09-21

    Updates

    • docs: fix readme heading depth (#999)
    Commits
    • d4ef29a chore(release): node-resolve v13.0.5
    • 68ee2c5 docs(node-resolve): fix readme heading depth (#999)
    • 6ad1721 chore(repo): update CHANGELOGs with missing info, add newline to CHANGELOG gen
    • 5b45582 chore(release): node-resolve v13.0.4
    • See full diff in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    dependencies 
    opened by dependabot[bot] 1
  • chore(deps-dev): bump @types/jest from 27.0.1 to 27.0.2

    chore(deps-dev): bump @types/jest from 27.0.1 to 27.0.2

    Bumps @types/jest from 27.0.1 to 27.0.2.

    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    dependencies 
    opened by dependabot[bot] 1
Releases(v0.5.4)
Owner
Welly
⚛️ Work for SAAS in the day. Contribute OSS at night, mostly @reactjs.
Welly
React Hook for accessing state and dispatch from a Redux store

redux-react-hook React hook for accessing mapped state and dispatch from a Redux store. Table of Contents Install Quick Start Usage StoreContext useMa

Facebook Incubator 2.2k Oct 4, 2021
Essential React custom hooks ⚓ to super charge your components!

Essential React custom hooks ⚓ to super charge your components!

Bhargav Ponnapalli 1.9k Oct 16, 2021
Custom React hooks for your project.

Captain hook Overview Here is a modest list of hooks that I use every day. I will add more in the next few days, keep watching. And if you have some g

Steven Persia 338 Sep 29, 2021
React hook library, ready to use, written in Typescript.

This is the repository for usehooks.ts, a Gatsby powered blog hosted with Github & netlify that publishes easy to understand React Hook code snippets.

Julien 550 Oct 13, 2021
Testing hooks with Jest

Jooks (Jest ❤ + Hooks ????) If you're going through hell testing React Hooks, keep going. (Churchill) What are Custom React Hooks React Hooks are a ne

Antoine Jaussoin 74 Oct 9, 2021
A React.js global state manager with Hooks

A React.js global state manager with Hooks

Lorenzo Spinelli 60 Sep 13, 2021
React hooks for Material UI

React hooks for Material UI Install npm install use-mui or yarn add use-mui Supported components For each state, each hook accepts an optional default

Charles Stover 43 Oct 15, 2021
React hook for conveniently use Fetch API

react-fetch-hook React hook for conveniently use Fetch API. Tiny (556 B). Calculated by size-limit Both Flow and TypeScript types included import Reac

Ilya Lesik 303 Oct 3, 2021
🧘Managed, cancelable and safely typed requests.

Managed, cancelable and safely typed requests. Table of Contents Install Quick Start Usage useResource useRequest request() createRequestError() Type

Matheus Schettino 111 Aug 19, 2021
🐭 React hook that tracks mouse events on selected element - zero dependencies

React Mighty Mouse React hook that tracks mouse events on selected element. Demo Demos created with React DemoTab ?? Install npm install react-hook-mi

mkosir 85 Sep 6, 2021
use css in js with react hook.

style-hook easy to write dynamic css features use css in react hook easy to get started install use npm npm i -S style-hook or use yarn yarn add style

null 15 Sep 1, 2021
Easily manage the Google Tag Manager via Hook

React Google Tag Manager Hook Use easily the Google Tag Manager With this custom hook, you can easily use the Google Tag Manager with 0 config, you ju

Guido Porcaro 105 Oct 13, 2021
😎 📍 React hook for Google Maps Places Autocomplete.

USE-PLACES-AUTOCOMPLETE This is a React hook for Google Maps Places Autocomplete, which helps you build a UI component with the feature of place autoc

Welly 874 Oct 12, 2021
React hook to handle any async operation in React components, and prevent race conditions

React-async-hook This library only does one small thing, and does it well. Don't expect it to grow in size, because it is feature complete: Handle fet

Sébastien Lorber 917 Oct 6, 2021
Tiny utility package for easily creating reusable implementations of React state provider patterns.

react-state-patterns Tiny utility package for easily creating reusable implementations of React state provider patterns. ?? react-state-patterns makes

Michael Clayton 15 Feb 1, 2021
⚛️ Minimal "optimistic UI" pattern implementation with a React Hook

react-optimistic-ui-hook Minimal zero dependency "optimistic UI" pattern implementation in a React Hook. What is "Optimistic UI"? Optimistic UIs don’t

Mohamad Jahani 21 Sep 7, 2021
React hook for using keyboard shortcuts in components.

react-hotkeys-hook React hook for using keyboard shortcuts in components. This is a hook version for the hotkeys package. Documentation and live examp

Johannes Klauss 843 Oct 13, 2021
React Hook used to make API calls

query-hook React hook used to make API calls Install npm install --save query-hook Usage JavaScript import { useQuery, Status } from 'query-hook'; co

Jérémy Surieux 2 Oct 13, 2021
⚛️ A React Hook to monitor changes in the size of an element using native ResizeObserver API 🔍

⚛️ A React Hook to monitor changes in the size of an element using native ResizeObserver API ??

Gautam Arora 23 Oct 3, 2021