Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unexpected jumpy behavior occurs when scrolling, suddenly jumping up to page 1. #447

Open
humanscape-mars opened this issue May 12, 2024 · 2 comments

Comments

@humanscape-mars
Copy link

humanscape-mars commented May 12, 2024

Describe the bug
When using the WindowVirtualizer component for infinite scrolling of paginated pages, there is unexpected behavior where the scroll position suddenly jumps to page 1. This issue occurs particularly when the list pages (e.g., pages 1, 2, 3...) are not cached and need to be fetched from the server.

This is my implementation.


import { CacheSnapshot, WindowVirtualizer, WindowVirtualizerHandle } from 'virtua';

import type { LazyQueryExecFunction } from '@apollo/client';

interface RestorableListProps {
  mergedNodes: Data[];
  currPage: number;
  setCurrPage: React.Dispatch<React.SetStateAction<number>>;

  fetchList: LazyQueryExecFunction<
    ListQuery,
    Exact<{
      listOptions: ListOptions;
    }>
  >;
}

const RestorableList = ({
  mergedNodes,
  currPage,
  setCurrPage,
  fetchList,
}: RestorableListProps) => {
  const cacheKey = 'window-list-cache';

  const ref = useRef<WindowVirtualizerHandle>(null);

  const [_, offset, cache] = useMemo(() => {
    const serialized = sessionStorage.getItem(cacheKey);
    if (!serialized) return [];
    try {
      return JSON.parse(serialized) as [number, number, CacheSnapshot];
    } catch (e) {
      return [];
    }
  }, []);

  useLayoutEffect(() => {
    if (!ref.current) return;
    const handle = ref.current;

    window.scrollTo(0, offset ?? 0);

    let scrollY = 0;
    const onScroll = () => {
      scrollY = window.scrollY;
    };
    window.addEventListener('scroll', onScroll);
    onScroll();

    return () => {
      window.removeEventListener('scroll', onScroll);
      // Use stored window.scrollY because it may return 0 in useEffect cleanup

      sessionStorage.setItem(cacheKey, JSON.stringify([currPage, scrollY, handle.cache]));
    };
  }, [currPage]);

  const onRangeChange = async (start: number, end: number) => {
    const pagePosition = Math.floor((end + 2) / 20) + 1;

    if (pagePosition > currPage) {
      setCurrPage(pagePosition);
      fetchList({
        variables: {
          listOptions: {
            page: pagePosition,
          },
        },
      });
    }
  };

  return (
    <WindowVirtualizer ref={ref} cache={cache} onRangeChange={onRangeChange}>
      {mergedNodes?.map((node, index) => (
        <EventCard
          key={index}
          eventData={node}
        />
      ))}
    </WindowVirtualizer>
  );
};

export default RestorableList;

To Reproduce
Steps to reproduce the behavior:

Scroll down through the list to trigger data fetching from the server.
Observe that as new data loads and is not present in the cache, the scroll position unexpectedly jumps back to page 1.

Expected behavior
The expected behavior is that as users scroll down, pages should load sequentially (pages 1, 2, 3, etc.) without the scroll position abruptly jumping back to the start or any previously viewed page.

Platform:

  • OS: [Windows, MacOS, Linux, Android, iOS]
  • Browser: [Chrome, Firefox, Safari, Edge]
  • Version of this package: 0.30.5
  • Version of framework: react 18.2.0

Additional context
Add any other context about the problem here.

If there are any workarounds to avoid this problem, please let me know. By the way, awesome library! I think it's the best in terms of virtual window functionality.

@inokawa
Copy link
Owner

inokawa commented May 13, 2024

As far as I granced the code, the useLayoutEffect seems to be called when currPage is changed. It should be called only on mount like:

+ const initialized = useRef(false);
  useLayoutEffect(() => {
    if (!ref.current) return;
    const handle = ref.current;

+   if (!initialized.current) {
+   initialized.current = false;
    window.scrollTo(0, offset ?? 0);
+   }

    let scrollY = 0;
    const onScroll = () => {
      scrollY = window.scrollY;
    };
    window.addEventListener('scroll', onScroll);
    onScroll();

    return () => {
      window.removeEventListener('scroll', onScroll);
      // Use stored window.scrollY because it may return 0 in useEffect cleanup

      sessionStorage.setItem(cacheKey, JSON.stringify([currPage, scrollY, handle.cache]));
    };
  }, [currPage]);

@humanscape-mars
Copy link
Author

Thank you, that's a reasonable solution, but the problem still persists.
hmm...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants