Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

react-viewport-list

Package Overview
Dependencies
Maintainers
1
Versions
60
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-viewport-list

📜 Virtualization for lists with dynamic item size

  • 7.1.2
  • latest
  • Source
  • npm
  • Socket score

Version published
Maintainers
1
Created
Source

React ViewPort List

NPM version typescript NPM license NPM total downloads NPM monthly downloads

If your application renders long lists of data (hundreds or thousands of rows), we recommended using a technique known as “windowing”. This technique only renders a small subset of your rows at any given time, and can dramatically reduce the time it takes to re-render the components as well as the number of DOM nodes created.

- React.js documentation

📜 Virtualization for lists with dynamic item size

Features 🔥

  • Simple API like Array.Prototype.map()
  • Created for dynamic item height or width (if you don't know item size)
  • Works perfectly with Flexbox (unlike other libraries with position: absolute)
  • Supports scroll to index
  • Supports initial index
  • Supports vertical ↕ and horizontal ↔ lists️️
  • Tiny (about 2kb minified+gzipped)

Try 100k list demo

Getting Started

  • Installation:

    npm install --save react-viewport-list
    
  • Basic Usage:

    import { useRef } from 'react';
    import { ViewportList } from 'react-viewport-list';
    
    const ItemList = ({
      items,
    }: {
      items: { id: string; title: string }[];
    }) => {
      const ref = useRef<HTMLDivElement | null>(
        null,
      );
    
      return (
        <div className="scroll-container" ref={ref}>
          <ViewportList
            viewportRef={ref}
            items={items}
          >
            {(item) => (
              <div key={item.id} className="item">
                {item.title}
              </div>
            )}
          </ViewportList>
        </div>
      );
    };
    
    export { ItemList };
    

MutableRefObject<HTMLElement / null> / RefObject<HTMLElement / null> / { current: HTMLElement / null } / null

Props

nametypedefaultdescription
viewportRefMutableRefObject<HTMLElement / null> / RefObject<HTMLElement / null> / { current: HTMLElement / null } / nullrequiredViewport and scroll container.
document.documentElement will be used if viewportRef not provided.
itemsT[][]Array of items.
itemSizenumber0Item average (estimated) size (height for axis="y" and width for axis="x") in px.
Size should be greater or equal zero.
Size will be computed automatically if itemMinSize not provided or equal zero.
itemMarginnumber-1Item margin (margin-bottom for axis="y" and margin-right for axis="x") in px.
Margin should be greater or equal -1.
Margin will be computed automatically if margin not provided or equal -1.
You should still set margin in item styles
overscannumber1Count of "overscan" items.
axis"y" / "x"'y'Scroll axis:
  • "y" - vertical
  • "x" - horizontal
initialIndexnumber-1Initial item index in viewport.
initialAlignToTopbooleantruescrollIntoView param.
Used with initialIndex
initialOffsetnumber0Offset after scrollIntoView call.
Used with initialIndex.
This value will be added to the scroll after scroll to index.
initialDelaynumber-1setTimeout delay for initial scrollToIndex.
Used with initialIndex.
initialPrerendernumber0Used with initialIndex.
This value will modify initial start index and initial end index like [initialIndex - initialPrerender, initialIndex + initialPrerender].
You can use it to avoid blank screen with only one initial item rendered
children(item: T, index: number, array: T[]) => ReactNoderequiredItem render function.
Similar to Array.Prototype.map().
onViewportIndexesChange(viewportIndexes: [number, number]) => voidoptionalWill be called on rendered in viewport indexes change.
overflowAnchor"none" / "auto""auto"Compatibility for overflow-anchor: none.
Set it to "none" if you use overflow-anchor: none in your parent container styles.
withCachebooleantrueCache rendered item heights.
scrollThresholdnumber0If scroll diff more than scrollThreshold setting indexes was skipped. It's can be useful for better fast scroll UX.
renderSpacer(props: { ref: MutableRefObject; style: CSSProperties; type: 'top' / 'bottom' }) => ReactNode({ ref, style }) => <div ref={ref} style={style} />In some rare cases you can use specific elements/styles instead of default spacers
countnumberoptionalYou can use items count instead of items directly. Use should use different children: (index: number) => ReactNode
indexesShift number0Every time you unshift (prepend items) you should increase indexesShift by prepended items count. If you shift items (remove items from top of the list you should decrease indexesShift by removed items count).
getItemBoundingClientRect (element: Element) => DOMRect / { bottom: number; left: number; right: number; top: number; width: number; height: number; }(element) => element.getBoundingClientRect()You can use custom rect getter to support display: contents or other cases when element.getBoundingClientRect() returns "bad" data

Methods

scrollToIndex

scrollToIndex method has only one param - options;

Options param

nametypedefaultdescription
indexnumber-1Item index for scroll.
alignToTopbooleantruescrollIntoView param. Only boolean option supported.
offsetnumber0Offset after scrollIntoView call.
This value will be added to the scroll after scroll to index.
delaynumber-1setTimeout delay for initial scrollToIndex.
prerendernumber0This value will modify initial start index and initial end index like [index - initialPrerender, index + initialPrerender].
You can use it to avoid blank screen with only one initial item rendered

Usage

import { useRef } from 'react';
import { ViewportList } from 'react-viewport-list';

const ItemList = ({
  items,
}: {
  items: { id: string; title: string }[];
}) => {
  const ref = useRef(null);
  const listRef = useRef(null);

  return (
    <div className="scroll-container" ref={ref}>
      <ViewportList
        ref={listRef}
        viewportRef={ref}
        items={items}
      >
        {(item) => (
          <div key={item.id} className="item">
            {item.title}
          </div>
        )}
      </ViewportList>
      <button
        className="up-button"
        onClick={() =>
          listRef.current.scrollToIndex({
            index: 0,
          })
        }
      />
    </div>
  );
};

export { ItemList };

getScrollPosition

getScrollPosition returns an object with scroll position: { index: number, offset: number }

Returns

nametypedescription
indexnumberItem index for scroll.
offsetnumberOffset after scrollIntoView call.
This value will be added to the scroll after scroll to index.

If items=[] or count=0 getScrollPosition returns { index: -1; offset: 0 }

Usage

import { useEffect, useRef } from 'react';
import { ViewportList } from 'react-viewport-list';

const ItemList = ({
  items,
}: {
  items: { id: string; title: string }[];
}) => {
  const ref = useRef(null);
  const listRef = useRef(null);

  useEffect(
    () => () => {
      window.sessionStorage.setItem(
        'lastScrollPosition',
        JSON.stringify(
          listRef.current.getScrollPosition(),
        ),
      );
    },
    [],
  );

  return (
    <div className="scroll-container" ref={ref}>
      <ViewportList
        ref={listRef}
        viewportRef={ref}
        items={items}
      >
        {(item) => (
          <div key={item.id} className="item">
            {item.title}
          </div>
        )}
      </ViewportList>
      <button className="up-button" />
    </div>
  );
};

export { ItemList };

Performance

If you have performance issues, you can add will-change: transform to a scroll container.

You should remember that in some situations will-change: transform can cause performance issues instead of fixing them.

.scroll-container {
  will-change: transform;
}

Children pseudo-classes

ViewportList render two elements (spacers) before first rendered item and after last rendered item. That's why children pseudo-classes like :nth-child(), :last-child, :first-child may work incorrectly.

Margin

If you want more accurate virtualizing you should use equal margin for all items. Also, you should use margin-top or margin-bottom (not both) for axis="y" and margin-right or margin-left (not both) for axis="x".

If you want to use different margins and stil want more accurate virtualizing you can wrap your items in some element like <div> and use padding instead of margin.

Non-keyed

You should avoid non-keyed usage of list. You should provide unique key prop for each list items. If you have issues with scroll in Safari and other browsers without overflow-anchor support, check item's key prop.

Advanced Usage

  • Grouping

    ViewportList render Fragment with items in viewport. So, grouping just work.

    import { useRef } from 'react';
    import { ViewportList } from 'react-viewport-list';
    
    const GroupedItemList = ({
      keyItems,
      items,
    }: {
      keyItems: { id: string; title: string }[];
      items: { id: string; title: string }[];
    }) => {
      const ref = useRef(null);
    
      return (
        <div className="scroll-container" ref={ref}>
          <span className="group-title">
            Key Items
          </span>
          <ViewportList
            viewportRef={ref}
            items={keyItems}
          >
            {(item) => (
              <div
                key={item.id}
                className="key-item"
              >
                {item.title}
              </div>
            )}
          </ViewportList>
          <span className="group-title">Items</span>
          <ViewportList
            viewportRef={ref}
            items={items}
          >
            {(item) => (
              <div key={item.id} className="item">
                {item.title}
              </div>
            )}
          </ViewportList>
        </div>
      );
    };
    export { GroupedItemList };
    
  • Sorting

    You can use React Sortable HOC

    import { useRef } from 'react';
    import {
      SortableContainer,
      SortableElement,
    } from 'react-sortable-hoc';
    import { ViewportList } from 'react-viewport-list';
    
    const SortableList = SortableContainer(
      ({ innerRef, ...rest }) => (
        <div {...rest} ref={innerRef} />
      ),
    );
    
    const SortableItem = SortableElement(
      (props) => <div {...props} />,
    );
    
    const SortableItemList = ({
      items,
      onSortEnd,
    }) => {
      const ref = useRef(null);
    
      return (
        <SortableList
          innerRef={ref}
          className="scroll-container"
          onSortEnd={onSortEnd}
        >
          <ViewportList
            viewportRef={ref}
            items={items}
          >
            {(item, index) => (
              <SortableItem
                key={index}
                index={index}
                className="item"
              >
                {item.title}
              </SortableItem>
            )}
          </ViewportList>
        </SortableList>
      );
    };
    
    export { SortableItemList };
    
  • Scroll to position

    Scroll to position may work incorrectly because scrollHeight and scrollTop (or scrollWidth and scrollLeft) changed automatically while scrolling. But you can scroll to position with scrollToIndex method with { index: 0, offset: scrollPosition }. For initial scroll to position you can use initialIndex={0} and initialOffset={scrollPosition}. You should remember that after scroll happened scroll position can be not equal to specified offset.

    import { useRef } from 'react';
    import { ViewportList } from 'react-viewport-list';
    
    const ItemList = ({
      items,
      savedScroll,
    }: {
      items: { id: string; title: string }[];
      savedScroll: number;
    }) => {
      const ref = useRef(null);
      const listRef = useRef(null);
    
      return (
        <div className="scroll-container" ref={ref}>
          <ViewportList
            ref={listRef}
            viewportRef={ref}
            items={items}
            initialIndex={0}
            initialOffset={savedScroll}
          >
            {(item) => (
              <div key={item.id} className="item">
                {item.title}
              </div>
            )}
          </ViewportList>
          <button
            className="up-button"
            onClick={() => {
              // this sets scrollTop of "scroll-container" to 1000
              listRef.current.scrollToIndex({
                index: 0,
                offset: 1000,
              });
            }}
          />
        </div>
      );
    };
    
    export { ItemList };
    
  • Tests

    You can mock ViewportList for unit tests:

    import {
      useImperativeHandle,
      forwardRef,
    } from 'react';
    
    export const ViewportListMock = forwardRef(
      ({ items = [], children }, ref) => {
        useImperativeHandle(
          ref,
          () => ({
            scrollToIndex: () => {},
          }),
          [],
        );
    
        return (
          <>
            <div />
            {items.map(children)}
            <div />
          </>
        );
      },
    );
    
    export default ViewportListMock;
    

Keywords

FAQs

Package last updated on 07 Dec 2023

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc