Pagination state resets on back navigation when syncing TanStack Query with React Router URL params
0 reputation · 02 Jun 2020, 18:44 UTC
0 reputation · 02 Jun 2020, 18:44 UTC
Establish a consistent pattern for synchronizing pagination state across the URL, TanStack Query v5 cache, and a virtualized list (react-virtual) in a React 18 application using React Router v6.
useInfiniteQuery manages page cache internally but does not persist page parameters to the URL by default.useSearchParams can sync page state to the URL for shareable links, yet writing search params may trigger component remounts depending on router configuration.useInfiniteQuery with React Router v6 loaders?select option for flattening pages eliminate the need for manual flatMap when feeding react-virtual, and how does that interact with URL-synced page parameters?35775 reputation · 02 Jun 2020, 20:45 UTC
Make the URL search parameter the single source of truth for the current page. Read page with useSearchParams, include it in the TanStack Query key, and let the query cache own only the fetched pages. Do not initialize a local page state to 1, because back navigation restores the URL but not local state, so the list restarts at page 1.
The reset happens when the component reads page from local state or when setSearchParams causes a remount. Back navigation restores the URL, but a remounted component re-initializes page to its default and useInfiniteQuery starts a fresh query at page 1.
const [searchParams, setSearchParams] = useSearchParams();
const page = Number(searchParams.get('page') || '1');
const { data, isFetching } = useInfiniteQuery({
queryKey: ['items', page],
queryFn: () => fetchPage(page),
initialPageParam: page,
getNextPageParam: () => undefined,
keepPreviousData: true,
select: (result) => result.pages.flatMap((pageData) => pageData.items),
});
function goToPage(nextPage) {
setSearchParams((prev) => {
prev.set('page', String(nextPage));
return prev;
}, { replace: true });
}
const scrollKey = `scroll:${window.location.pathname}${window.location.search}`;
const containerRef = useRef(null);
const virtualizerRef = useRef(null);
useLayoutEffect(() => {
const el = containerRef.current;
if (!el) return;
const onScroll = () => sessionStorage.setItem(scrollKey, String(Math.round(el.scrollTop)));
el.addEventListener('scroll', onScroll, { passive: true });
return () => el.removeEventListener('scroll', onScroll);
}, [scrollKey]);
useEffect(() => {
if (!data || !containerRef.current || !virtualizerRef.current) return;
const saved = sessionStorage.getItem(scrollKey);
if (saved !== null) {
const offset = Number(saved);
containerRef.current.scrollTop = offset;
virtualizerRef.current.scrollToOffset(offset, { align: 'start' });
}
}, [data, scrollKey]);
Assumes client-side React Router v6 navigation, TanStack Query v5, a fixed-height react-virtual container, and stable item IDs. If you use React Router v6 loaders, derive page in the loader and pass it into the same query key; the loader preloads data but does not replace the URL as the source of truth. Verify loader behavior if you are using server-side rendering.
Missing diagnostic: does back navigation keep the same route and component instance, or does the list remount under a new route or key? If it remounts, fix the route structure first because URL sync alone cannot preserve scroll across a remount.
Use comments to ask for clarification. Post a solution as an answer.
35,775 reputation · 02 Jun 2020, 19:34 UTC
Make the URL page the source of truth, but note that useInfiniteQuery is built for sequential "load more" fetching, not arbitrary page jumps restored by the browser's back button. initialPageParam only applies on the first mount; if the component remounts, the query key must already contain the URL-derived page before TanStack Query evaluates it, otherwise the cache restarts at page 1.
For react-virtual, native scroll restoration runs before virtual items are measured. Restore position manually in an effect after measure(), using sessionStorage keyed by route and page. Verify in TanStack Query DevTools that the cache key matches the URL after back navigation.