export const DataSourceRendererVirtual:()

in desktop/flipper-plugin/src/data-source/DataSourceRendererVirtual.tsx [78:328]


export const DataSourceRendererVirtual: <T extends object, C>(
  props: DataSourceProps<T, C>,
) => React.ReactElement = memo(function DataSourceRendererVirtual({
  dataSource,
  defaultRowHeight,
  useFixedRowHeight,
  context,
  itemRenderer,
  autoScroll,
  onKeyDown,
  virtualizerRef,
  onRangeChange,
  onUpdateAutoScroll,
  emptyRenderer,
}: DataSourceProps<any, any>) {
  /**
   * Virtualization
   */
  // render scheduling
  const renderPending = useRef(UpdatePrio.NONE);
  const lastRender = useRef(Date.now());
  const [, setForceUpdate] = useState(0);
  const forceHeightRecalculation = useRef(0);
  const parentRef = React.useRef<null | HTMLDivElement>(null);
  const isUnitTest = useInUnitTest();

  const virtualizer = useVirtual({
    size: dataSource.view.size,
    parentRef,
    useObserver: isUnitTest ? () => ({height: 500, width: 1000}) : undefined,
    // eslint-disable-next-line
    estimateSize: useCallback(
      () => defaultRowHeight,
      [forceHeightRecalculation.current, defaultRowHeight],
    ),
    // TODO: optimise by using setting a keyExtractor if DataSource is keyed
    overscan: 0,
  });
  if (virtualizerRef) {
    virtualizerRef.current = virtualizer;
  }

  const redraw = useCallback(() => {
    forceHeightRecalculation.current++;
    setForceUpdate((x) => x + 1);
  }, []);

  useEffect(
    function subscribeToDataSource() {
      const forceUpdate = () => {
        if (unmounted) {
          return;
        }
        timeoutHandle = undefined;
        setForceUpdate((x) => x + 1);
      };

      let unmounted = false;
      let timeoutHandle: any = undefined;

      function rerender(prio: 1 | 2, invalidateHeights = false) {
        if (invalidateHeights && !useFixedRowHeight) {
          // the height of some existing rows might have changed
          forceHeightRecalculation.current++;
        }
        if (isUnitTest) {
          // test environment, update immediately
          forceUpdate();
          return;
        }
        if (renderPending.current >= prio) {
          // already scheduled an update with equal or higher prio
          return;
        }
        renderPending.current = Math.max(renderPending.current, prio);
        if (prio === UpdatePrio.LOW) {
          // Possible optimization: make DEBOUNCE depend on how big the relative change is, and how far from the current window
          if (!timeoutHandle) {
            timeoutHandle = setTimeout(forceUpdate, LOW_PRIO_UPDATE);
          }
        } else {
          // High, drop low prio timeout
          if (timeoutHandle) {
            clearTimeout(timeoutHandle);
            timeoutHandle = undefined;
          }
          if (lastRender.current < Date.now() - HIGH_PRIO_UPDATE) {
            forceUpdate(); // trigger render now
          } else {
            // debounced
            timeoutHandle = setTimeout(forceUpdate, HIGH_PRIO_UPDATE);
          }
        }
      }

      dataSource.view.setListener((event) => {
        switch (event.type) {
          case 'reset':
            rerender(UpdatePrio.HIGH, true);
            break;
          case 'shift':
            if (dataSource.view.size < SMALL_DATASET) {
              rerender(UpdatePrio.HIGH, false);
            } else if (
              event.location === 'in' ||
              // to support smooth tailing we want to render on records directly at the end of the window immediately as well
              (event.location === 'after' &&
                event.delta > 0 &&
                event.index === dataSource.view.windowEnd)
            ) {
              rerender(UpdatePrio.HIGH, false);
            } else {
              // optimization: we don't want to listen to every count change, especially after window
              // and in some cases before window
              rerender(UpdatePrio.LOW, false);
            }
            break;
          case 'update':
            // in visible range, so let's force update
            rerender(UpdatePrio.HIGH, true);
            break;
        }
      });

      return () => {
        unmounted = true;
        dataSource.view.setListener(undefined);
      };
    },
    [dataSource, setForceUpdate, useFixedRowHeight, isUnitTest],
  );

  useEffect(() => {
    // initial virtualization is incorrect because the parent ref is not yet set, so trigger render after mount
    setForceUpdate((x) => x + 1);
  }, [setForceUpdate]);

  useLayoutEffect(function updateWindow() {
    const start = virtualizer.virtualItems[0]?.index ?? 0;
    const end = start + virtualizer.virtualItems.length;
    if (start !== dataSource.view.windowStart && !autoScroll) {
      onRangeChange?.(
        start,
        end,
        dataSource.view.size,
        parentRef.current?.scrollTop ?? 0,
      );
    }
    dataSource.view.setWindow(start, end);
  });

  /**
   * Scrolling
   */
  const onScroll = useCallback(() => {
    const elem = parentRef.current;
    if (!elem) {
      return;
    }
    const fromEnd = elem.scrollHeight - elem.scrollTop - elem.clientHeight;
    if (autoScroll && fromEnd > 1) {
      onUpdateAutoScroll?.(false);
    } else if (!autoScroll && fromEnd < 1) {
      onUpdateAutoScroll?.(true);
    }
  }, [onUpdateAutoScroll, autoScroll]);

  useLayoutEffect(function scrollToEnd() {
    if (autoScroll) {
      virtualizer.scrollToIndex(
        dataSource.view.size - 1,
        /* smooth is not typed by react-virtual, but passed on to the DOM as it should*/
        {
          align: 'end',
          behavior: 'smooth',
        } as any,
      );
    }
  });

  /**
   * Render finalization
   */
  useEffect(function renderCompleted() {
    renderPending.current = UpdatePrio.NONE;
    lastRender.current = Date.now();
  });

  /**
   * Observer parent height
   */
  useEffect(
    function redrawOnResize() {
      if (!parentRef.current) {
        return;
      }

      let lastWidth = 0;
      const observer = observeRect(parentRef.current, (rect) => {
        if (lastWidth !== rect.width) {
          lastWidth = rect.width;
          redraw();
        }
      });
      observer.observe();
      return () => observer.unobserve();
    },
    [redraw],
  );

  /**
   * Rendering
   */
  return (
    <RedrawContext.Provider value={redraw}>
      <div ref={parentRef} onScroll={onScroll} style={tableContainerStyle}>
        {virtualizer.virtualItems.length === 0
          ? emptyRenderer?.(dataSource)
          : null}
        <div
          style={{
            ...tableWindowStyle,
            height: virtualizer.totalSize,
          }}
          onKeyDown={onKeyDown}
          tabIndex={0}>
          {virtualizer.virtualItems.map((virtualRow) => {
            const value = dataSource.view.get(virtualRow.index);
            // the position properties always change, so they are not part of the TableRow to avoid invalidating the memoized render always.
            // Also all row containers are renderd as part of same component to have 'less react' framework code in between*/}
            return (
              <div
                key={virtualRow.index}
                style={{
                  position: 'absolute',
                  top: 0,
                  left: 0,
                  width: '100%',
                  height: useFixedRowHeight ? virtualRow.size : undefined,
                  transform: `translateY(${virtualRow.start}px)`,
                }}
                ref={useFixedRowHeight ? undefined : virtualRow.measureRef}>
                {itemRenderer(value, virtualRow.index, context)}
              </div>
            );
          })}
        </div>
      </div>
    </RedrawContext.Provider>
  );
}) as any;