magrega
magrega

Reputation: 178

Infinite feed scroll jumps when setting overflow to scroll

I am using react-query for infinite loading feed and '@tanstack/react-virtual' for virtualization of the cards which are composed of mui elements. I want to keep my app height to 100vh so there's no whole screen scroll down. But the component which contains the feed can be scrollable. I use overflowY: 'scroll' on it so I can scroll inside this element without scrolling down the screen.

The Problem is that when I apply overflowY: 'scroll' to the div inside LogCards which wraps the map iterations the scroll becomes very jittery, it flickers very fast as if it has trouble calculating scroll position properly and then it breaks and all cards disappear. How can I fix this? As soon as I remove overflowY: 'scroll' and allow the whole screen to be scrolled the issue goes away. A short video of it.

Here's the component holding the feed:

const LogCards = ({ pageLimit }: LogCardsProps) => {
  const {
    data: logs,
    fetchNextPage,
    isFetchingNextPage,
    isLoading,
  } = useInfiniteLogsQuery(pageLimit);
  const { ref, inView } = useInView();

  const allLogs = useMemo(() => logs?.pages.flatMap((log) => log.results), [logs]) || [];
  const parentRef = useRef<HTMLDivElement>(null);

  const rowVirtualizer = useVirtualizer({
    count: allLogs.length,
    getScrollElement: useCallback(() => parentRef.current, []),
    estimateSize: useCallback(() => 300, []),
    overscan: 2,
    gap: 8,
  });

  useEffect(() => {
    if (inView) fetchNextPage();
  }, [fetchNextPage, inView]);

  return !isLoading ? (
    <Box ref={parentRef} sx={{ overflowY: 'scroll' }}>
      <div style={{ height: `${rowVirtualizer.getTotalSize()}px` }}>
        {allLogs &&
          rowVirtualizer.getVirtualItems().map((log, index) => {
            const LogCardData = allLogs[log.index];
            console.log(LogCardData);

            if (!LogCardData) return <span key={`no card index ${index}`}>No cards</span>;
            return (
              <LogCard
                key={LogCardData.id}
                data-index={log.index}
                ref={rowVirtualizer.measureElement}
                log={LogCardData}
              />
            );
          })}
      </div>
      <Box sx={{ height: '20px' }} ref={ref}>
        {isFetchingNextPage && <LinearProgress sx={{ m: '2px' }} />}
      </Box>
    </Box>
  ) : (
    <LinearLoader />
  );
};

Here's the component which holds LogCards:

const LogsList = () => {
  const [pageLimit, setPageLimit] = useState(25);

  return (
    <Paper
      sx={{
        display: 'flex',
        flexDirection: 'column',
        height: '100vh',
        px: 1,
        borderRadius: { xs: '4px', md: 0 },
      }}
    >
      <Typography variant="h5" textAlign="center" sx={{ py: 2 }}>
        List of unsolved logs
      </Typography>
      <LogsLimitButtons pageLimit={pageLimit} setPageLimit={setPageLimit} />
      <LogCards pageLimit={pageLimit} />
    </Paper>
  );
};

And here's general layout of the app:

const LaunchDesktop = () => {
  return (
    <Grid2 container spacing={1}>
      <Grid2
        size={{ xs: 12, md: 8 }}
        sx={{
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        <SosForm />
      </Grid2>
      <Grid2 size={{ xs: 12, md: 4 }}>
        <LogsList />
      </Grid2>
    </Grid2>
  );
};

Upvotes: 0

Views: 26

Answers (1)

magrega
magrega

Reputation: 178

Refered to this example on tanstack website and wrapped my LogCard iteration with a div that has a transform style. I guess under the hood all of the elements are absolutely positined and need to be explicitly set.

const LogCards = ({ pageLimit }: LogCardsProps) => {
  const {
    data: logs,
    fetchNextPage,
    isFetchingNextPage,
    isLoading,
  } = useInfiniteLogsQuery(pageLimit);
  const { ref, inView } = useInView();

  const allLogs = useMemo(() => logs?.pages.flatMap((log) => log.results), [logs]) || [];
  const parentRef = useRef<HTMLDivElement>(null);

  const rowVirtualizer = useVirtualizer({
    count: allLogs.length,
    getScrollElement: useCallback(() => parentRef.current, []),
    estimateSize: useCallback(() => 300, []),
    overscan: 2,
    gap: 8,
  });

  useEffect(() => {
    if (inView) fetchNextPage();
  }, [fetchNextPage, inView]);

  return !isLoading ? (
    <Box
      ref={parentRef}
      sx={{
        overflowY: 'scroll',
      }}
    >
      <div
        style={{
          height: rowVirtualizer.getTotalSize(),
          position: 'relative',
        }}
      >
        <div
          style={{
            transform: `translateY(${rowVirtualizer.getVirtualItems()[0]?.start ?? 0}px)`,
          }}
        >
          {allLogs &&
            rowVirtualizer.getVirtualItems().map((log, index) => {
              const LogCardData = allLogs[log.index];
              if (!LogCardData) return <span key={`no card index ${index}`}>No cards</span>;
              return (
                <LogCard
                  key={LogCardData.id}
                  data-index={log.index}
                  ref={rowVirtualizer.measureElement}
                  log={LogCardData}
                />
              );
            })}
        </div>
      </div>
      <Box sx={{ height: '20px' }} ref={ref}>
        {isFetchingNextPage && <LinearProgress sx={{ m: '2px' }} />}
      </Box>
    </Box>
  ) : (
    <LinearLoader />
  );
};

Upvotes: 0

Related Questions