Similar to react-virtualized, but:
- No deps
- No fighting with containers
- No magical divs will wrap your list with position: absolute and height: 0
- No scroll syncing problems
- No AutoWindow over AutoSize with VerticalSpecialList
- Dead simple infinity loader
- Full rendering controll
- It just works
- ~0.5kb gzipped
Currently only fixed item sizes supported, but will add dynamic sizing later.
Components & hooks in this library will automatically find all containers with overflows and render only visible items.
So you could stack and wrap your list in anyway you want, everything will work.
You also could use some parts of this library for example to calculate only visible on screen rect of element.
This library track only scroll/resize/orientationchange events (on capture phase), so if you resize some wrapper with overflow through styles, you should use hooks and call recalculation manually.
npm i react-virtual-overflow
import { VirtualListY } from "react-virtual-overflow/lib/fixed-list-y";
function MyApp() {
const items = Array.from({ length: 300 }).map((_, i) => `item ${i}`);
const itemHeight = 40;
const renderItem = (item) => (
<div style={{ height: '40px' }}>{item}</div>
);
return (
<div style={{ overflowY: 'scroll', height: '300px', background: 'lightgreen' }}>
{/* !this component will not add container with overflow! */}
{/* the only overflow here is element above */}
<VirtualListY
items={items}
itemHeight={itemHeight}
itemKey={x => x}
renderItem={renderItem}
/>
</div>
);
}
Advanced example with hook
import { useVirtualOverflowY } from "react-virtual-overflow";
function MyApp() {
const items = Array.from({ length: 300 }).map((_, i) => `item ${i}`);
const containerRef = useRef<HTMLDivElement>(undefined!);
const itemHeight = 40;
const { renderedItems } = useVirtualOverflowY({
containerRef,
itemsLengthY: items.length,
itemHeight,
renderItem: (itemIndex, offsetTop) => {
const item = items[itemIndex];
return (
<div style={{ position: "absolute", top: `${offsetTop}px` }} key={item}>
{item}
</div>
)
},
}, []);
return (
<div style={{ overflowY: "scroll", height: "300px" }}>
<div ref={containerRef} style={{ height: `${itemHeight * items.length}px` }}>
{renderedItems}
</div>
</div>
);
}
Vertical list component
This component is used to render vertical list
import { VirtualListY } from "react-virtual-overflow/lib/fixed-list-y";
type VirtualListYProps<ItemT> = {
items: ItemT[],
itemHeight: number,
// used to calculate react key when rendering
itemKey: (item: ItemT, itemIndex: number) => string,
overscanItemsCount?: number,
renderItem: (item: ItemT, itemIndex: number, contentTopOffset: number) => React.ReactNode,
calcVisibleRect?: VirtualOverflowCalcVisibleRectFn
};
function MyApp() {
const items = Array.from({ length: 300 }).map((_, i) => `item ${i}`);
const itemHeight = 40;
const renderItem = (item) => (
<div style={{ height: '40px' }}>{item}</div>
);
return (
<div style={{ overflowY: 'scroll', height: '300px', background: 'lightgreen' }}>
<VirtualListY
items={items}
itemHeight={itemHeight}
itemKey={x => x}
renderItem={renderItem}
/>
</div>
);
}
Horizontal list component
This component is used to render horizontal list
import { VirtualListX } from "react-virtual-overflow/lib/fixed-list-x";
type VirtualListXProps<ItemT> = {
items: ItemT[],
itemWidth: number,
itemKey: (item: ItemT, itemIndex: number) => string,
overscanItemsCount?: number,
renderItem: (item: ItemT, itemIndex: number, contentTopOffset: number) => React.ReactNode,
calcVisibleRect?: VirtualOverflowCalcVisibleRectFn
};
function MyApp() {
const items = Array.from({ length: 300 }).map((_, i) => `item ${i}`);
const itemWidth = 40;
const renderItem = (item) => (
<div style={{ width: '40px' }}>{item}</div>
);
return (
<div style={{ overflowX: 'scroll', height: '300px', background: 'lightgreen' }}>
<VirtualListX
items={items}
itemWidth={itemWidth}
itemKey={x => x}
renderItem={renderItem}
/>
</div>
);
}
Grid component
This component is used to render grid
import { VirtualGrid } from "react-virtual-overflow/lib/fixed-grid";
type VirtualGridProps<ItemT> = {
// rows
items: ItemT[][],
columnsNum: number,
itemWidth: number,
itemHeight: number,
itemKey: (item: ItemT, itemIndexX: number, itemIndexY: number) => string,
overscanItemsCount?: number,
renderItem: (item: ItemT, itemIndexX: number, leftOffsetPx: number, itemIndexY: number, topOffsetPx: number) => React.ReactNode,
calcVisibleRect?: VirtualOverflowCalcVisibleRectFn
};
function GridExample() {
const items = itemsGrid;
return (
<div style={{ overflowY: 'scroll', height: '300px', background: 'lightgreen' }}>
<VirtualGrid
items={items}
columnsNum={300}
itemWidth={40}
itemHeight={80}
itemKey={x => x}
overscanItemsCount={3}
renderItem={item => <div style={{ width: '40px', height: '80px' }}>{item}</div>}
/>
</div>
);
}
Vertical list hook
useVirtualOverflowY
hook that computes and renders vertical list
It accepts this params:
type UseVirtualOverflowParamsY = {
// reference to container with elements (not scroll)
containerRef: React.MutableRefObject<HTMLElement>;
// total num of items
itemsLengthY: number;
// how to render each item
renderItem: (itemIndex: number, contentTopOffsetPx: number) => React.ReactNode;
// height of one item in pixels
itemHeight: number;
// how much items should be rendered beyond visible border
// default=3
overscanItemsCount?: number;
// function to calculate visible rect (check utils for other options)
calcVisibleRect?: CalcVisibleRectFn;
};
And returns:
{
renderedItems: React.Node[],
// method that will force update calculations
updateViewRect: () => void,
itemSlice: {
topStartIndex: number;
lengthY: number;
leftStartIndex: number;
lengthX: number;
}
}
Horizontal list hook
useVirtualOverflowX
hook that computes and renders horizontal list
It accepts this params:
type UseVirtualOverflowParamsX = {
// reference to container with elements (not scroll)
containerRef: React.MutableRefObject<HTMLElement>;
// total num of items
itemsLengthX: number;
// how to render each item
renderItem: (itemIndex: number, contentLeftOffsetPx: number) => React.ReactNode;
// width of one item in pixels
itemWidth: number;
// how much items should be rendered beyond visible border
// default=3
overscanItemsCount?: number;
// function to calculate visible rect (check utils for other options)
calcVisibleRect?: CalcVisibleRectFn;
};
And returns:
{
renderedItems: React.Node[],
// method that will force update calculations
updateViewRect: () => void,
itemSlice: {
topStartIndex: number;
lengthY: number;
leftStartIndex: number;
lengthX: number;
}
}
Grid hook
useVirtualOverflowGrid
hook that computes and renders grid
It accepts this params:
type UseVirtualOverflowParamsGrid = {
// reference to container with elements (not scroll)
containerRef: React.MutableRefObject<HTMLElement>;
// total num of items horizontal
itemsLengthX: number;
// total num of items vertical
itemsLengthY: number;
// how to render each item
renderItem: (itemIndexX: number, leftOffsetPx: number, itemIndexY: number, topOffsetPx: number) => React.ReactNode;
// width of one item in pixels
itemWidth: number;
// height of one item in pixels
itemHeight: number;
// how much items should be rendered beyond visible border
// default=3
overscanItemsCount?: number;
// function to calculate visible rect (check utils for other options)
calcVisibleRect?: CalcVisibleRectFn;
};
And returns:
{
renderedItems: React.Node[],
// method that will force update calculations
updateViewRect: () => void,
itemSlice: {
topStartIndex: number;
lengthY: number;
leftStartIndex: number;
lengthX: number;
}
}
useCalcVirtualOverflow - universal hook for fixed list/grid
useCalcVirtualOverflow
hook that computes visible rect at calculates slice of items that should be rendered
It could be used if you want to render items manually, and you need only slice calculated
It accepts this params:
type UseVirtualOverflowParams = {
containerRef: React.MutableRefObject<HTMLElement>,
itemsLengthX?: number,
itemsLengthY?: number,
/** if undefined, then horizontal calculation will be skipped */
itemWidth?: number,
/** if undefined, then vertical calculation will be skipped */
itemHeight?: number,
/** default=3 */
overscanItemsCount?: number,
calcVisibleRect?: VirtualOverflowCalcVisibleRectFn,
};
And returns:
{
itemSlice: {
topStartIndex: number;
lengthY: number;
leftStartIndex: number;
lengthX: number;
};
updateViewRect: () => void;
}
Calculate visible on screen rect
virtualOverflowCalcVisibleRect
method will calculate on screen visible rect of some element
It accepts this params:
function virtualOverflowCalcVisibleRect(element: HTMLElement): {
top: number;
left: number;
bottom: number;
right: number;
contentOffsetTop: number;
contentOffsetLeft: number;
contentVisibleHeight: number;
contentVisibleWidth: number;
};
Slice calculation from visible rect
virtualOverflowCalcItems
method will calculate slice of items from visible rect
You can pass here horizontal and vertical values from "calcVisibleRect" method.
This method is axis-agnostic, so you just first calculate vertical data by passing vertical coords of rect, and then (if you need) horizontal.
function virtualOverflowCalcItems(
contentOffsetStartPx: number,
contentVisibleSizePx: number,
itemSize: number,
overscanItemsCount: number,
itemsLength: number
);
// returns
{
// index of starting item that should be rendered (including overscan)
itemStart: number,
// total count of items (including start & end overscan)
itemLen: number
};
// Example for vertical slice calculation:
const visibleRect = calcVisibleRect(containerRef.current);
const verticalSlice = virtualOverflowCalcItems(
visibleRect.contentOffsetTop,
visibleRect.contentVisibleHeight,
itemHeight,
overscanItemsCount,
itemsLengthY
);
Infinity loader
All hooks (useCalcVirtualOverflow
, useVirtualOverflowY
, useVirtualOverflowX
, useVirtualOverflowGrid
) returns itemSlice
which you can use to trigger infinity loading.
For example:
const [items, setItems] = useState([] as any[]);
// here we get current rendered itemSlice
const { renderedItems, itemSlice } = useVirtualOverflowY({
itemsLengthY: items.length,
// ...
});
// here we check if we render bottom range of items
useEffect(() => {
if (itemSlice.topStartIndex + itemSlice.lengthY >= items.length - 4) {
// load more
setItems((prev) => [...prev, ...newItems]);
}
}, [itemSlice.topStartIndex, itemSlice.lengthY]);
All methods here are inside virtualOverflowUtils
namespace in react-virtual-overflow/utils
. I will not write namespace here below for readability purposes.
Usually react app is static, so you dont need to calc all parents rect (except if it has floating parents, than use default method).
So for this case better use calcVisibleRectOverflowed
.
This method will find only parents with overflow style set and calculate clipping only with them. It may boost performance for some cases.
Also if you know all containers with scroll (which you can find with findScrollContainerTopStack
) you can calculate directly with calcVisibleRectWithStack
.
import { virtualOverflowUtils } from "react-virtual-overflow/lib/utils";
// in component
const [parentsWithOverflow, setParentsWithOverflow] = useState([] as any[]);
useLayoutEffect(() => {
// we find all elements with overflow once
const stack = findScrollContainerTopStack(containerRef.current);
setParentsWithOverflow(stack);
}, []);
const { renderedItems } = useVirtualOverflowY({
containerRef,
itemsLengthY,
itemHeight,
calcVisibleRect: (el: HTMLElement) => {
// calculate only by found overflows
return virtualOverflowUtils.calcVisibleRectWithStack(el, parentsWithOverflow);
},
renderItem,
},
// add overflow stack to deps
[parentsWithOverflow]
);