GithubHelp home page GithubHelp logo

wellyshen / react-cool-virtual Goto Github PK

View Code? Open in Web Editor NEW
1.2K 10.0 37.0 4.34 MB

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

License: MIT License

JavaScript 6.22% Shell 0.20% HTML 3.06% TypeScript 90.03% SCSS 0.50%
dom-recycle server-side-rendering infinite-scroll react hook performance memory typescript lazy-loading smooth-scrolling

react-cool-virtual's Introduction

React Cool Virtual

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

npm version npm downloads coverage status gzip size best of js 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 using and styling it can be verbose (because it's a low-level hook). Furthermore, it lacks many of the useful features.

React Cool Virtual is a tiny React hook that gives you a better DX and modern way for virtualizing a large amount of data without struggle 🤯.

Docs

Getting Started

Requirement

To use React Cool Virtual, you must use [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> tag to add it after React CDN links as below:

<script crossorigin src="https://unpkg.com/react/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"></script>
<!-- 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} // 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

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

💡 The scrollbar is jumping (or unexpected position)? It's because the total size of the items is gradually corrected along with an item that has been measured. You can tweak the itemSize to reduce the phenomenon.

Real-time Resize

This example demonstrates how to create a real-time resize row (e.g. accordion, collapse, etc.). For column or grid, please refer to CodeSandbox.

Edit RCV - Real-time Resize

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

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

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

💡 For better performance & accessibility. We encourage you to add will-change:transform to the positioned elements to render the element in its own layer, improving repaint speed and therefore improving performance and accessibility.

💡 The scrollbar disappears when using Chrome in Mac? If you encounter this issue, you can add will-change:transform to the outer element to workaround this problem.

Scroll to Offset / Items

You can imperatively scroll to offset or items as follows:

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

💡 When working with dynamic size, the scroll position will be automatically corrected along with the items are measured. To optimize it, we can provide an estimated item size to the itemSize option.

The default easing effect is easeInOutSine, and the duration is 100ms <= distance * 0.075 <= 500ms. You can easily customize your own effect as follows:

const { scrollTo } = useVirtual({
  // For 500 milliseconds
  scrollDuration: 500,
  // Or whatever duration you want based on the scroll distance
  scrollDuration: (distance) => distance * 0.05,
  // Using "easeInOutBack" effect (default = easeInOutSine), see: https://easings.net/#easeInOutSine
  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) => {
      const nextComments = [...prevComments];

      comments.forEach((comment) => {
        nextComments[comment.id - 1] = comment;
      });

      return nextComments;
    });
  } 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: "500px", overflow: "auto" }}
      ref={outerRef}
    >
      <div ref={innerRef}>
        {items.map(({ index, measureRef }) => (
          <div
            key={comments[index]?.id || `fb-${index}`}
            style={{ padding: "16px", minHeight: "122px" }}
            ref={measureRef} // Used to measure the unknown item size
          >
            {comments[index]?.body || "⏳ Loading..."}
          </div>
        ))}
      </div>
    </div>
  );
};

Working with A Loading Indicator

import { Fragment, useState } from "react";
import useVirtual from "react-cool-virtual";
import axios from "axios";

const TOTAL_COMMENTS = 500;
const BATCH_COMMENTS = 5;
const isItemLoadedArr = [];
// 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: "500px", overflow: "auto" }}
      ref={outerRef}
    >
      <div ref={innerRef}>
        {items.length ? (
          items.map(({ index, measureRef }) => {
            const showLoading =
              index === comments.length - 1 && comments.length < TOTAL_COMMENTS;

            return (
              <Fragment key={comments[index].id}>
                <div ref={measureRef}>{comments[index].body}</div>
                {showLoading && <Loading />}
              </Fragment>
            );
          })
        ) : (
          <Loading />
        )}
      </div>
    </div>
  );
};

Pre-pending Items

This example demonstrates how to pre-pend items and maintain scroll position for the user.

Edit RCV - Prepend Items

import { useEffect, useLayoutEffect, useState } from "react";

import useVirtual from "react-cool-virtual";
import axios from "axios";

const TOTAL_COMMENTS = 500;
const BATCH_COMMENTS = 5;
let shouldFetchData = true;
let postId = 100;

const fetchData = async (postId, setComments) => {
  try {
    const { data: comments } = await axios(`/comments?postId=${postId}`);

    // Pre-pend new items
    setComments((prevComments) => [...comments, ...prevComments]);
  } catch (err) {
    // Try again
    fetchData(postId, setComments);
  }
};

const List = () => {
  const [comments, setComments] = useState([]);
  const { outerRef, innerRef, items, startItem } = useVirtual({
    // Provide the number of comments
    itemCount: comments.length,
    onScroll: ({ scrollForward, scrollOffset }) => {
      // Tweak the threshold of data fetching that you want
      if (!scrollForward && scrollOffset < 50 && shouldFetchData) {
        fetchData(--postId, setComments);
        shouldFetchData = false;
      }
    },
  });

  useEffect(() => fetchData(postId, setComments), []);

  // Execute the `startItem` through `useLayoutEffect` before the browser to paint
  // See https://reactjs.org/docs/hooks-reference.html#uselayouteffect to learn more
  useLayoutEffect(() => {
    // After the list updated, maintain the previous scroll position for the user
    startItem(BATCH_COMMENTS, () => {
      // After the scroll position updated, re-allow data fetching
      if (comments.length < TOTAL_COMMENTS) shouldFetchData = true;
    });
  }, [comments.length, startItem]);

  return (
    <div
      style={{ width: "300px", height: "500px", overflow: "auto" }}
      ref={outerRef}
    >
      <div ref={innerRef}>
        {items.length ? (
          items.map(({ index, measureRef }) => (
            // Used to measure the unknown item size
            <div key={comments[index].id} ref={measureRef}>
              {comments[index].body}
            </div>
          ))
        ) : (
          <div className="item">⏳ Loading...</div>
        )}
      </div>
    </div>
  );
};

Filtering Items

When working with filtering items, we can reset the scroll position when the itemCount is changed by enabling the resetScroll option.

Edit RCV - Filter Items

import { useState } from "react";
import useVirtual from "react-cool-virtual";

const List = () => {
  const [itemCount, setItemCount] = useState(100);
  const { outerRef, innerRef, items } = useVirtual({
    itemCount,
    // Resets the scroll position when the `itemCount` is changed (default = false)
    resetScroll: true,
  });

  return (
    <div
      style={{ width: "300px", height: "300px", overflow: "auto" }}
      ref={outerRef}
    >
      <div ref={innerRef}>
        {items.map(({ index, size }) => (
          <div key={index} style={{ height: `${size}px` }}>
            ⭐️ {index}
          </div>
        ))}
      </div>
    </div>
  );
};

Sticking to Bottom

This example demonstrates the scenario of sticking/unsticking the scroll position to the bottom for a chatroom.

Edit RCV - Stick to Bottom

import { useState, useEffect } from "react";
import useVirtual from "react-cool-virtual";
import axios from "axios";

const TOTAL_MESSAGES = 200;
let isScrolling = false; // Used to prevent UX conflict
let id = 0;

const loadData = async (id, setMessages) => {
  try {
    const { data: messages } = await axios(`/messages/${id}`);

    setMessages((prevMessages) => [...prevMessages, messages]);
  } catch (err) {
    loadData(id, setMessages);
  }
};

const Chatroom = () => {
  const [shouldSticky, setShouldSticky] = useState(true);
  const [messages, setMessages] = useState([]);
  const { outerRef, innerRef, items, scrollToItem } = useVirtual({
    // Provide the number of messages
    itemCount: messages.length,
    // You can speed up smooth scrolling
    scrollDuration: 50,
    onScroll: ({ userScroll }) => {
      // If the user scrolls and isn't automatically scrolling, cancel stick to bottom
      if (userScroll && !isScrolling) setShouldSticky(false);
    },
  });

  useEffect(() => {
    // Mock messages service
    if (id <= TOTAL_MESSAGES)
      setTimeout(
        () => loadData(++id, setMessages),
        Math.floor(500 + Math.random() * 2000)
      );
  }, [messages.length]);

  useEffect(() => {
    // Automatically stick to bottom, using smooth scrolling for better UX
    if (shouldSticky) {
      isScrolling = true;
      scrollToItem({ index: messages.length - 1, smooth: true }, () => {
        isScrolling = false;
      });
    }
  }, [messages.length, shouldSticky, scrollToItem]);

  return (
    <div>
      <div
        style={{ width: "300px", height: "400px", overflow: "auto" }}
        ref={outerRef}
      >
        <div ref={innerRef}>
          {items.map(({ index, measureRef }) => (
            // Used to measure the unknown item size
            <div key={`${messages[index].id}`} ref={measureRef}>
              <div>{messages[index].content}</div>
            </div>
          ))}
        </div>
      </div>
      {!shouldSticky && (
        <button onClick={() => setShouldSticky(true)}>Stick to Bottom</button>
      )}
    </div>
  );
};

Working with Input Elements

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

Edit RCV - Input Elements

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.

Edit RCV - RCF

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.

const List = () => {
  const { outerRef, innerRef, items } = useVirtual();

  return (
    <div
      ref={outerRef}
      style={{ width: "300px", height: "300px", overflow: "auto" }}
    >
      <div ref={innerRef}>
        {items.map(({ index, size }) => (
          // Use IDs from your data as keys
          <div key={someData[index].id} style={{ height: `${size}px` }}>
            {someData[index].content}
          </div>
        ))}
      </div>
    </div>
  );
};

Server-side Rendering (SSR)

Server-side rendering allows us to provide a fast FP and FCP, it also benefits for SEO. React Cool Virtual supplies you a seamless DX between SSR and CSR.

const List = () => {
  const { outerRef, innerRef, items } = useVirtual({
    itemCount: 1000,
    ssrItemCount: 30, // 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.

resetScroll

boolean

It's used to tell the hook to reset the scroll position when the itemCount is changed (default = false). It's useful for filtering items.

overscanCount

number

The number of items to render behind and ahead of the visible area (default = 1). That can be used for two reasons:

  • To slightly reduce/prevent a flash of empty screen while the user is scrolling. Please note, too many can negatively impact performance.
  • To allow the tab key to focus on the next (invisible) item for better accessibility.

useIsScrolling

boolean

To enable/disable the isScrolling indicator of an item (default = false). It's useful for UI placeholders or performance optimization when the list is being scrolled. Please note, using it will result in an additional render after scrolling has stopped.

stickyIndices

number[]

An array of indexes to make certain items in the list sticky. See the example to learn more.

  • The values must be provided in ascending order, i.e. [0, 10, 20, 30, ...].

scrollDuration

number | (distance: number) => number

The duration of smooth scrolling, the unit is milliseconds (default = 100ms <= distance * 0.075 <= 500ms).

scrollEasingFunction

(time: number) => number

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

loadMoreCount

number

How many number of items that you want to load/or pre-load (default = 15), it's used for infinite scroll. A number 15 means the loadMore callback will be invoked when the user scrolls within every 15 items, e.g. 1 - 15, 16 - 30, and so on.

isItemLoaded

(index: number) => boolean

A callback for us to provide the loaded state of a batch items, it's used for infinite scroll. It tells the hook whether the loadMore should be triggered or not.

loadMore

(event: Object) => void

A callback for us to fetch (more) data, it's used for infinite scroll. It's invoked when more items need to be loaded, which based on the mechanism of loadMoreCount and isItemLoaded.

const loadMore = ({
  startIndex, // (number) The index of the first batch item
  stopIndex, // (number) The index of the last batch item
  loadIndex, // (number) The index of the current batch items (e.g. 1 - 15 as `0`, 16 - 30 as `1`, and so on)
  scrollOffset, // (number) The scroll offset from top/left, depending on the `horizontal` option
  userScroll, // (boolean) Tells you the scrolling is through the user or not
}) => {
  // Fetch data...
};

const props = useVirtual({ loadMore });

onScroll

(event: Object) => void

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

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

const props = useVirtual({ onScroll });

onResize

(event: Object) => void

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

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

const props = useVirtual({ onResize });

Return Values

An object with the following properties:

outerRef

React.useRef<HTMLElement>

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

innerRef

React.useRef<HTMLElement>

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

items

Object[]

The virtualized items for rendering rows/columns. Each item is an object that contains the following properties:

Name Type Description
index number The index of the item.
size number The fixed/variable/measured size of the item.
width number The current content width of the outer element. It's useful for a RWD row/column.
start number The starting position of the item. We might only need this when working with grids.
isScrolling true | undefined An indicator to show a placeholder or optimize performance for the item.
isSticky true | undefined An indicator to make certain items become sticky in the list.
measureRef Function It's used to measure an item with dynamic or real-time heights/widths.

scrollTo

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

This method allows us to scroll to the specified offset from top/left, depending on the horizontal option.

// 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
scrollToItem({
  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.

startItem

(index: number, callback?: () => void) => void

This method is used to work with pre-pending items. It allows us to main the previous scroll position for the user.

Others

Performance Optimization

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

When working with non-dynamic size, we can extract the item to it's own component and wrap it with React.memo. It shallowly compares the current props and the next props to avoid unnecessary re-renders.

import { memo } from "react";
import useVirtual from "react-cool-virtual";

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

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:

import { useRef } from "react";
import useVirtual from "react-cool-virtual";

const App = () => {
  const ref = useRef();
  const { outerRef } = useVirtual();

  return (
    <div
      ref={(el) => {
        outerRef.current = el; // Set the element to the `outerRef`
        ref.current = el; // Share the element for other purposes
      }}
    />
  );
};

Layout Items

React Cool Virtual is designed to simplify the styling and keep all the items in the document flow for rows/columns. However, when working with grids, we need to layout the items in two-dimensional. For that reason, we also provide the start property for you to achieve it.

import { Fragment } from "react";
import useVirtual from "react-cool-virtual";

const Grid = () => {
  const row = useVirtual({
    itemCount: 1000,
  });
  const col = useVirtual({
    horizontal: true,
    itemCount: 1000,
    itemSize: 100,
  });

  return (
    <div
      style={{ width: "400px", height: "400px", overflow: "auto" }}
      ref={(el) => {
        row.outerRef.current = el;
        col.outerRef.current = el;
      }}
    >
      <div
        style={{ position: "relative" }}
        ref={(el) => {
          row.innerRef.current = el;
          col.innerRef.current = el;
        }}
      >
        {row.items.map((rowItem) => (
          <Fragment key={rowItem.index}>
            {col.items.map((colItem) => (
              <div
                key={colItem.index}
                style={{
                  position: "absolute",
                  height: `${rowItem.size}px`,
                  width: `${colItem.size}px`,
                  // The `start` property can be used for positioning the items
                  transform: `translateX(${colItem.start}px) translateY(${rowItem.start}px)`,
                }}
              >
                ⭐️ {rowItem.index}, {colItem.index}
              </div>
            ))}
          </Fragment>
        ))}
      </div>
    </div>
  );
};

Working in TypeScript

React Cool Virtual is built with TypeScript, you can tell the hook what type of your outer and inner elements are as follows.

If the outer element and inner element are the different types:

const App = () => {
  // 1st is the `outerRef`, 2nd is the `innerRef`
  const { outerRef, innerRef } = useVirtual<HTMLDivElement, HTMLUListElement>();

  return (
    <div ref={outerRef}>
      <ul ref={innerRef}>{/* Rendering items... */}</ul>
    </div>
  );
};

If the outer element and inner element are the same types:

const App = () => {
  // By default, the `innerRef` will refer to the type of the `outerRef`
  const { outerRef, innerRef } = useVirtual<HTMLDivElement>();

  return (
    <div ref={outerRef}>
      <div ref={innerRef}>{/* Rendering items... */}</div>
    </div>
  );
};

💡 For more available types, please check it out.

ResizeObserver Polyfill

ResizeObserver has good support amongst browsers, but it's not universal. You'll need to use polyfill for browsers that don't support it. Polyfills is something you should do consciously at the application level. Therefore React Cool Virtual doesn't include it.

We recommend using @juggle/resize-observer:

$ yarn add @juggle/resize-observer
# 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...

  • Support window scrolling
  • Leverage the power of Offscreen API (maybe...)

Articles / Blog Posts

💡 If you have written any blog post or article about React Cool Virtual, please open a PR to add it here.

Contributors ✨

Thanks goes to these wonderful people (emoji key):

Welly
Welly

🤔 💻 📖 🚇 🚧
Nikita Pilgrim
Nikita Pilgrim

💻
Jie Peng
Jie Peng

📖
Alex Lyakhnitskiy
Alex Lyakhnitskiy

💻
Adam Pash
Adam Pash

📖

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

react-cool-virtual's People

Contributors

adampash avatar allcontributors[bot] avatar dependabot[bot] avatar github-actions[bot] avatar neighborhood999 avatar nikitapilgrim avatar wellyshen avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-cool-virtual's Issues

scrollToItem not resolving with dynamic height items

Bug Report

Before I get to the bug details I would first like to say how impressed I've been with using this library so far.
It is straight-forward, well documented and I really like how the implementation doesn't make assumptions on the way to use your components.
Thanks for the time and effort you have put into this! 💯

Describe the Bug

I have a content panel with a long list of variable height items, each using the measureRef option.
The scrollToItem action with the smooth option can get caught in an unbreakable bouncing animation loop as it constantly tries to find the target scroll offset position - see the glitchy video examples below.

How to Reproduce

In my case the content items are quite large so it happens more easily if the initial itemSize is set to a small value like 100, and less so if it is something like window.innerHeight. It also seems to happen less often with align: 'start' but can still occur.

Also note that the measured heights of the items are sometimes floating point values (I'm using an HDPI display).

Forcing the measured size to be rounded down to an integer:

const measuredSize = Math.floor(target.getBoundingClientRect()[sizeKey]);

at

const measuredSize = target.getBoundingClientRect()[sizeKey];

helped, but the problem still happens occasionally.

CodeSandbox Link

I managed to replicate the problem here (it might take a few clicks of the button to see the issue):
https://codesandbox.io/s/rcv-scroll-to-methods-forked-62gf3?file=/index.js

Expected Behavior

It scrolls to position without getting stuck in a bouncing around animation.

It might be that dynamic height items will always have potential for indeterministic behaviour so maybe there needs to be an extra guard against getting stuck in a never ending animation (maybe if the direction changes more than a couple of times it will end the animation).

Screenshots

The issue when it appears on CodeSandbox:
https://user-images.githubusercontent.com/3010/124582941-6c3fd180-de4a-11eb-8ed0-dc7496366ca2.mp4

The extreme case from my app (flicker warning!):
https://user-images.githubusercontent.com/3010/124571677-ecad0500-de3f-11eb-8dec-c1e54848846e.mp4

Your Environment

  • Device: Mac Pro
  • OS: MacOS 11.4
  • Browser: Chrome
  • Version: v92.0.4515.80

Lost sticky header?

https://streamable.com/v6ad13

as you can see the sticky header doesn't work expected

import numeral from 'numeral'
import useVirtual from 'react-cool-virtual'

export default () => {
  const [{ loading, records, count, stickyIndices }, setState] = useState({ loading: true, records: [], count: 0, stickyIndices: [] })

  const countRecords = () => connection.count({
    from: 'Records',
    where: {
      date: {
        '-': {
          low: dayjs().startOf('year').toDate(),
          high: new Date(),
        }
      }
    }
  })

  const getRecords = () => connection.select({
    from: 'Records',
    where: {
      date: {
        '-': {
          low: dayjs().startOf('year').toDate(),
          high: new Date(),
        }
      }
    },
    order: [
      { by: 'date', type: 'desc' },
      { by: 'createdAt', type: 'desc' },
    ]
  })

  const getData = () => {
    Promise.all([countRecords(), getRecords()]).then(([count, records]) => {
      console.log(count)
      const dateHeaderExist = {}
      const newList = []
      const _stickyIndices = []

      let lastHeaderName

      for (const itemIdx in records) {
        const item = records[itemIdx]

        const headerName = dayjs(item.date).format('YYYYMM')


        if (!dateHeaderExist[headerName]) {
          const dateHeader = { headerName, count: 1, heading: dayjs(headerName).format('M'), date: dayjs(headerName).toDate(), fmt: dayjs(headerName).format('YYYY.MM.DD') }
          newList.push(dateHeader)
          newList.push(item)
          lastHeaderName = dateHeaderExist[headerName] = dateHeader
          _stickyIndices.push(parseInt(itemIdx))
        } else {
          newList.push(item)
          lastHeaderName.count += 1
        }
      }

      console.log(_stickyIndices)

      setState({ loading: false, records: newList, count, stickyIndices: _stickyIndices })
    })
  }

  useEffect(() => {
    getData()
    emitter.on('refresh', getData)

    return () => {
      emitter.off('refresh', getData)
    }
  }, [])

  if (loading) {
    return (
      <div className={`flex flex-col flex-center min-h-screen pt-16 pb-14`}>
        <i className='mdi mdi-loading text-4xl animate-spin' />
      </div>
    )
  }

  if (records.length === 0) {
    return (
      <div className='min-h-screen pt-16 pb-14 flex flex-col flex-center'>
        <div>点击下边 + 号记一笔</div>
      </div>
    )
  }

  return (
    <RecordsComponent records={records} count={count} stickyIndices={stickyIndices} />
  )
}

function RecordsComponent ({ records, count, stickyIndices }) {
  const { outerRef, innerRef, items } = useVirtual({
    itemCount: count,
    itemSize: 60,
    stickyIndices,
  })

  return (
    <div ref={outerRef} className='max-h-screen pt-16 pb-14 overflow-y-auto'>
      <div ref={innerRef}>
        {/* {records.map(ItemComponent)} */}
        {items.map(wrapper(records))}
      </div>
    </div>
  )
}

function wrapper (records) {
  return function ItemComponent ({ index, measureRef }) {
    const item = records[index]

    if (item.heading) {
      return (
        <div ref={measureRef} className='sticky top-0 px-4 py-1 bg-blue-50 text-xs'>{item.fmt}</div>
      )
    }

    const incomingTextColor = item.incoming ? 'text-red-500' : 'text-green-500'
    const incomingText = item.incoming ? '+' : '-'

    return (
      <div ref={measureRef} key={item.id} className='flex items-center px-4 py-2 bg-white'>
        <div className='flex-auto flex flex-col pr-8'>
          <div className='font-thin text-xl'>{item.name}</div>
          <div className='space-x-2 text-xs'>
            <span>{item.tag}</span>
            <span>{dayjs(item.date).format('M.D')}</span>
            <span>{index}</span>
          </div>
        </div>
        <div className={`text-sm ${incomingTextColor}`}>
          {incomingText}{numeral(item.amount).format('0,0.00')}
        </div>
      </div>
    )
  }
}

How to use react-cool-virtual with grid css ?

I have responsive grid layout with posts that have titles, cover images, descriptions etc.
React-cool-virtual doesn't detect position of my elements. It's behaviour like it has deal with list and set start position of elements like 0 - 200 - 400 - 600 px etc. Maybe you have some examples i've not found or ideas about such type of problem ?
P.S start property can't be changed. How then to implement margins in grid ?

Bi-directional infinite scroll example

Would be really helpful to have an example of infinite scroll working in both sides (up and down). I was only able to find examples to top or bottom, but not both. Use cases: chat applications.

Thank you for your hard work!

scrollToItem doesn't work as expected when using StickyHeaders

Bug Report

Describe the Bug

scrollToItem doesn't work as expected when using StickyHeaders. When scrolling to an index, and align is 'start', it should be scrolled to the top, but still be visible underneath any 'stuck' headers. Currently, it's scrolled to the absolute top, and thus, is obscured by sticky heads.

How to Reproduce

Create a a demo with sticky headers and try to scrollToItem to an index which is not a sticky header.

Bug when virtualizing a filtered array

Bug Report

Describe the Bug

I'm facing some kind of strange behavior when trying to use virtual scrolling along item filtering. For some reason I'm getting more items in the virtual items array than in the filtered items array. Maybe it's some React quirk that I don't understand?

How to Reproduce

I've prepared a code sandbox (see below).

Try to enter Item #22 in the search box and the app will crash. In the browser console you are able to see that while filtered array has length of 1 (which is correct), virtual array has length of 5 and the app crashes. The same thing will happen if you try to find item that does not exist, try putting "hello" in the search box. If you instead use a commented line, it will fix this.

CodeSandbox Link

https://codesandbox.io/p/sandbox/bold-lucy-ypnsk1

I was searching through issues in this repo and found this: #248
In the code sandbox for this issue you can see that the author was puzzled too with the same thing: https://codesandbox.io/s/lively-fast-5834r?file=/src/List.js:448-655

Expected Behavior

I expect virtual items array to have a length that is not greater than the original items array length.

When chrome smooth scrolling flag is enabled, the scrollToItem api is not working as expected.

Bug Report

Describe the Bug

When chrome smooth scrolling flag is enabled, the scrollToItem api is not working as expected.

How to Reproduce

  1. chrome://flags/#smooth-scrolling (ONLY: Windows, Linux, Chrome OS, Android)
  2. toggle smooth-scrolling into Enabled
  3. scrollToItem is not working

CodeSandbox Link

Expected Behavior

scrollToItem is working and the success callback should be invoked.

Screenshots

Add screenshots to help explain your problem.

Your Environment

  • Device: PC
  • OS: Windows
  • Browser: Chrome
  • Version: 0.5.5

Additional Information

This bug can only be reproduced on Windows. I've already ship this package into production env, please help me with this issue.
Thanks.

Reverse infinity scroll with Safari / iOS flickers and jumps

Bug Report

Describe the Bug

When pre-pending on a reverse list with Safari the list will jump to the top of the added data, sometimes the list will also flicker when new data is added.

This is reproducible for me on iOS/Safari (iphone and ipad) using the reverse infinite scroll example https://codesandbox.io/s/rcv-prepend-items-ui06h (mentioned in #230).

Is there a way to suspend scrolling (or prevent scroll events) when reaching the top of the list before pre-pending more items?

Thanks for this library!

How to Reproduce

Run the below sandbox on iOS using Safari and scroll up to load new data.

CodeSandbox Link

https://codesandbox.io/s/rcv-prepend-items-ui06h

Expected Behavior

Ideally the scroll position would stay at the top of the list like how it works on chrome/desktop currently instead of jumping to the top of the new items added.

Screenshots

React.Cool.virtual.ios.safari.export.mov

Your Environment

  • Device: iPhone, iPad
  • OS: iOS
  • Browser: Safari
  • Version: iOS 16

A scrolling error occurs when "transform: scale(0.5)" is applied to the parent element (outer).

Bug Report

Describe the Bug

I get a scrolling error when transform: scale(0.5) is applied to the parent element (outer). The movement is unnatural and points in a completely different direction.

How to Reproduce

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

스크린샷 2023-05-03 오후 4 16 05

  1. Go to https://codesandbox.io/s/rcv-stick-to-bottom-cxsgw?fontsize=14&hidenavigation=1&theme=dark
  2. Open Devtool, Apply transform: scale(0.5) to the elements with class="outer".
  3. Try hitting Stick to Bottom or scrolling up and down a lot quickly.

CodeSandbox Link

https://codesandbox.io/s/rcv-stick-to-bottom-cxsgw?fontsize=14&hidenavigation=1&theme=dark

Expected Behavior

When I hit the Paste to Bottom button, it should stick to the bottom, but it doesn't, and if I scroll down quickly, it shakes violently.

Screenshots

KakaoTalk_Video_2023-05-03-16-24-13.mp4

Your Environment

  • Device: MacBook Pro
  • OS: mac OS
  • Browser: Chrome
  • Version: 112.0.5615.137

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.

🤔 Who uses React Cool Virtual?

👋🏻 Hi guys, this is Welly from Taiwan. I spend a lot of time in this library. I'd love to know which companies are using this library for their product(s) so I can mention you in the README.

Please share the following information with me:

  • Company name (might be used in the README or website etc.)
  • Company logo (might be used in the README or website etc.)
  • What kind of product(s)?
  • The URL of your product(s)

Can't scroll at the end of a million data.

Bug Report

Describe the Bug

Can't scroll at the end of a million data.

How to Reproduce

The position of the scroll bar cannot scroll after the position of 600,000 data

CodeSandbox Link

Show me the bug on CodeSandbox.

Expected Behavior

Can use mouse to scroll

Screenshots

image

Your Environment

  • OS: macOS
  • Browser: Chrome 103

Typescript error TS2322 - for outerRef and innerRef

Bug Report

Describe the Bug

Im getting this error

TS2322: Type 'MutableRefObject<HTMLElement | null>' is not assignable to type 'LegacyRef<HTMLDivElement> | undefined'.   Type 'MutableRefObject<HTMLElement | null>' is not assignable to type 'RefObject<HTMLDivElement>'.     Types of property 'current' are incompatible.       Type 'HTMLElement | null' is not assignable to type 'HTMLDivElement | null'.         Property 'align' is missing in type 'HTMLElement' but required in type 'HTMLDivElement'.  lib.dom.d.ts(6327, 5): 'align' is declared here. index.d.ts(137, 9): The expected type comes from property 'ref' which is declared here on type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'

Example of use

 const { outerRef, innerRef, items, scrollToItem } = useVirtual<HTMLElement, HTMLElement>({
    itemCount: 1000,
    itemSize: 25,
  });

  return (
    <div ref={outerRef} >
      <div ref={innerRef}>
            ...       
      </div>
   </div>
  );

A clear and concise description of what the bug is.

How to Reproduce

  • Create a react typescript project
  • set both outerRef, innerRef to ref like in the example

Expected Behavior

Not to see the error

Screenshots

Add screenshots to help explain your problem.
Screen Shot 2023-01-31 at 14 11 37

Are there any examples with <table> tag?

I think this is one of the most agile virtual scrolling libraries that I've found so far. I'm trying to decide which lib I'm going to use and really wanted to use this one in my project. Is there any working examples with our dear

approach to create a table?

All the examples here (and inside other libraries's repos) are always focused on

, but most of the time the libraries won't work as expected.

Glad if someone could provide a basic example, as I didn't get it to work here :/

Removing items from a list.

The readme mentions filtering but the example doesnt really make sense. It mostly just looks like you can change how many things you wish to render.

Initial render at end of list

Option to start first render at end of the list.

Describe the Feature

I'm looking at using react-cool-virtual to render a chat window. Loading my items, and then scrolling to the end of the list, is proving to be pretty messy. Among other things, you get some render flashes, sometimes jumpiness, etc.

Describe the Solution You'd Like

It would be nice to have the option to start the chat at the end, and skip the initial render that happens at the top of the list.

Describe Alternatives You've Considered

I've spend a lot of time trying to optimize/improve the scroll-to-end on load, but it's proving rather difficult to improve matters with that path.

Additional Information

Another option I've considered is a flex column with flex-direction: column-reverse, in which case you could, in theory, just load your items in reverse order, but I don't think the way react-cool-virtual works would play nicely with this idea.

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

Add easier way to scroll to the bottom of the list

Feature Request

Describe the Feature

I'd like a way to scroll to the bottom of the list.

Describe the Solution You'd Like

Either a scrollToBottom callback, or have the scrollTo callback take an option to go to the bottom.

const { scrollToBottom } = useVirtual({});
scrollToBottom();
scrollToBottom({ smooth: true });
const { scrollTo } = useVirtual({});
scrollTo("bottom");
scrollTo({ offset: "bottom", smooth: true });

Describe Alternatives You've Considered

I currently implement this with scrollToItem({ index: itemCount - 1, align: "end" });, though it required me rearranging some code. This is also what's used in the "stick to bottom" example.

Additional Information

Thanks!

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-top exceeds 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"
}

ResizeObserver loop completed with undelivered notifications

Bug Report

Describe the Bug

The error ResizeObserver loop completed with undelivered notifications always appears when the new item is being rendered into the list.

How to Reproduce

I don't know how to reproduce it, but this is the same problem as described here wellyshen/react-cool-dimensions#373.

Expected Behavior

There are no such errors during rendering of the list.

Your Environment

  • Device: MacBook Pro M2
  • OS: macOS
  • Browser: Chrome
  • Version: 120.0.6062.2, dev (arm64)

OuterRef and InnerRef can`t assign to my elements

Bug Report

I use this library in my react project, so when i create a data list like in examples from documentation i get an error.

изображение

These components are just styled div elements. And i also tried it with casual div-tag.

I dont know what happens and cant find solution in the internet.

That`s an exception:

изображение

изображение

sticking to bottom invalid without "smooth"

Bug Report

Describe the Bug

In my project, I accept data and render lists through websocket. In some cases, I don't want to use smooth, so I found this problem.
The problem occurs in concurrent network requests(maybe, i guess😂). When smooth is set to false, it cannot stick to bottom.
But when smooth is set to true, the bug disappears

How to Reproduce

👉CodeSandbox.
Although the code is not well written, it can be reproduced. I modified lines 43 and 53. You can see the effect

CodeSandbox Link

Show me the bug on CodeSandbox.

Expected Behavior

sticking to bottom all the time

dynamic stickyIndices

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

#231

Rendering data on test environment

Hi @wellyshen

I found an old issue about testing with react-cool-virtual where you mentioned that a polyfill to ResizeObserver is needed and I've done that, so this problem is not related to the ResizeObserver.

I created a lib that uses react-cool-virtual to render some data and when I use this lib in another project, the data isn't loaded when running unit tests - the loadMore function simply isn't called when the component is rendered on the test environment; I've been trying to debug this for a while but can't find the problem or if I'm doing something wrong.

Note that when running the application on the browser, everything works as expected.

EDITED ON 04-19-2023: well, it turns out that loading the polyfill for ResizeObserver to the test environment didn't work and you have to explicitly mock it; it would be nice to have this documented, so anyone with the same problem didn't have to look through the source code for an example.

`requestAnimationFrame` doesn't really help with scroll jumping

requestAnimationFrame practice in "Pre-pending Items" demo doesn't help a lot with scroll jumping. Yes, maybe it helps "a bit", but the lag is pretty annoying even in provided demo.

I checked how it works in real app (much more heavy than your demo) and observed lag is pretty much noticeable.
Any other ways how to prevent this lag?

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?

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.

If you scroll quickly to the end of the list of items in the Infinite Scroll, it does not load the data

Bug Report

Describe the Bug

If you scroll quickly to the end of the list of items in the Infinite Scroll, it does not load the data, you have to scroll a bit for it to load correctly.

How to Reproduce

The Infinite Scroll example shows the bug.

  1. Go to https://codesandbox.io/s/rcv-infinite-scroll-forked-wuov0v
  2. Once the demo loads, scroll directly to the end of the first list (Skeleton) and the "Loading..." messages will remain and the data will not be loaded.

CodeSandbox Link

Show me the bug on CodeSandbox.

Expected Behavior

The data should be loaded.

Screenshots

image

https://i.gyazo.com/742f0acfb10847bb684c1d9a5288ff38.mp4

Your Environment

  • Device: PC
  • OS: Windows 10
  • Browser: Chome
  • Version: 106.0.5249.119

measureRef prevents component list item component memoization

Bug Report

Describe the Bug

I'm testing the library and with dynamic sizing I cannot get a memoized component to work. What I can see is that measureRef is always a new reference which prevents us from using memo() to its effect.

Maybe moving the measureRef function to the top component level and wrapping it in useCallback can help, but I'm having trouble making a working PR.

How to Reproduce

  1. Start from the Dynamic Size example.
  2. add some logging to keep track of renders
  3. add memoization to the accordion component

You can see that adding memoization does not change how many times a component is rendered. It is rerendered whenever something new is added/removed from the list.

It seems to be the measureRef reference as when I don't provide the measureRef function to the component it seems to work fine.

CodeSandbox Link

Show me the bug on CodeSandbox.

Expected Behavior

Only the newly inserted elements should rerender.

Screenshots

image

Your Environment

  • Device: [MacBook Pro]
  • OS: [macOS]
  • Browser: [Chrome]
  • Version: [latest]

Option for (real) fast scrolling (aka immediate jump)

Feature Request

Describe the Feature

Is it possible to achieve more or less immediate scroll/jump to item with index?
It would be ideal to scroll fast from top to bottom (or anywhere in-between) and back.
Note: Scrolling from bottom to top already behaves like expected.

Describe the Solution You'd Like

It would be nice, if scrollToItem() would resemble Element.scrollIntoView(). Specifically with { behavior: 'auto' }. Supporting block and inline options would be perfect, but (very) optional.

Describe Alternatives You've Considered

N/A. Currently I'm relying on Element.scrollIntoView() for non-virtualised lists.

Additional Information

The following CodeSandbox demonstrates an issue with scrolling from top to the bottom of the list: https://codesandbox.io/s/white-glade-xnu3u

  1. Focus list element
  2. Press End key
  3. Wait
  4. Wait some more, ..., wait for it, ..., yes, yes, nearly done...
  5. Got it, finally!

Feature Request: support window scrolling + scroll restoration.

Feature Request

This lib looks totally promising. I want to use this library on my website.
But it missed the 2 most important features: window scrolling + scroll restoration when navigating back.
I tried https://virtuoso.dev/ but it doesn't work with scroll restoration.

Other libraries are hard to use and buggy. I spent 2-3 days learn react-window + virtuoso, and disappointed 😥
Also, I hope you can test with next.js - the most popular react framework.

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.