When using Link
for routing and then pressing the browser's back button, the page will not remember the previous scroll position.
For example, go from page1
to page2
and then click the back button. page1
is rendered at the top of the viewport.
The browser is keeping track of the scroll position because if the page is refreshed it will refresh at the appropriate position. Is this an issue with the History API?
It might have to do with the latency of getting the data? I've been wanting to experiment with passing popStateEvent
(the event from the popstate
event) to getInitialProps
as a way of letting the user optionally load the data from a cache that they maintain
Hmm, do you mean when using prefetch? I tried both with and without prefetch and the issue was still there.
I'm not sure how data latency might affect the scroll position of the viewport when going back a page. Do you mind explaining?
I was assuming that the browser has a heuristic for how long to wait for a scroll height to become available to scroll to.
Because, keep in mind that the popstate event is _optimistic_. We can't block the location from changing, it changes immediately (this is different from how push is handled).
Therefore, you press back, we start fetching data, that might take N milliseconds. I was assuming that browsers give up on preserving scroll if that N is sufficiently large.
Ah, I see. That sounds reasonable. What is the difference between clicking the back button or refreshing the page, though? Does clicking the back trigger the History API and you listen for that in order to fetch the page's data?
When the location changes immediately does it do another render server side, or is it all taken care of in the client? If it's done in the client then perhaps the browser might try to scroll before React mounts the component tree (when the viewport has no height). Which would make sense then that when you do a full refresh it works (the full refresh goes to the server and comes back with the entire markup).
I just took a look at what was going on behind the scenes and it does look that there are no network requests (other than static assets) when going back a page. This definitely sounds like it would make everything go much faster but it comes at the cost of losing the scroll position. Is there any way to avoid this?
At the time we always fetch data fresh. I'm thinking that if we expose said event to getInitialProps
, we can write a decorator for _fast back_:
export default withPopstateCache(class extends React.Component {
static async getInitialProps () {
cont res = await fetch('api.com/this/will/be/cached/when/you/press/back')
return res.json()
}
})
but anyway, my rant could just be unrelated to the scroll position bug :P
Haha, I think that might work. Maybe also a way to simply skip the History API? So perhaps a way to not listen for the browser's back button and allow to set this as an option?
Does anyone know why is it that when I added in this code the scroll position retains when I go from page1 to page2 and then click the back button:
componentDidMount () {
console.log(document.body.scrollTop);
}
@stacygohyunsi You can use scroll
prop on link component for your use case like this <Link href="/path" scroll />
Ref: https://github.com/zeit/next.js/blob/master/lib/link.js#L55-L69
@vinaypuppal is there a way to achieve this while using router.push?
@coluccini Yes
Router.push('/path').then(() => window.scrollTo(0, 0))
Oh, I didn't know that router return a promise :) Thanks!
Currently this is not implemented with Link. But if someone could send a PR I'd happy to take it.
For now, your Router.push() with your own logic.
May be someone could create a package like: next-scroll-link
or something.
@migueloller I don't think I quite understood the solution @rauchg suggested. The premise is to cache the data on client-side to expedite rendering speed? Did you ever figure out a solution?
Also, @arunoda, you suggested someone could make a PR. Do you have any pointers as to which files or folders I should look at? I would love to make that contribution.
Currently, this is how I'm persisting scroll position with a route listener instance:
function initRouterListeners () {
const scrollPositions = []
let hasCompleted = false
Router.onRouteChangeStart = () => {
hasCompleted = false
scrollPositions.push(window.scrollY)
}
Router.onRouteChangeComplete = path => {
hasCompleted = true
}
window.onpopstate = popEvent
function popEvent (e) {
// sometimes this event fires first
// and needs to wait for onRouteChangeComplete
if (hasCompleted) {
scrollTo()
} else {
setTimeout(popEvent, 20)
}
}
function scrollTo () {
// pop 2 and scroll to that
scrollPositions.pop()
const scrollPosition = scrollPositions.pop()
if (scrollPosition) {
window.requestAnimationFrame(() => window.scrollTo(0, scrollPosition))
}
}
}
This function is called when I bootstrap client. This only works sometimes because data may take too long to load and the browser will scroll to top by default.
I would like a way to persist the scroll position without having to optimizing my render function (I mean I could, but I want to know if there's a functional way of solving this issue).
@sojungko I think it's better to implement this as a user package. What you've done is correct.
Try to do the scrolling updates inside onRouteChangeComplete
.
That'll be called when we completed getInitialProps
(loads data).
May be we need to delay a bit as well. (Allow the browser to render data).
You can also listen to router events like this: https://github.com/zeit/next.js/blob/canary/client/on-demand-entries-client.js#L8
That's a best suited method for a package like this.
Hi @sojungko , can you please tell me where I should execute that function?
EDIT: I think I found out, that the place to run that code is the _app.js
. I didn't know about this because I just discovered this framework.
Also, did you find a better solution? Thanks!
@arunoda About your last comment, how do we know in onRouteChangeComplete
that the back button was clicked and we're not just going to another page?
Or, is there a better way to handle this issue now?
@fmaylinch did you find the right way to solve this? i m struggling with this problem too since several days. Any help will be greatly appreciated.
Sorry @karanmartian, I saw your message and forgot to reply. Today I found your message again.
I am not working on the project now, but I did this hack to solve it. This code is in my _app.js
file, but outside of my App class.
export default class MyApp extends App {
....
}
initRouterListeners();
function initRouterListeners() {
console.log("Init router listeners");
const routes = [];
Router.events.on('routeChangeStart', (url) => {
pushCurrentRouteInfo();
});
Router.events.on('routeChangeComplete', (url) => {
fixScrollPosition();
});
// Hack to set scrollTop because of this issue:
// - https://github.com/zeit/next.js/issues/1309
// - https://github.com/zeit/next.js/issues/3303
function pushCurrentRouteInfo() {
routes.push({pathname: Router.pathname, scrollY: window.scrollY});
}
// TODO: We guess we're going back, but there must be a better way
// https://github.com/zeit/next.js/issues/1309#issuecomment-435057091
function isBack() {
return routes.length >= 2 && Router.pathname === routes[routes.length - 2].pathname;
}
function fixScrollPosition () {
let scrollY = 0;
if (isBack()) {
routes.pop(); // route where we come from
const targetRoute = routes.pop(); // route where we return
scrollY = targetRoute.scrollY; // scrollY we had before
}
console.log("Scrolling to", scrollY);
window.requestAnimationFrame(() => window.scrollTo(0, scrollY));
console.log("routes now:", routes);
}
}
Hi, I hope this helps someone.
My requirements:
back
or forward
button clicked)Link
component)My solution:
// inside _app.tsx
scrollPositionRestorer()
function scrollPositionRestorer() {
const scrollMemories: { [asPath: string]: number } = {};
let isPop = false;
if (process.browser) {
window.history.scrollRestoration = 'manual';
window.onpopstate = () => {
isPop = true;
};
}
Router.events.on('routeChangeStart', () => {
saveScroll();
});
Router.events.on('routeChangeComplete', () => {
if (isPop) {
restoreScroll();
isPop = false;
} else {
scrollToTop();
}
});
function saveScroll() {
scrollMemories[Router.asPath] = window.scrollY;
}
function restoreScroll() {
const prevScrollY = scrollMemories[Router.asPath];
if (prevScrollY !== undefined) {
window.requestAnimationFrame(() => window.scrollTo(0, prevScrollY));
}
}
function scrollToTop() {
window.requestAnimationFrame(() => window.scrollTo(0, 0));
}
}
Fantastica!!! Tysm @Shota :))
On Fri, 11 Sep 2020 at 2:05 PM Shota Tamura notifications@github.com
wrote:
>
>
Hi, I hope this helps someone.
My requirements:
Restore scroll position when pop (when back or forward button
clicked)Scroll to top otherwise (For example, on the first load or when
navigating with the Link component)My solution:
// inside _app.tsx
scrollPositionRestorer()
function scrollPositionRestorer() {
const scrollMemories: { [asPath: string]: number } = {};
let isPop = false;
if (process.browser) {
window.history.scrollRestoration = 'manual'; window.onpopstate = () => { isPop = true; };
}
Router.events.on('routeChangeStart', () => {
saveScroll();
});
Router.events.on('routeChangeComplete', () => {
if (isPop) { restoreScroll(); isPop = false; } else { scrollToTop(); }
});
function saveScroll() {
scrollMemories[Router.asPath] = window.scrollY;
}
function restoreScroll() {
const prevScrollY = scrollMemories[Router.asPath]; if (prevScrollY !== undefined) { window.requestAnimationFrame(() => window.scrollTo(0, prevScrollY)); }
}
function scrollToTop() {
window.requestAnimationFrame(() => window.scrollTo(0, 0));
}
}
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/vercel/next.js/issues/1309#issuecomment-690957041,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/ACPW73AKDT3BKMAXEQ5RQS3SFHOMFANCNFSM4DBW65WQ
.--
Karan Kamdar
CEO, 1 Martian Way Industries Pvt. Ltd
https://1martianway.com
Founder and President, IDRL
https://droneracingindia.com
Stanford Ignite Fellow ‘15,
Stanford Graduate School of Business
M.S. Robotics
Cell: +91-8850-227233
LinkedIn: https://www.linkedin.com/in/karankamdar/
Most helpful comment
@migueloller I don't think I quite understood the solution @rauchg suggested. The premise is to cache the data on client-side to expedite rendering speed? Did you ever figure out a solution?
Also, @arunoda, you suggested someone could make a PR. Do you have any pointers as to which files or folders I should look at? I would love to make that contribution.
Currently, this is how I'm persisting scroll position with a route listener instance:
This function is called when I bootstrap client. This only works sometimes because data may take too long to load and the browser will scroll to top by default.
I would like a way to persist the scroll position without having to optimizing my render function (I mean I could, but I want to know if there's a functional way of solving this issue).