From 509b7b871b85d7bfc8f0cfcff86d1ce22f64ba8e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 2 Jun 2022 19:31:25 -0500 Subject: [PATCH] ScrollableList: useMemo, useCallback, throttle, refactor, make it nice --- app/soapbox/components/scrollable_list.tsx | 40 +++++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/app/soapbox/components/scrollable_list.tsx b/app/soapbox/components/scrollable_list.tsx index 83721567b..ae9b861e9 100644 --- a/app/soapbox/components/scrollable_list.tsx +++ b/app/soapbox/components/scrollable_list.tsx @@ -1,6 +1,7 @@ -import React, { useEffect, useRef } from 'react'; +import { throttle } from 'lodash'; +import React, { useEffect, useRef, useMemo, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; -import { Virtuoso, Components, VirtuosoProps, VirtuosoHandle, ListRange } from 'react-virtuoso'; +import { Virtuoso, Components, VirtuosoProps, VirtuosoHandle, ListRange, IndexLocationWithAlign } from 'react-virtuoso'; import PullToRefresh from 'soapbox/components/pull-to-refresh'; import { useSettings } from 'soapbox/hooks'; @@ -13,6 +14,7 @@ type Context = { listClassName?: string, } +/** Scroll position saved in sessionStorage. */ type SavedScrollPosition = { index: number, offset: number, @@ -55,6 +57,7 @@ interface IScrollableList extends VirtuosoProps { /** Legacy ScrollableList with Virtuoso for backwards-compatibility */ const ScrollableList = React.forwardRef(({ + scrollKey, prepend = null, alwaysPrepend, children, @@ -80,8 +83,8 @@ const ScrollableList = React.forwardRef(({ const autoloadMore = settings.get('autoloadMore'); // Preserve scroll position - const scrollDataKey = `soapbox:scrollData:${location.pathname}`; - const scrollData: SavedScrollPosition | null = JSON.parse(sessionStorage.getItem(scrollDataKey)!); + const scrollDataKey = `soapbox:scrollData:${scrollKey}`; + const scrollData: SavedScrollPosition | null = useMemo(() => JSON.parse(sessionStorage.getItem(scrollDataKey)!), []); const topIndex = useRef(scrollData ? scrollData.index : 0); const topOffset = useRef(scrollData ? scrollData.offset : 0); @@ -103,20 +106,23 @@ const ScrollableList = React.forwardRef(({ data.push(); } - const handleScroll = () => { + const handleScroll = useCallback(throttle(() => { + // HACK: Virtuoso has no better way to get this... const node = document.querySelector(`[data-virtuoso-scroller] [data-item-index="${topIndex.current}"]`); if (node) { topOffset.current = node.getBoundingClientRect().top * -1; } - }; + }, 150, { trailing: true }), []); useEffect(() => { document.addEventListener('scroll', handleScroll); sessionStorage.removeItem(scrollDataKey); return () => { - const data = { index: topIndex.current, offset: topOffset.current }; - sessionStorage.setItem(scrollDataKey, JSON.stringify(data)); + if (scrollKey) { + const data: SavedScrollPosition = { index: topIndex.current, offset: topOffset.current }; + sessionStorage.setItem(scrollDataKey, JSON.stringify(data)); + } document.removeEventListener('scroll', handleScroll); }; }, []); @@ -166,6 +172,22 @@ const ScrollableList = React.forwardRef(({ handleScroll(); }; + /** Figure out the initial index to scroll to. */ + const initialIndex = useMemo(() => { + if (showLoading) return 0; + if (initialTopMostItemIndex) return initialTopMostItemIndex; + + if (scrollData && history.action === 'POP') { + return { + align: 'start', + index: scrollData.index, + offset: scrollData.offset, + }; + } + + return 0; + }, [showLoading, initialTopMostItemIndex]); + /** Render the actual Virtuoso list */ const renderFeed = (): JSX.Element => ( (({ endReached={handleEndReached} isScrolling={isScrolling => isScrolling && onScroll && onScroll()} itemContent={renderItem} - initialTopMostItemIndex={showLoading ? 0 : initialTopMostItemIndex || (scrollData && history.action === 'POP' ? { align: 'start', index: scrollData.index, offset: scrollData.offset } : 0)} + initialTopMostItemIndex={initialIndex} rangeChanged={handleRangeChange} style={style} context={{