Scroll restoration is the process of restoring the scroll position of a page when the user navigates back to it. This is normally a built-in feature for standard HTML based websites, but can be difficult to replicate for SPA applications because:
history.pushState
API for navigation, so the browser doesn't know to restore the scroll position nativelyNot only that, but it's very common for applications to have multiple scrollable areas within an app, not just the body. For example, a chat application might have a scrollable sidebar and a scrollable chat area. In this case, you would want to restore the scroll position of both areas independently.
To alleviate this problem, TanStack Router provides a scroll restoration component and hook that handle the process of monitoring, caching and restoring scroll positions for you.
It does this by:
window
and body
)That may sound like a lot, but for you, it's as simple as this:
import { ScrollRestoration } from '@tanstack/react-router'
function Root() { return ( <> <ScrollRestoration /> <Outlet /> </> )}
Just render the ScrollRestoration
component (or use the useScrollRestoration
hook) at the root of your application and it will handle everything automatically!
Falling in behind Remix's own Scroll Restoration APIs, you can also customize the key used to cache scroll positions for a given scrollable area using the getKey
option. This could be used, for example, to force the same scroll position to be used regardless of the users browser history.
The getKey
option receives the relevant Location
state from TanStack Router and expects you to return a string to uniquely identify the scrollable measurements for that state.
The default getKey
is (location) => location.state.key!
, where key
is the unique key generated for each entry in the history.
You could sync scrolling to the pathname:
import { ScrollRestoration } from '@tanstack/react-router'
function Root() { return ( <> <ScrollRestoration getKey={(location) => location.pathname} /> <Outlet /> </> )}
You can conditionally sync only some paths, then use the key for the rest:
import { ScrollRestoration } from '@tanstack/react-router'
function Root() { return ( <> <ScrollRestoration getKey={(location) => { const paths = ['/', '/chat'] return paths.includes(location.pathname) ? location.pathname : location.state.key! }} /> <Outlet /> </> )}
Sometimes you may want to prevent scroll restoration from happening. To do this you can utilize the resetScroll
option available on the following APIs:
<Link resetScroll={false}>
navigate({ resetScroll: false })
redirect({ resetScroll: false })
When resetScroll
is set to false
, the scroll position for the next navigation will not be restored (if navigating to an existing history event in the stack) or reset to the top (if it's a new history event in the stack).
Most of the time, you won't need to do anything special to get scroll restoration to work. However, there are some cases where you may need to manually control scroll restoration. The most common example is virtualized lists.
To manually control scroll restoration, you can use the useElementScrollRestoration
hook and the data-scroll-restoration-id
DOM attribute:
function Component() { // We need a unique ID for manual scroll restoration on a specific element // It should be as unique as possible for this element across your app const scrollRestorationId = 'myVirtualizedContent'
// We use that ID to get the scroll entry for this element const scrollEntry = useElementScrollRestoration({ id: scrollRestorationId, })
// Let's use TanStack Virtual to virtualize some content! const virtualizerParentRef = React.useRef<HTMLDivElement>(null) const virtualizer = useVirtualizer({ count: 10000, getScrollElement: () => virtualizerParentRef.current, estimateSize: () => 100, // We pass the scrollY from the scroll restoration entry to the virtualizer // as the initial offset initialOffset: scrollEntry?.scrollY, })
return ( <div ref={virtualizerParentRef} // We pass the scroll restoration ID to the element // as a custom attribute that will get picked up by the // scroll restoration watcher data-scroll-restoration-id={scrollRestorationId} className="flex-1 border rounded-lg overflow-auto relative" > ... </div> )}
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.