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:
Not 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:
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 />
</>
)
}
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 />
</>
)
}
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 />
</>
)
}
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:
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>
)
}
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.