Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Router.push('/link') don't scroll top of the page when triggered #3249

Closed
andylacko opened this issue Nov 7, 2017 · 34 comments · Fixed by #20606
Closed

Router.push('/link') don't scroll top of the page when triggered #3249

andylacko opened this issue Nov 7, 2017 · 34 comments · Fixed by #20606
Assignees
Milestone

Comments

@andylacko
Copy link

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

@liweinan0423
Copy link
Contributor

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

@gragland
Copy link

gragland commented Nov 8, 2017

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

@timneutkens
Copy link
Member

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 ❤️

@andylacko
Copy link
Author

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 😅

@MBach
Copy link

MBach commented Dec 11, 2017

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.

@tamer-mohamed
Copy link

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

@exogen
Copy link
Contributor

exogen commented Jun 16, 2018

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

@MHwishes
Copy link

@gragland it doesn't work

@macmenak
Copy link

macmenak commented Nov 7, 2018

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.

@seanconnollydev
Copy link
Contributor

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

@alexsenichev
Copy link

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

@Yonben
Copy link

Yonben commented Jan 14, 2019

@alexsenichev Did you try @macmenak solution?

@341
Copy link

341 commented Dec 13, 2019

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

@giovannipds
Copy 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

@remjx
Copy link

remjx commented Mar 29, 2020

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.

@giovannipds
Copy link

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

@remjx
Copy link

remjx commented Mar 30, 2020

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.

@giovannipds
Copy link

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

@oyeanuj
Copy link

oyeanuj commented Jun 4, 2020

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

@cullylarson
Copy link
Contributor

cullylarson commented Jul 17, 2020

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.

@oyeanuj
Copy link

oyeanuj commented Jul 18, 2020

I added a related issue here for those interested: #15206

@nfantone
Copy link

nfantone commented Aug 26, 2020

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;

@aomini
Copy link

aomini commented Sep 4, 2020

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

@nfantone
Copy link

nfantone commented Sep 4, 2020

@rakeshshubhu This only affects navigation through next/router.

@mutuadavid93
Copy link

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

@crisu83
Copy link

crisu83 commented Nov 26, 2020

Here's a simplified version of @nfantone's useRouterScroll hook above without any bells and whistles. 😅

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

export default function useRouterScroll() {
  const router = useRouter();
  useEffect(() => {
    const handler = () => {
      window.scrollTo(0, 0);
    };
    router.events.on('routeChangeComplete', handler);
    return () => {
      router.events.off('routerChangeComplete', handler);
    };
  });
}

@Timer Timer added this to the iteration 15 milestone Dec 8, 2020
@Timer Timer reopened this Dec 8, 2020
@Timer Timer modified the milestones: iteration 15, iteration 14 Dec 8, 2020
stefnnn added a commit to teachen-ch/voty that referenced this issue Dec 23, 2020
@Timer Timer added the point: 3 label Dec 30, 2020
@martpie
Copy link
Contributor

martpie commented Dec 30, 2020

@Timer would you accept a PR for adding a flag/parameter to router.push?

@Timer Timer self-assigned this Dec 30, 2020
Timer added a commit to Timer/next.js that referenced this issue Dec 30, 2020
This pull request makes `Router#push` and `Router#replace` function identically to `<Link />`, i.e. reset scroll when the new render is complete.

Users can opt out of this new behavior via:
```tsx
const path = '/my-page'
router.push(path, path, { scroll: false })
```

---

Fixes vercel#3249
@kodiakhq kodiakhq bot closed this as completed in #20606 Dec 30, 2020
kodiakhq bot pushed a commit that referenced this issue Dec 30, 2020
This pull request makes `Router#push` and `Router#replace` function identically to `<Link />`, i.e. reset scroll when the new render is complete.

Users can opt out of this new behavior via:
```tsx
const path = '/my-page'
router.push(path, path, { scroll: false })
```

---

Fixes #3249
@Timer
Copy link
Member

Timer commented Dec 30, 2020

This is fixed on next@canary!

@jleclanche
Copy link
Contributor

@Timer Hi, FYI this is not documented in https://nextjs.org/docs/api-reference/next/router#routerpush - Would be good to add there.

@talentlessguy
Copy link

talentlessguy commented Feb 4, 2021

Is it possible to make links work with new scroll option?

<Link scroll href="/about"><a>link</a></Link> seems to have no effect

@Joelgullander
Copy link

Is NOT fixed with 10.0.8-canary.15, atleast not when using a new basePath in next.config.js

@jmuzsik
Copy link

jmuzsik commented Mar 5, 2021

if you don't mind using router.push after it being instantiated with useRouter() you can simply do:

    const router = useRouter();
    // ... do stuff
    // page you want to go to, data you want to query, etc.
    router.push(
      // query params, URL, etc.
      null,
      { scroll: false }
    );

{scroll: false} is what prevents scroll to top.

@fazlulkarimweb
Copy link

fazlulkarimweb commented May 25, 2021

An old-school workaround from me. Just get the particular component in the view. Here's how. I use it with the router.push("/myaccount") for my peace of mind! I'm using react device detect. For some reason, in mobile browsers, it is not going to the top of the page after clicking router.push. The desktop browser is working fine. So, I wrapped my mobile menu bar Whatever happens, my component will be smoothly scrolled to the top! 😅

import React, { useEffect, useRef } from 'react';

function MyAccount() {
  const containerRef = useRef(null);

  useEffect(() => {
    setTimeout(() => {
      containerRef.current.scrollIntoView({ behavior: 'smooth' });
    }, 250);
  }, []);

  return (
    <div ref={containerRef}>
{/*   It will be your particular component that 
        needs to be scrolled to the top after users click to router.push 
       In my case, it's menubar.
*/}
        <MenuBar /> 
    </div>
  );
}

export default MyAccount;

@balazsorban44
Copy link
Member

This issue has been automatically locked due to no recent activity. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@vercel vercel locked as resolved and limited conversation to collaborators Jan 28, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.