📜 Virtualization for lists with dynamic item size
npm install react-viewport-list
!typescript
!NPM license


> 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.
- 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
- ### Installation:
``shell script`
npm install --save react-viewport-list
- ### Basic Usage:
`typescript jsx
import { useRef } from 'react';
import { ViewportList } from 'react-viewport-list';
const ItemList = ({
items,
}: {
items: { id: string; title: string }[];
}) => {
const ref = useRef
null,
);
return (
export { ItemList };
`
MutableRefObject\
| name | type | default | description | scrollToIndex method has only one param - options; Options param | name | type | default | description | Usage const ItemList = ({ return ( export { ItemList }; getScrollPosition returns an object with scroll position: Returns | name | type | description | If Usage const ItemList = ({ useEffect( return ( export { ItemList }; If you have performance issues, you can add You should remember that in some situations If you want more accurate virtualizing you should use equal margin for all items. If you want to use different margins and stil want more accurate virtualizing you can wrap your items in some element like You should avoid non-keyed usage of list. You should provide unique key prop for each list items. - ### Grouping const GroupedItemList = ({ return ( - ### Sorting You can use React Sortable HOC const SortableList = SortableContainer( const SortableItem = SortableElement( const SortableItemList = ({ return ( export { SortableItemList }; - ### Scroll to position Scroll to position may work incorrectly because scrollHeight and scrollTop (or scrollWidth and scrollLeft) changed automatically while scrolling. const ItemList = ({ return ( export { ItemList }; - ### Tests You can mock ViewportList for unit tests: export const ViewportListMock = forwardRef( return ( export default ViewportListMock;
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| viewportRef | MutableRefObject\document.documentElement will be used if viewportRef not provided. |items
| | T[] | [] | Array of items. |itemSize
| | number | 0 | Item average (estimated) size (height for axis="y" and width for axis="x") in px.itemMinSize
Size should be greater or equal zero.
Size will be computed automatically if not provided or equal zero. |itemMargin
| | number | -1 | Item margin (margin-bottom for axis="y" and margin-right for axis="x") in px.margin
Margin should be greater or equal -1.
Margin will be computed automatically if not provided or equal -1.overscan
You should still set margin in item styles |
| | number | 1 | Count of "overscan" items. |axis
| | "y" / "x" | 'y' | Scroll axis:initialIndex
|
| | number | -1 | Initial item index in viewport. |initialAlignToTop
| | boolean | true | scrollIntoView param.initialIndex
Used with |initialOffset
| | number | 0 | Offset after scrollIntoView call.initialIndex
Used with .initialDelay
This value will be added to the scroll after scroll to index. |
| | number | -1 | setTimeout delay for initial scrollToIndex.initialIndex
Used with . |initialPrerender
| | number | 0 | Used with initialIndex.[initialIndex - initialPrerender, initialIndex + initialPrerender]
This value will modify initial start index and initial end index like .children
You can use it to avoid blank screen with only one initial item rendered |
| | (item: T, index: number, array: T[]) => ReactNode | required | Item render function.Array.Prototype.map()
Similar to . |onViewportIndexesChange
| | (viewportIndexes: [number, number]) => void | optional | Will be called on rendered in viewport indexes change. |overflowAnchor
| | "none" / "auto" | "auto" | Compatibility for overflow-anchor: none.overflow-anchor: none
Set it to "none" if you use in your parent container styles. |withCache
| | boolean | true | Cache rendered item heights. |scrollThreshold
| | number | 0 | If scroll diff more than scrollThreshold setting indexes was skipped. It's can be useful for better fast scroll UX. |renderSpacer
| | (props: { ref: MutableRefObject | In some rare cases you can use specific elements/styles instead of default spacers |
| count | number | optional | You can use items count instead of items directly. Use should use different children: (index: number) => ReactNode |indexesShift
| | number | 0 | Every 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 |indexMethods
$3
| ------------ | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| | number | -1 | Item index for scroll. |alignToTop
| | boolean | true | scrollIntoView param. Only boolean option supported. |offset
| | number | 0 | Offset after scrollIntoView call.delay
This value will be added to the scroll after scroll to index. |
| | number | -1 | setTimeout delay for initial scrollToIndex. |prerender
| | number | 0 | This 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 |typescript jsx`
import { useRef } from 'react';
import { ViewportList } from 'react-viewport-list';
items,
}: {
items: { id: string; title: string }[];
}) => {
const ref = useRef(null);
const listRef = useRef(null);
viewportRef={ref}
items={items}
>
{(item) => (
{item.title}
)}
className="up-button"
onClick={() =>
listRef.current.scrollToIndex({
index: 0,
})
}
/>
);
};{ index: number, offset: number }$3
index
| -------- | ------ | ----------------------------------------------------------------------------------------------------- |
| | number | Item index for scroll. |offset
| | number | Offset after scrollIntoView call.items=[]
This value will be added to the scroll after scroll to index. | or count=0 getScrollPosition returns { index: -1; offset: 0 }`typescript jsx`
import { useEffect, useRef } from 'react';
import { ViewportList } from 'react-viewport-list';
items,
}: {
items: { id: string; title: string }[];
}) => {
const ref = useRef(null);
const listRef = useRef(null);
() => () => {
window.sessionStorage.setItem(
'lastScrollPosition',
JSON.stringify(
listRef.current.getScrollPosition(),
),
);
},
[],
);
viewportRef={ref}
items={items}
>
{(item) => (
{item.title}
)}
);
};will-change: transformPerformance
to a scroll container.will-change: transform can cause performance issues instead of fixing them.`css`
.scroll-container {
will-change: transform;
}ViewportListChildren pseudo-classes
render two elements (spacers) before first rendered item and after last rendered item.:nth-child()
That's why children pseudo-classes like , :last-child, :first-child may work incorrectly.margin-topMargin
Also, you should use or margin-bottom (not both) for axis="y" and margin-right or margin-left (not both) for axis="x". and use padding instead of margin.overflow-anchorNon-keyed
If you have issues with scroll in Safari and other browsers without support, check item's key prop.ViewportListAdvanced Usage
render Fragment with items in viewport. So, grouping just work.`typescript jsx
import { useRef } from 'react';
import { ViewportList } from 'react-viewport-list';
keyItems,
items,
}: {
keyItems: { id: string; title: string }[];
items: { id: string; title: string }[];
}) => {
const ref = useRef(null);
Key Items
items={keyItems}
>
{(item) => (
key={item.id}
className="key-item"
>
{item.title}
)}
Items
items={items}
>
{(item) => (
{item.title}
)}
);
};
export { GroupedItemList };
``javascript`
import { useRef } from 'react';
import {
SortableContainer,
SortableElement,
} from 'react-sortable-hoc';
import { ViewportList } from 'react-viewport-list';
({ innerRef, ...rest }) => (
),
);
(props) =>
);
items,
onSortEnd,
}) => {
const ref = useRef(null);
className="scroll-container"
onSortEnd={onSortEnd}
>
items={items}
>
{(item, index) => (
index={index}
className="item"
>
{item.title}
)}
);
};
scrollToIndex
But you can scroll to position with 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.`typescript jsx`
import { useRef } from 'react';
import { ViewportList } from 'react-viewport-list';
items,
savedScroll,
}: {
items: { id: string; title: string }[];
savedScroll: number;
}) => {
const ref = useRef(null);
const listRef = useRef(null);
viewportRef={ref}
items={items}
initialIndex={0}
initialOffset={savedScroll}
>
{(item) => (
{item.title}
)}
className="up-button"
onClick={() => {
// this sets scrollTop of "scroll-container" to 1000
listRef.current.scrollToIndex({
index: 0,
offset: 1000,
});
}}
/>
);
};
`javascript``
import {
useImperativeHandle,
forwardRef,
} from 'react';
({ items = [], children }, ref) => {
useImperativeHandle(
ref,
() => ({
scrollToIndex: () => {},
}),
[],
);
<>
{items.map(children)}
>
);
},
);