Next.js: Router.push('/link') don't scroll top of the page when triggered

Created on 7 Nov 2017  路  25Comments  路  Source: vercel/next.js

Hello,

maybe I am missing something, but when I use this function and redirect to another page scroll doesn't reset to top but the page loads scrolled to middle, I know why this issue happens, but can I somehow fix it with some flag like JAVASCRIPT Router.pusht({pathname: '/link', scrollreset: true}) or something like this, didn't find anything similar in documentation

Most helpful comment

I agree there should be a scrollreset option. If you don't want to do it in componentDidMount you can also do Router.push('/link').then(() => window.scrollTo(0, 0));.

All 25 comments

whether it should scroll to top or not is decided by the user. I think you can do it in componentDidMount

I agree there should be a scrollreset option. If you don't want to do it in componentDidMount you can also do Router.push('/link').then(() => window.scrollTo(0, 0));.

Router.push('/link').then(() => window.scrollTo(0, 0)); this is the right way to do it, feel free to add it to the readme @gragland 鉂わ笍

Thanks @gralagland your solution seems not so ugly, but it would be nice to have such option to keep DRY principe. The only fast solution for now is to make live template to write this 馃槄

Woaw... You mean I need to review every single page to add .then()! By the way, I'm using next-routes, so is there a way to add this globally, just to add this code once, like addRequestTransform if you need to add some headers before calling an API.

@MBach you can also hook window.scrollTo(0, 0) on componentDidMount of the traget component.

I just want to chime in and say I found this behavior extremely surprising. Been building production sites with Next.js for almost a year now and only just now discovered that Router.push wasn't returning to the top 鈥撀營 always thought Router.push behaved exactly like <Link> but was just the programmatic way to do it.

And in terms of matching default behavior like <a> vs. <Link>, the non-SPA equivalent of Router.push would seem to be setting window.location which also returns you to the top of the page, no? It would make more sense as the default behavior to me.

@gragland it doesn't work

To make it work globally you can use the built-in next.js router event listener: Router.events.on('routeChangeComplete', () => { window.scrollTo(0, 0); });.
Just place this in a component shared across all pages e.g. the header.

@timneutkens Is there a reason why the default scroll behavior for the imperative Router.push and <Link> are different? Seems like an obvious bug to me that they behave differently.

Router.back() doesn't return promise, but has the same issue, what to do, manually scrolling up?

@alexsenichev Did you try @macmenak solution?

<Link href="/product/[id]" as={/product/${item.id}} beforePopState={()=>{ window.screenTop = 0; }}> <a> <h6>{item.name}</h6> </a> </Link>

Hi. Why would we have the Link component default scroll to top true but Router.push not?

I think this issue should be revisited.

Sorry but tagging the contributors: @liweinan0423 @timneutkens @exogen @goldenshun

It would be nice if there was an option on router.push() to force scroll to top, but I don't think it should be default as it would be a breaking change for everyone already assuming it does not do this.

@markjackson02 I don't see how this would be a breaking change since this is the default way browser would have handle this. I do not agree with the fact that we have two different approaches for the same functionality.

It would definitely be a breaking change. People have been building apps with the way it is now and expect it to function that way.

I'm not against them both having the same default functionality but I don't think it's worth the migration effort.

Anyway, this issue is closed so we should open a new issue with whatever we want to do.

@markjackson02 OMHO, the breaking change is that it's not working that way yet! The wrong behavior's already made. So I think it would be a good idea to put it the way that it should. But I agree with you, contributors will probably not give attention here.

@Timer @timneutkens Also, got bitten by the difference in behavior of Link and Router.push. FWIW, +1 to adding to the options of Router.push as that would keep it backward compatible.

You can use this to route and scroll to the top of the page:

// route using nextjs router and scroll to the top of the page
const routeToTop = (router, ...args) => {
    if(typeof window !== 'undefined') window.scrollTo(0, 0)
    return router.push.apply(router, args)
}

// usage

const router = useRouter()

routeToTop(router, '/my/page')

routeToTop(router, '/articles/[slug]', `/articles/${article.slug}`)

routeToTop(router, {
    pathname: '/search',
    query: {q: searchQuery},
})

Btw, this isn't as nice of a user experience as the Link behavior, since you scroll to the top of the page immediately instead of when the next page loads.

I added a related issue here for those interested: https://github.com/vercel/next.js/issues/15206

FWIW, here's my take on a workaround that was shared in a related issue.

import { useRouter } from 'next/router';
import { useEffect } from 'react';

/**
 * React hook that forces a scroll reset to a particular set of coordinates in the document
 * after `next/router` finishes transitioning to a new page client side. It smoothly scrolls back to
 * the top by default.
 *
 * @see https://github.com/vercel/next.js/issues/3249
 * @see https://github.com/vercel/next.js/issues/15206
 * @see https://developer.mozilla.org/en-US/docs/Web/API/ScrollToOptions
 * @param {ScrollOptions} [options={}] Hook options.
 * @param {ScrollBehavior} [options.behavior='smooth'] Specifies whether the scrolling should animate smoothly,
 *  or happen instantly in a single jump.
 * @param {number} [options.left=0] Specifies the number of pixels along the X axis to scroll the window.
 * @param {number} [options.top=0] Specifies the number of pixels along the Y axis to scroll the window.
 */
function useRouterScroll({ behavior = 'smooth', left = 0, top = 0 } = {}) {
  const router = useRouter();
  useEffect(() => {
    // Scroll to given coordinates when router finishes navigating
    // This fixes an inconsistent behaviour between `<Link/>` and `next/router`
    // See https://github.com/vercel/next.js/issues/3249
    const handleRouteChangeComplete = () => {
      window.scrollTo({ top, left, behavior });
    };

    router.events.on('routeChangeComplete', handleRouteChangeComplete);

    // If the component is unmounted, unsubscribe from the event
    return () => {
      router.events.off('routeChangeComplete', handleRouteChangeComplete);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [behavior, left, top]);
}

export default useRouterScroll;

Use it somewhere at the top of your component tree (_app.js works).

// _app.js
function MyApp({ Component, pageProps }) {
  // Make sure pages scroll to the top after we navigate to them using `next/router`
  useRouterScroll();

  /* ... */
}

export default MyApp;

@nfantone when does these fields change? behavior, left, top. On back and forward doesn't work for me.

@rakeshshubhu This only affects navigation through next/router.

Woaw... You mean I need to review every single page to add .then()! By the way, I'm using next-routes, so is there a way to add this globally, just to add this code once, like addRequestTransform if you need to add some headers before calling an API.

You don't have to. I instead create one module and export it to those components which need it. So once it needs change you change that one single place.

Also you notice am passing the router as param since you can only use React Hooks e.g. useRouter() inside a component

export const navigate = (event, url, router) => {
  event.preventDefault();
  event.stopPropagation();

  // Force scrolling to top
  router.push(url).then(() => window.scrollTo(0, 0));
};
Was this page helpful?
0 / 5 - 0 ratings