[feature] Allow keeping already rendered items mounted
See original GitHub issueIt would be nice to have an option to keep already mounted items in the list.
This could largely improve scroll performance for components which are expensive to render by not needlessly unmounting already rendered instances.
Of course this only works if the rendered items are properly optimized by eg. React.memo.
Here is a sample CodeSandbox
It works by keeping previously rendered virtualItems in the array and updating them when new items arrive.
However, there is an issue if items change their size which could potentially cause to render overlapping items. I did not test yet, if this can actually become an issue, but if so, it could only be solved if this feature is integrated into react-virtual because then we would have access to the measurement cache and could prevent outdated item positions.
What do you think of such a feature?
PS: This is the typescript code of the implementation:
import React from 'react';
import { useVirtual, VirtualItem } from 'react-virtual';
/**
* Merges two arrays while preserving their order.
* Ordering is determined by the passed getIndex function.
* If two items have the same index, the item from the first array (primary) is used so duplicates are eliminated.
*
* @example
* merge([1, 2, 5, 6, 8, 9], [3, 4, 5, 6, 8, 9], x => x);
* // returns [1, 2, 3, 4, 5, 6, 8, 9];
*/
export function mergeArraysWithOrder<T>(primary: T[], secondary: T[], getIndex: (item: T) => number): T[] {
let pi = 0;
let si = 0;
const result: T[] = [];
while (true) {
if (pi >= primary.length) {
result.push(...secondary.slice(si));
break;
} else if (si >= secondary.length) {
result.push(...primary.slice(pi));
break;
}
const pIndex = getIndex(primary[pi]);
const sIndex = getIndex(secondary[si]);
if (pIndex < sIndex) {
result.push(primary[pi]);
pi++;
} else if (pIndex > sIndex) {
result.push(secondary[si]);
si++;
} else {
result.push(primary[pi]);
pi++;
si++;
}
}
return result;
}
/**
* Same as useVirtual, but keeps once rendered items mounted
*/
export function useVirtualKeepMounted(options: Parameters<typeof useVirtual>[0]) {
const result = useVirtual(options);
const totalCount = options.size;
const lastVirtualItemsRef = React.useRef<VirtualItem[]>([]);
const virtualItems = React.useMemo(() => {
let lastItems = lastVirtualItemsRef.current;
if (lastItems.length > 0 && lastItems[lastItems.length - 1].index >= totalCount) {
lastItems = lastItems.filter(x => x.index < totalCount);
}
const vItems = mergeArraysWithOrder(
result.virtualItems, lastItems, x => x.index
);
lastVirtualItemsRef.current = vItems;
return vItems;
}, [totalCount, result.virtualItems]);
return { ...result, virtualItems };
}
Issue Analytics
- State:
- Created 2 years ago
- Comments:11

Top Related StackOverflow Question
Having
rangeExtractorwould also allow us to support sticky elements, it’s also a pretty common use case #115Something like this
Even better if rangeExtractor would return array of indexes to render, then keeping indexes already rendered would be really easy. Something like this https://github.com/tannerlinsley/react-virtual/compare/master...piecyk:feat/range-extractor
Having also
isVisibleon item would give really easy optimisation by custom equal method to skip re-rendering items that are not in viewport