After transiting from one page to another page via next <Link />
, if user clicks the back button and the previous page has getInitialProps
method that takes some time to finish, the scroll position of the previous will be restored before the previous pages gets rendered.
source code can be found here
After clicking back button, the "go back" text should still be visible (not scroll down to previous position) until the previous page gets rendered
This seems to happen because next's router (and <Link />
) uses window.history.pushState
and then window.history.back
restores the scroll. This doesn't happen with common <a />
because it doesn't use pushState
.
This is an interesting article about how scroll restoration works on each browser. Basically you can disable it and manage the scroll on your own:
history.scrollRestoration = 'manual'
Thanks for you explanation.
Using a plain <a />
tag would cause a full page reload, which is not what I expected. I expect the back button would either not trigger the getInitialProps
method or don’t restore scroll until the getInitialProps
is finished
On 28 Dec 2017, at 12:10, Pablo Varela notifications@github.com wrote:
This seems to happen because next's router (and ) uses window.history.pushState and then window.history.back restores the scroll. This doesn't happen with common because it doesn't use pushState.
This is an interesting article https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration about how scroll restoration works on each browser. Basically you can disable it and manage the scroll on your own:
history.scrollRestoration = 'manual'
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub https://github.com/zeit/next.js/issues/3303#issuecomment-354225323, or mute the thread https://github.com/notifications/unsubscribe-auth/AAncD7Vrn7rZJW5NYdokbPsXWn7O4N5Eks5tExSmgaJpZM4QiFwt.
Yeah, I understand. I'm not sure what the solution would be from within next.
@liweinan0423 I'm not sure but this could be a workaround
We've come across this too and it feels like something next should be doing internally. Otherwise everyone needs to write their own code to store scrollpositions and then scroll on completion of getInitialProps.
we're having a related issue where, upon clicking a link from a scrolled-down position (say, result 20 in a list) the new page shows up halfway down so the user needs to scroll up to see the item itself.
here's an example screen capture of the issue we're having:
We're also running into this issue and it makes the user experience very frustrating to use. @arunoda Any thoughts or way to fix this?
I was having this same issue as @mgiraldo awhile back and I originally was just using router.push(), but as soon as I switched to using the tag, my problem resolved itself with out needing to implement a hack like scroll-to-top in component did mount or something ghetto like that. Curious if you are using a Link tag at all?
it is a regular <Link />
see: https://github.com/dpla/dpla-frontend/blob/master/components/ExhibitionsComponents/AllExhibitions/components/ExhibitionsList/index.js#L8-L18
but we ended up using componentDidMount
with window.scrollTo
in the destination page: https://github.com/dpla/dpla-frontend/blob/master/pages/exhibitions/exhibition/index.js#L25-L27
feels very hacky, though... and we have the Back button issue mentioned in the issue in other parts of the site.
you can see the problem in action here: https://dp.la/exhibitions (scroll down and click)... the patch will be up tomorrow
you can see the patched version here: https://beta-staging.dp.la/exhibitions
@mgiraldo I see a slight flash when using the back button. We're running into the same issue.
@mgiraldo In another part of my site I forgot I did this:
Router.push("/account")
window.scrollTo(0, 0)
document.body.focus()
which works when navigating TO a page, but like you said when you hit the back button, its at the top of the window because I think the window recorded that in its history....
What are you doing to mitigate that back button issue? Its like I can get one to work but not the other.
@spencersmb What do you mean switch to using the tag? What tag is that?
We've a similar issues reported here: https://spectrum.chat/thread/3d65b436-b085-4eef-ad84-0941c473e09d
Next.js doesn't hijack scrolling. But this is a real issue. Here's what's happening.
This is because of browser's default scrolling behavior. See: https://github.com/zeit/next.js/issues/3303#issuecomment-354225323
When you go back, browser tries to go to the previous scroll position for better UX. But in the ^^ case, the data is not loaded yet. But the data will be there in a bit with the latency of getInitialProps()
. When the data rendered, it's not the correct position as before.
So, in order fix this you can do a few things:
Router.push()
. (This will return a promise and it'll get resolved when the page is rendered)We've no plan to manage scrolling inside Next.js yet. But you can do it in the userland and publish a new Link component like LinkBetterScrolling
.
I mean every time a javascript library gets popular and people start using it in production ... some majors issues show up and the official response is: "_Fix it yourself._" 😢
I fail to understand how does Router.push()
fix scrolling issue that happens when you press button back
in a browser.
@mgiraldo Posted a link to a patched version of his website.
1) Go here: https://beta-staging.dp.la/exhibitions
2) Scroll to the bottom
3) Click on activism in the US
4) Click on button back
in your browser or just swipe back
There are two major issues:
1) Before the list gets rendered. Item detail scrolls to the bottom for no reason.
2) When the list gets rendered you are not at the bottom of the list but somewhere in the middle.
I believe it would be nice to _at least consider fixing this issue in the future_.
Other than that thanks for all the hard work that you put in nextjs. It might take a little longer but we are getting closer to stable libraries in javascript community.
@developer239 We have the exact same issue with our Next.js app as documented here: https://spectrum.chat/thread/3d65b436-b085-4eef-ad84-0941c473e09d
It completely kills the user experience.
Basically, the current page scrolls to the bottom of the page because it's trying to restore scroll position before the previous page is rendered.
@arunoda Could you explain why the Next team isn't considering an official fix to this issue? It seems like it's a breaking bug that completely ruins the user experience.
if i understand correctly the back button/scroll position functionality is a native browser behavior that assumes a standard page refresh (user goes from url x to url y via a standard http request). what client-side javascript-based frameworks do is implement a page transition without making use of the native browser behavior (via pushState
or similar). the browser is counting on itself having a cached version of the page to return you to, but it doesn't (it has an incomplete version that is completed by the framework after the jump is made). however, if your app is not fast enough to produce page transitions/content using the framework (in either direction, backwards or forward), you will get this jarring, second-rate experience.
what i think the nextjs team is saying is the effort of implementing a cross-browser solution to this problem is beyond the scope they are willing to take on.
I think if a next.js team provides its own routing management they should care about similar critical issues as well. For example react-router doesn't have this issue and they don't think that is should be implemented by another developer themselves.
Definitely this should be fixed because it makes the user experience very frustrating and confusing.
Hi to all! I think i solve this issue by following the advice of @pablopunk
history.scrollRestoration = 'manual'
using it inside componentDidMount() inside the _app.js file.
However, after some minutes i removed it and the problem seems that has gone.
Strange thing, by using and removing it, it seems that it also fix the issue in a build that is deployed with now. Maybe is only a browser issue?
I came across mentioned issue while building my portfolio site and I think this is really a big breaking thing in user experience overall.
I can confirm that problem occurs when the page to go back has more height than the current page. And (IMHO) this has nothing to do with getInitialProps as was stated in official next team response. I get similar bad result with or without using getInitialProps . More likely it happens because scroll restoration occurs before route change but not after it. Try to set simple page transitions and you'll see that scroll height jumps instantly despite the content of current page is still there.
In my case I found temporary solution. This involves setting the height of "html" tag to a very high value (like 15000px) on onRouteChangeStart event, and restoring its original height in 2 seconds after onRouteChangeComplete. This works for me and hope it will help someone until we get official fix. Good luck.
My solution for restoring scroll position when the browser back button clicked:
_app.js
componentDidMount() {
const cachedPageHeight = []
const html = document.querySelector('html')
Router.events.on('routeChangeStart', () => {
cachedPageHeight.push(document.documentElement.offsetHeight)
})
Router.events.on('routeChangeComplete', () => {
html.style.height = 'initial'
})
Router.beforePopState(() => {
html.style.height = `${cachedPageHeight.pop()}px`
return true
})
}
@hardwit well done!
however it works for me only if I restore height asynchronously like this:
Router.events.on("routeChangeComplete", () => {
setTimeout(() => {
html.style.height = 'initial'
}, 1500);
});
My solution for restoring scroll position when the browser back button clicked:
_app.js
componentDidMount() { const cachedPageHeight = [] const html = document.querySelector('html') Router.events.on('routeChangeStart', () => { cachedPageHeight.push(document.documentElement.offsetHeight) }) Router.events.on('routeChangeComplete', () => { html.style.height = 'initial' }) Router.beforePopState(() => { html.style.height = `${cachedPageHeight.pop()}px` return true }) }
Where does Router come from?
This worked for me:
_app.js
componentDidMount() {
window.history.scrollRestoration = "manual";
const cachedScroll = [];
Router.events.on("routeChangeStart", () => {
cachedScroll.push([window.scrollX, window.scrollY]);
});
Router.beforePopState(() => {
const [x, y] = cachedScroll.pop();
setTimeout(() => {
window.scrollTo(x, y);
}, 100);
return true;
});
}
For me, the above didn't work. I'm not sure if it was related to the fixed timeout, but beforePopState
didn't trigger at the scroll at the right time.
What I ended up with was storing a potential scroll and then triggering it in onRouteChangeComplete
.
componentDidMount() {
window.history.scrollRestoration = "manual";
const cachedScrollPositions = [];
let shouldScrollRestore = false;
Router.events.on("routeChangeStart", () => {
cachedScrollPositions.push([window.scrollX, window.scrollY]);
});
Router.events.on("routeChangeComplete", () => {
if (shouldScrollRestore) {
const { x, y } = shouldScrollRestore;
window.scrollTo(x, y);
shouldScrollRestore = false;
}
});
Router.beforePopState(() => {
const [x, y] = cachedScroll.pop();
shouldScrollRestore = { x, y };
return true;
});
}
I found some issues with window.history.scrollRestoration = "manual"
on moving through few pages and then when we returning back and reloading page - it's jumping through the top to previous location.
So i found solution to swithing window.history.scrollRestoration
on different window lifeCycles:
_app.tsx
componentDidMount() {
window.history.scrollRestoration = 'auto';
const cachedScrollPositions: number[][] = [];
let shouldScrollRestore: { x: number, y: number };
Router.events.on('routeChangeStart', () => {
cachedScrollPositions.push([window.scrollX, window.scrollY]);
});
Router.events.on('routeChangeComplete', () => {
if (shouldScrollRestore) {
const { x, y } = shouldScrollRestore;
window.scrollTo(x, y);
shouldScrollRestore = null;
}
window.history.scrollRestoration = 'auto';
});
Router.beforePopState(() => {
if (cachedScrollPositions.length > 0) {
const [x, y] = cachedScrollPositions.pop();
shouldScrollRestore = { x, y };
}
window.history.scrollRestoration = 'manual';
return true;
});
}
I'm curious why people are still trying to manually scroll Next.js pages. @arunoda gave a clear explanation of the problem here: https://github.com/zeit/next.js/issues/3303#issuecomment-376804494
It's because getInitialProps runs even when someone clicks on the browser back button. This means it'll try to grab fresh data. But the browser expects the data to be there already. Just cache any remote data from getInitialProps in the client and the problem should go away. I gave an example here: https://levelup.gitconnected.com/6-tips-using-next-js-for-your-next-web-app-e3f056fa46
I added this code and it works perfect for me
`componentDidMount() {
if ('scrollRestoration' in window.history) {
window.history.scrollRestoration = 'manual';
const cachedScrollPositions = [];
let shouldScrollRestore;
Router.events.on('routeChangeStart', () => {
cachedScrollPositions.push([window.scrollX, window.scrollY]);
});
Router.events.on('routeChangeComplete', () => {
if (shouldScrollRestore) {
const { x, y } = shouldScrollRestore;
window.scrollTo(x, y);
shouldScrollRestore = false;
}
});
Router.beforePopState(() => {
const [x, y] = cachedScrollPositions.pop();
shouldScrollRestore = { x, y };
return true;
});
}
}`
@pavlo-vasylkivskyi-scx thank you!
I little bit changed your code for using with functional component.
import Router from 'next/router'
let cachedScrollPositions = [];
const Home = props => {
useEffect(() => {
if ('scrollRestoration' in window.history) {
window.history.scrollRestoration = 'manual';
let shouldScrollRestore;
Router.events.on('routeChangeStart', () => {
cachedScrollPositions.push([window.scrollX, window.scrollY]);
});
Router.events.on('routeChangeComplete', () => {
if (shouldScrollRestore) {
const { x, y } = shouldScrollRestore;
window.scrollTo(x, y);
shouldScrollRestore = false;
}
});
Router.beforePopState(() => {
const [x, y] = cachedScrollPositions.pop();
shouldScrollRestore = { x, y };
return true;
});
}
}, []);
return (...long list of elements with links)
}
Also I cached my list like this https://levelup.gitconnected.com/6-tips-using-next-js-for-your-next-web-app-e3f056fa46. Thank you @jlei523
@pavlo-vasylkivskyi-scx Thank you!
But an error occurred when I refreshed the page and click browser back button.
TypeError: Invalid attempt to destructure none-iterable instance
so I changed your code.
componentDidMount() {
if ("scrollRestoration" in window.history) {
window.history.scrollRestoration = "manual";
const cachedScrollPositions: Array<any> = [];
let shouldScrollRestore;
Router.events.on("routeChangeStart", () => {
if (!shouldScrollRestore) { // <---- Only recording for history push.
cachedScrollPositions.push([window.scrollX, window.scrollY]);
}
});
Router.events.on("routeChangeComplete", () => {
if (shouldScrollRestore) {
const { x, y } = shouldScrollRestore;
window.scrollTo(x, y);
shouldScrollRestore = false;
}
});
Router.beforePopState(() => {
if (cachedScrollPositions.length > 0) { // <---- Add this !
const [x, y] = cachedScrollPositions.pop();
shouldScrollRestore = { x, y };
}
return true;
});
}
}
I've been working for a whole week to find a reliable solution to this problem. I couldn't fine one which:
1) works reliably with both Firefox and Chrome.
2) works reliably using both the back-button and the forward-button across your website.
3) works reliably when you hit the back-/forward-button coming from an external website.
4) works reliably even if you hit the refresh button on one of your pages during the session.
5) works reliably even if you hammer the back-button or the forward-button multiple times.
I tried various approaches which I found across the web. I tried with different approaches of my own. Not a single one worked reliably. The approach which took me the farthest was one which used scrollRestoration = 'manual'
and sessionStorage
to keep track of the position history. It worked flawlessly except for 3.
.
I'm wondering, has anyone found a solution which works for all items from the list above?
I had this requirement too on an actual project and I came up with the following solution:
https://gist.github.com/schmidsi/2719fef211df9160a43808d505e30b4e
It does not address all points from @feluxe, but it works pretty well for us. Maybe this can help someone as a boilerplate for their own solution.
Here's my take:
https://gist.github.com/claus/992a5596d6532ac91b24abe24e10ae81
It should satisfy all of @feluxe's requirements and behave like native scrollRestoration, although i haven't tested it too much yet. I ran it through Chrome, Firefox and Safari and it worked fine. YMMV if you do out of the ordinary stuff with the router. One caveat is that sometimes scrollTo seems to come a tiny bit too late so you see the top of the page flash for a frame or so.
Caching helped with getInitialProps, since it run client side. Now with getServerSideProps the issue is back.
Caching helped with getInitialProps, since it run client side. Now with getServerSideProps the issue is back.
Yep, when I use getServerSideProps
, scroll restoration happens and after that the page gets rendered.
Im seeing this issue too with getServerSideProps, the scroll restoration happens too early
@Timer
I think we need more notice on this issue, getServerSideProps
would cause scroll restoration happens too early.
The flikering causes very bad user experience, especially on mobile platform.
I came across the same issue today. It is really frustrating that Next.js doesn't have a baked in solution for this problem.
@hardwit's solution works for me.
This is a UX killer that many developers didn't notice.
Yes, there should be more notice on this issue for all Next.js users or atleast attach this thread in the common issues so people know the solutions in advance.
Thanks @hardwit for starting all the solutions and it solves for me now.
Also came across this problem, thank you everyone for the possible solutions.
This definitely should be officially addressed, huge impact in UX and may even be a necessity for a lot of use cases.
Most helpful comment
My solution for restoring scroll position when the browser back button clicked:
_app.js