next.js icon indicating copy to clipboard operation
next.js copied to clipboard

Scroll restoration happens too early before the page gets rendered after hitting browser back button

Open liweinan0423 opened this issue 7 years ago • 63 comments

  • [x] I have searched the issues of this repository and believe that this is not a duplicate.

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.

Demo

source code can be found here out

After clicking back button, the "go back" text should still be visible (not scroll down to previous position) until the previous page gets rendered

liweinan0423 avatar Nov 17 '17 14:11 liweinan0423

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'

pablopunk avatar Dec 28 '17 04:12 pablopunk

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 [email protected] wrote:

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 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.

liweinan0423 avatar Dec 28 '17 05:12 liweinan0423

Yeah, I understand. I'm not sure what the solution would be from within next.

pablopunk avatar Dec 28 '17 23:12 pablopunk

@liweinan0423 I'm not sure but this could be a workaround

pablopunk avatar Jan 04 '18 22:01 pablopunk

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.

raffij avatar Feb 15 '18 14:02 raffij

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.

mgiraldo avatar Mar 06 '18 23:03 mgiraldo

here's an example screen capture of the issue we're having:

scroll

mgiraldo avatar Mar 07 '18 20:03 mgiraldo

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?

jlei523 avatar Mar 13 '18 00:03 jlei523

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 <Link> 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?

spencersmb avatar Mar 13 '18 01:03 spencersmb

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

mgiraldo avatar Mar 13 '18 02:03 mgiraldo

you can see the patched version here: https://beta-staging.dp.la/exhibitions

mgiraldo avatar Mar 13 '18 02:03 mgiraldo

@mgiraldo I see a slight flash when using the back button. We're running into the same issue.

jlei523 avatar Mar 13 '18 03:03 jlei523

@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 avatar Mar 13 '18 05:03 spencersmb

@spencersmb What do you mean switch to using the tag? What tag is that?

jlei523 avatar Mar 28 '18 06:03 jlei523

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:

  • Cache the data locally and render the page from that cache. You can even update the data in behind the scene. In this case data will be there as the user hit the back button.
  • Control scrolling manually via 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.

arunoda avatar Mar 28 '18 08:03 arunoda

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 avatar Mar 28 '18 14:03 developer239

@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.

jlei523 avatar Mar 28 '18 20:03 jlei523

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.

mgiraldo avatar Mar 29 '18 02:03 mgiraldo

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.

rus-yurchenko avatar Nov 13 '18 09:11 rus-yurchenko

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?

Emiliano-Bucci avatar Nov 26 '18 08:11 Emiliano-Bucci

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.

vitaliy-webmaster avatar Apr 13 '19 22:04 vitaliy-webmaster

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 avatar Jul 01 '19 13:07 hardwit

@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); });

vitaliy-webmaster avatar Jul 03 '19 08:07 vitaliy-webmaster

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?

malthemilthers avatar Aug 21 '19 09:08 malthemilthers

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;
    });
  }

daniel-primea avatar Sep 07 '19 00:09 daniel-primea

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;
    });
  }

bnhovde avatar Sep 26 '19 11:09 bnhovde

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;
    });
  }

nickmurr avatar Sep 28 '19 08:09 nickmurr

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

jlei523 avatar Sep 29 '19 04:09 jlei523

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;
      });
    }
  }`

ghost avatar Nov 13 '19 10:11 ghost

@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

vitalyliber avatar Dec 06 '19 13:12 vitalyliber

@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;
        });
    }
}

kimnagui avatar Jan 31 '20 08:01 kimnagui

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?

feluxe avatar Mar 11 '20 18:03 feluxe

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.

schmidsi avatar Apr 22 '20 12:04 schmidsi

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.

claus avatar May 14 '20 05:05 claus

Caching helped with getInitialProps, since it run client side. Now with getServerSideProps the issue is back.

alekslario avatar Jul 22 '20 20:07 alekslario

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.

onderonur avatar Aug 11 '20 23:08 onderonur

Im seeing this issue too with getServerSideProps, the scroll restoration happens too early

callumbooth avatar Oct 27 '20 17:10 callumbooth

@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.

wddwycc avatar Oct 28 '20 13:10 wddwycc

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.

OnurErtugral avatar Oct 30 '20 10:10 OnurErtugral

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.

atirudom avatar Nov 04 '20 07:11 atirudom

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.

igormartimiano avatar Nov 16 '20 20:11 igormartimiano

Tested all scripts in this issue and always get problems. I am trying to use this module and it works well for now: https://github.com/moxystudio/next-router-scroll

meotimdihia avatar Jan 10 '21 14:01 meotimdihia

This behavior should've been fixed on Next.js 10.0.5+.

Timer avatar Jan 11 '21 13:01 Timer

@Timer tried upgrade to 10.0.5, the problem is still there.

wddwycc avatar Jan 11 '21 13:01 wddwycc

I just tested with 10.0.5 again. Scroll restoration still random, even if it works, the position is not the same as the last position. My pages almost render on the client or render after fetch data on the client.

meotimdihia avatar Jan 11 '21 13:01 meotimdihia

Has anyone solved this problem? Applying @claus code causes the same problem.

JinDevT avatar Jan 21 '21 02:01 JinDevT

It has already been implemented here. https://demo.vercel.store/ why is it so hard to provide a solution. This moxystudio guy is the best https://github.com/moxystudio/next-router-scroll

ynsfasttoday avatar Jan 30 '21 17:01 ynsfasttoday

It has already been implemented here. https://demo.vercel.store/ why is it so hard to provide a solution. This moxystudio guy is the best https://github.com/moxystudio/next-router-scroll

I tried this package but found some issues, can you share any repo where you might have used this package and it worked?

PixeledCode avatar Feb 13 '21 13:02 PixeledCode

I used next-router-scroll on the site: https://animek.fun/

meotimdihia avatar Feb 13 '21 13:02 meotimdihia

I used next-router-scroll on the site: https://animek.fun/

great site, I just bookmarked it lol. can you share the part of code where you used the the package?

PixeledCode avatar Feb 13 '21 13:02 PixeledCode

I can't share the code but I used use it with disableNextLinkScroll option:

              <RouterScrollProvider disableNextLinkScroll={false}>
                <Layout>
                  <Component {...pageProps} />
                </Layout>
              </RouterScrollProvider>

meotimdihia avatar Feb 13 '21 13:02 meotimdihia

edit: Got it working by using @hardwit solution

i'm using react-query and forgot to configure staleTime. In my case the problem was that everytime I clicked on router.back() the data got refetched with a loading state

Edjevw12 avatar Mar 04 '21 21:03 Edjevw12

moxystudio/next-router-scroll not working anymore.


  const [mounted, setMounted] = useState(false)
  useEffect(() => 
     setMounted(true)
  }, [])

return (
  renderFullScreenHeight() // scroll restoration was stopped at here
  {mounted && renderSomething()}
)

when clicking back on the browser, it won't scroll to the mounted position.


I fixed it with this code:

  const mounted = typeof window !== 'undefined'

  • getServerSideProps still have the problem.

My latest solution:

let mounted = false
export default function Loading() {
  useEffect(() => {
    mounted = true
  }, [])
  
  return (
      {mounted && <div>Run on client only</div>}
  )
 }

2021-05-15

This is my latest code. it takes me months to figure out, and it solves all other problems:

  • Scroll restoration when fetching data on the client.
  • https://github.com/vercel/next.js/discussions/17443#discussioncomment-739561
  • To avoid re-render pages when the user goes back.
import { useEffect, useRef } from "react"
import { useUpdate } from "react-use"


export default function useMounted() {
  const mounted = useRef(false)
  const update = useUpdate()
  useEffect(() => {
    if (mounted.current == false) {
      mounted.current = true
      update()
    }
  }, [update])
  return mounted.current
}

And in combine with configs:

module.exports = {
  experimental: {
    scrollRestoration: true
  }
}

It looks complicated but solved scroll restoration bugs. 😀

meotimdihia avatar Mar 15 '21 00:03 meotimdihia

In my case I've fixed it with enabling experimental scrollRestoration by changing next.config.js to

module.exports = {
  experimental: {
    scrollRestoration: true
  }
}

It's mentioned also here https://github.com/vercel/next.js/pull/22727#issuecomment-803276977

filippofilip95 avatar Mar 28 '21 10:03 filippofilip95

To set true to scrollRestoration worked well with Safari, but did not work with Chrome (in iOS). :cry:

satoyan avatar Apr 26 '21 01:04 satoyan

as @filippofilip95 mentioned, changing the next.config.js to include the scrollRestoration expermental flag solved my issue on iOS 14.4.

micksabox avatar May 03 '21 02:05 micksabox

@claus I'm using your solution in conjunction with a page transition approach (with framer motion), and I'm noticing that exclusively on iOS Chrome, sporadically during history changes that the new scroll position gets applied immediately. It seems like Next's routeChangeComplete event is firing too early on history changes, my setTimeout isn't being respected, or something else is going on.

I'm not sure why this is only happening on history changes with iOS Chrome. Has anyone else experienced this?

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.

ndimatteo avatar Sep 10 '21 16:09 ndimatteo

@ndimatteo In a current project i also ran into something like that (consistently in all browsers though). Something is setting scroll position immediately on click on a Link, even though i do the {scroll: false} thing. Weird thing is that this happens only when i'm scrolled down far enough, and it sets the position to something seemingly random (not really random, but also nothing that would make sense to me). I tried to figure out what exactly it is that's setting scroll position but i didn't find anything. Pretty sure it isn't my code (scroll restoration only kicks in much later), and it's not a layout shift. This is driving me nuts.

claus avatar Sep 10 '21 18:09 claus

I'm my project, sometimes I can't even scroll down after changing page in development... if it was a production issue I'd switch framework.

lmf-git avatar Oct 03 '21 23:10 lmf-git

Hi ! It seems to still not work on nextjs.org with Chrome iOS when I click on footer links. I also tried with another projects based on Next 12 and got same issues. Have you any solutions ?

Tested with Chrome IOS (v.96.0.4664.101) on 15.1 and 14.7.

https://user-images.githubusercontent.com/63233026/146800065-a3398c1c-5d80-4ad4-98af-a02844e19629.MP4

veasna-ung avatar Dec 20 '21 16:12 veasna-ung

Welp, I just ran into this issue 2 days before I'm scheduled to ship my project... None of the solutions work. Nextjs has been awesome, but this bug really sucks. Any chance this has been looked at recently?

dejesus2010 avatar Jun 25 '22 01:06 dejesus2010

Welp, I just ran into this issue 2 days before I'm scheduled to ship my project... None of the solutions work. Nextjs has been awesome, but this bug really sucks. Any chance this has been looked at recently?

@dejesus2010 Trying to get this solved since last month but still didn't get any solution. Any help ?

abhimanyuPatil avatar Jun 25 '22 19:06 abhimanyuPatil

I'm having the same issue. I've added a setTimeout to delay the scroll, but this is obviously not a sustainable solution.

Kipitup avatar Aug 03 '22 08:08 Kipitup

next: 12.2.2 chrome: 105

module.exports = {
  experimental: {
    scrollRestoration: true
  }
}

this flag triggers replaceState on every scroll, it broke the browser back/forward buttons for me. I ended up doing something like this: (in the _app.tsx)

  const router = useRouter();
  const scrollCache = useRef<Record<string, [number, number]>>({});
  const activeRestorePath = useRef<string>();
  useEffect(() => {
    if (history.scrollRestoration !== "manual") {
      history.scrollRestoration = "manual";
    }
    const getCurrentPath = () => location.pathname + location.search;
    router.beforePopState(() => {
      activeRestorePath.current = getCurrentPath();
      return true;
    });
    const onComplete = () => {
      const scrollPath = activeRestorePath.current;
      if (!scrollPath || !(scrollPath in scrollCache.current)) {
        return;
      }
      activeRestorePath.current = undefined;
      const [scrollX, scrollY] = scrollCache.current[scrollPath];
      window.scrollTo(scrollX, scrollY);
      // sometimes rendering the page can take a bit longer
      const delays = [10, 20, 40, 80, 160];
      const checkAndScroll = () => {
        if (
          (window.scrollX === scrollX && window.scrollY === scrollY) ||
          scrollPath !== getCurrentPath()
        ) {
          return;
        }
        window.scrollTo(scrollX, scrollY);
        const delay = delays.shift();
        if (delay) {
          setTimeout(checkAndScroll, delay);
        }
      };
      setTimeout(checkAndScroll, delays.shift());
    };
    const onScroll = () => {
      scrollCache.current[getCurrentPath()] = [window.scrollX, window.scrollY];
    };
    router.events.on("routeChangeComplete", onComplete);
    window.addEventListener("scroll", onScroll);
    return () => {
      router.events.off("routeChangeComplete", onComplete);
      window.removeEventListener("scroll", onScroll);
    };
  }, []);

hope it helps

reza-akbari avatar Sep 07 '22 11:09 reza-akbari

next: 12.2.2 chrome: 105

module.exports = {
  experimental: {
    scrollRestoration: true
  }
}

this flag triggers replaceState on every scroll, it broke the browser back/forward buttons for me. I ended up doing something like this: (in the _app.tsx)

  const router = useRouter();
  const scrollCache = useRef<Record<string, [number, number]>>({});
  const activeRestorePath = useRef<string>();
  useEffect(() => {
    if (history.scrollRestoration !== "manual") {
      history.scrollRestoration = "manual";
    }
    const getCurrentPath = () => location.pathname + location.search;
    router.beforePopState(() => {
      activeRestorePath.current = getCurrentPath();
      return true;
    });
    const onComplete = () => {
      const scrollPath = activeRestorePath.current;
      if (!scrollPath || !(scrollPath in scrollCache.current)) {
        return;
      }
      activeRestorePath.current = undefined;
      const [scrollX, scrollY] = scrollCache.current[scrollPath];
      window.scrollTo(scrollX, scrollY);
      // sometimes rendering the page can take a bit longer
      const delays = [10, 20, 40, 80, 160];
      const checkAndScroll = () => {
        if (
          (window.scrollX === scrollX && window.scrollY === scrollY) ||
          scrollPath !== getCurrentPath()
        ) {
          return;
        }
        window.scrollTo(scrollX, scrollY);
        const delay = delays.shift();
        if (delay) {
          setTimeout(checkAndScroll, delay);
        }
      };
      setTimeout(checkAndScroll, delays.shift());
    };
    const onScroll = () => {
      scrollCache.current[getCurrentPath()] = [window.scrollX, window.scrollY];
    };
    router.events.on("routeChangeComplete", onComplete);
    window.addEventListener("scroll", onScroll);
    return () => {
      router.events.off("routeChangeComplete", onComplete);
      window.removeEventListener("scroll", onScroll);
    };
  }, []);

hope it helps

This does not work for me bro!

jsvelte avatar Sep 09 '22 00:09 jsvelte