Reputation: 11184
How do I set up React Router 6 to restore scroll position when I navigate and when the browser window is refreshed?
React Router 5 has a page about scroll restoration, but I can't find anything about scrolling in the docs for v6, so I guess that you're supposed to handle this yourself with another package. Fair enough, but I can't find anything that's compatible with React Router 6.
The packages react-scroll-restoration and oaf-react-router require v5. (oaf-react-router does list that it supports v6, but the basic usage code example isn't compatible with v6, and the related issue #210 is still open.)
Gatsby and Next.js support scroll restoration out of the box, but I doesn't look like there's a neatly packaged package that you can just use.
This little demo app with server side rendered pages does what I want. Scroll position is restored when navigation back and forth and refreshing the browser window.
Here is the same app using React Router 6, where the scroll position isn't saved and restored, but actually reused between pages. The usual workaround for that is to scroll to the top whenever the page is navigated, but I am not interested in that behaviour.
Edit: React Query writes that the issue with scroll restoration is that pages are refetching data, thereby implying that if the data for rendering the pages is there, scroll restoration just works. I cannot confirm that, because my small React Router 6 app has the issue even without doing any data fetching at all. I feel like there is something small think I am missing in order to get it to work.
Rant: I am quite surprised that the typical answer to this issue is to call window.scrollTo(0, 0)
when navigating. This only fixes the issue of the scroll position being transferred between pages. When the scroll position isn't restored, the user experience when navigating between pages is seriously deteriorated. I guess this is partly why pop-up windows have become so popular, but they bring a long suite of other UX issues, so I really want to avoid using them.
Upvotes: 16
Views: 34150
Reputation: 89204
The ScrollRestoration
component handles exactly this. It requires using a data router, such as one created by calling createBrowserRouter
(which is recommended for all new React Router web projects).
This component will emulate the browser's scroll restoration on location changes after loaders have completed to ensure the scroll position is restored to the right spot, even across domains.
To use it, simply render it once in the application root component:
import { ScrollRestoration } from "react-router-dom";
function App() {
return <>
<div>Some content...</div>
<ScrollRestoration/>
</>;
}
Upvotes: 6
Reputation: 39
now react-router-dom v6.4 cover that issue https://dev.to/tywenk/how-to-use-nested-routes-in-react-router-6-4jhd
Upvotes: 0
Reputation: 11
Using createBrowserHistory and storing yscroll in session storage, one can achieve this. Refer to below link: https://medium.com/@knl.shivam.gupta/react-hooks-scroll-position-using-react-router-dom-v6-6cd59730b18d
Here, the location.state in v6 is used to store the yscroll in the browser history stack which thus makes it possible to manipulate the display using useLayoutEffect.
Upvotes: 1
Reputation: 11184
I know I already posted one answer, but I ended up with a different solution, and I think it deserves a separate answer.
The first thing I had to realize is that StackBlitz and CodeSandbox both have some custom route handling that breaks scroll restoration. This issue is still there when opening up the demo app in a separate window. If the app is running in a normal environment, the scroll position is restored when navigating between pages using the browser's back and forward buttons.
These two issues remain:
The scroll position is persisted between pages when navigating using links in the app. Technically when using React Router's <Link to="..."/>
or navigate(...)
. The page should be scrolled to the top.
Recap: Go to page A, scroll down and click a link that takes you to page B. If you navigate back to A using the browser's back button, the scroll position should be restore. If you navigate back to A using a link to A on page B, the scroll position should be at the top of the page.
The scroll position of a page should be restored (kept) when refreshing the page. The pages are scrolled to the top when refreshing.
I solved the first issue using the code below, and accepted that the second issue is still an issue. I think Gatsby and Next.js are able to solve the refresh issue by storing the scroll position in session storage, and that felt like overkill for my use case.
import { MouseEventHandler, ReactNode } from "react";
import { Link } from "react-router-dom";
import { useNavigateToTop } from "./useNavigateToTop";
interface Props {
children: ReactNode;
className?: string;
to: string;
}
/** Link to the top of a page so that the scroll position isn't persisted between pages. Use this instead of React's build-in @see {@link Link}. */
export const LinkToTop = (props: Props) => {
const navigateToTop = useNavigateToTop();
const navigateAndReset: MouseEventHandler<HTMLAnchorElement> = (event) => {
event.preventDefault();
navigateToTop(props.to);
};
return (
<Link className={props.className} onClick={navigateAndReset} to={props.to}>
{props.children}
</Link>
);
};
import { useNavigate } from "react-router-dom";
/** Navigate to the top of a page so that the scroll position isn't persisted between pages. Use this instead of React Dom's build-in @see {@link useNavigate}. */
export const useNavigateToTop = () => {
const navigate = useNavigate();
const navigateAndReset = (to: string) => {
navigate(to, { replace: true });
window.scrollTo(0, 0);
};
return navigateAndReset;
};
Upvotes: 1
Reputation: 11184
Thanks to this comment in oaf-react-router I was able to get it to work with React Router 6. There are a few caveats, though, so I do not consider this a viable solution for a professional web app.
As stated in this code comment, oaf-react-router
has to use the same version of history
as react-router-dom
does. That's why HistoryRouter
is exported as unstable_HistoryRouter
. This solution does indeed feel quite unstable.
oaf-react-router
does not restore the scroll position when refreshing a web page. I don't know if this can be achieved easily, and it's something that might be acceptable.
import { createBrowserHistory } from 'history';
import { wrapHistory } from 'oaf-react-router';
import React from 'react';
import ReactDOM from 'react-dom';
import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom';
const history = createBrowserHistory();
wrapHistory(history);
ReactDOM.render(
<React.StrictMode>
<HistoryRouter history={history}>
<App />
</HistoryRouter>
</React.StrictMode>,
document.getElementById('root')
);
Here a full working example on StackBlitz.
Upvotes: 1