question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[feature] Allow keeping already rendered items mounted

See original GitHub issue

It 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:closed
  • Created 2 years ago
  • Comments:11

github_iconTop GitHub Comments

1reaction
piecykcommented, Jul 13, 2021

Having rangeExtractor would also allow us to support sticky elements, it’s also a pretty common use case #115

Something like this

const rows = new Array(10000)
  .fill(true)
  .map(() => 25 + Math.round(Math.random() * 100));

function StickyDemo() {
  const parentRef = React.useRef();
  const visibleStartIndexRef = React.useRef(0);

  const stickyRef = React.useRef([0, 10, 20, 30, 40, 50, 60]);

  const isSticky = index => stickyRef.current.includes(index);

  const isActiveSticky = index =>
    isSticky(index) && visibleStartIndexRef.current >= index;

  const rowVirtualizer = useVirtual({
    size: rows.length,
    parentRef,
    estimateSize: React.useCallback(() => 50, []),
    rangeExtractor: React.useCallback(range => {
      visibleStartIndexRef.current = range.start;

      const next = new Set([
        ...stickyRef.current,
        ...defaultRangeExtractor(range)
      ]);

      return [...next].sort((a, b) => a - b);
    }, [])
  });

  return (
    <>
      <div
        ref={parentRef}
        className="List"
        style={{
          height: `200px`,
          width: `400px`,
          overflow: "auto"
        }}
      >
        <div
          style={{
            height: `${rowVirtualizer.totalSize}px`,
            width: "100%",
            position: "relative"
          }}
        >
          {rowVirtualizer.virtualItems.map(virtualRow => (
            <div
              key={virtualRow.index}
              className={virtualRow.index % 2 ? "ListItemOdd" : "ListItemEven"}
              style={{
                ...(isSticky(virtualRow.index)
                  ? {
                      background: "red",
                      zIndex: 1
                    }
                  : {}),
                ...(isActiveSticky(virtualRow.index)
                  ? {
                      position: "sticky"
                    }
                  : {
                      position: "absolute",
                      transform: `translateY(${virtualRow.start}px)`
                    }),
                top: 0,
                left: 0,
                width: "100%",
                height: `${virtualRow.size}px`
              }}
            >
              Row {virtualRow.index}
            </div>
          ))}
        </div>
      </div>
    </>
  );
}
1reaction
piecykcommented, Jul 12, 2021

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 isVisible on item would give really easy optimisation by custom equal method to skip re-rendering items that are not in viewport

Read more comments on GitHub >

github_iconTop Results From Across the Web

[feature] Allow keeping already rendered items mounted #163
It works by keeping previously rendered virtualItems in the array and updating them when new items arrive. However, there is an issue if...
Read more >
Rendering and Updating Data using Component Lifecycle ...
To do that, we can use a widely used lifecycle hook called componentDidMount . The componentDidMount() method will be triggered as soon as...
Read more >
How to keep React component state between mount/unmount?
I have another component <App> that toggles whether or not <StatefulView> is rendered. However, I want to keep <StatefulView> 's internal state between...
Read more >
5 Ways to Avoid React Component Re-Renderings
Memoization enables your code to re-render components only if there's a change in the props. With this technique, developers can avoid unnecessary renderings ......
Read more >
React re-renders guide: everything, all at once - Developer way
re-render - second and any consecutive render of a component that is already on the screen. Re-render happens when React needs to update...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found