27

When attempting shallow routing with a dynamic route in Next.js the page is refreshed and shallow ignored. Seems a lot of people are confused about this.

Say we start on the following page

router.push(
  '/post/[...slug]',
  '/post/2020/01/01/hello-world',
  { shallow: true }
);

Then we route to another blog post:

router.push(
  '/post/[...slug]',
  '/post/2020/01/01/foo-bar',
  { shallow: true }
);

This does not trigger shallow routing, the browser refreshes, why?

In the codebase its very clear that this is a feature:

// If asked to change the current URL we should reload the current page
// (not location.reload() but reload getInitialProps and other Next.js stuffs)
// We also need to set the method = replaceState always
// as this should not go into the history (That's how browsers work)
// We should compare the new asPath to the current asPath, not the url
if (!this.urlIsNew(as)) {
  method = 'replaceState'
}

I can achieve the same manually using window.history.pushState() although this would of course be a bad idea:

window.history.pushState({
  as: '/post/2020/01/01/foo-bar',
  url: '/post/[...slug]',
  options: { shallow: true }
}, '', '/post/2020/01/01/foo-bar');

As the internal API of Next.JS could change at any time... I may be missing something... but why is shallow ignored in the case? Seems odd.

AmerllicA
  • 29,059
  • 15
  • 130
  • 154
AndrewMcLagan
  • 13,459
  • 23
  • 91
  • 158
  • 1
    I am running into the same issue, I went for your solution for now, but I still feel like its a hack :/ – Avin A Good-time Chadee Jul 08 '20 at 02:24
  • 1
    there is an open question i posted around this over on the next github discussion section – AndrewMcLagan Jul 10 '20 at 07:58
  • The root source of the problem could be in the component or in the route to render it. Could you please share the code of your router and of your component? – Daniele Ricci Jul 10 '20 at 13:28
  • No, this is the way this feature works in NextJS. It is documented in the code. – AndrewMcLagan Jul 13 '20 at 05:31
  • Seems like it should only happen if dynamic routes are being treated as "new pages" by design as they mention in [caveats](https://nextjs.org/docs/routing/shallow-routing#caveats). Hard to say why this would be without official response but I'm guessing data requirements of dynamic routes can change a lot since they can potentially cover "catch all" cases. On a side note does `Router.push('/post/[...slug]', '/post/2020/01/01/foo-bar')` exhibit same behavior? – Divyanshu Maithani Jul 13 '20 at 15:37
  • 1
    Whether or not this is documented, it doesn't make a lot of sense if you're going to offer up dynamic routing as a major feature of your platform. This situation could easily be missed early on and then ran into much later. I love Next, but even after devving on it for 5+ years this is the first time I've ran into this issue. I will also be using the hack :) – Max Phillips Jan 05 '21 at 01:05

5 Answers5

18

I think this is the expected behavior because you are routing to a new page. If you are just changing the query parameters shallow routing should work for example:

router.push('/?counter=10', undefined, { shallow: true })

but you are using route parameters

router.push(
  '/post/[...slug]',
  '/post/2020/01/01/hello-world',
  { shallow: true }
);

That indicates you are routing to a new page, it'll unload the current page, load the new one, and wait for data fetching even though we asked to do shallow routing and this is mentioned in the docs here Shallow routing caveats.

By the way, you say "the page is refreshed" but router.push doesn't refresh the page even when used without shallow: true. It's a single page app after all. It just renders the new page and runs getStaticProps, getServerSideProps, or getInitialProps.

Ahmed Mokhtar
  • 2,388
  • 1
  • 9
  • 19
4

Shallow Routing Caveats

Shallow Routing gives you the ability to update pathname or query params without losing state i.e., only the state of route is changed.. But the condition is, you have to be on the same page(as shown in docs Caveats image).

For that you have to pass the second arg to router.push or Router.push as undefined. Otherwise, new page will be loaded after unloading the first page and you won't get the expected behavior.

I mean shallow routing will no longer be working in terms of pathname changing and that's because we chose to load a new page not only the url. Hope this helps

Example

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

// Current URL is '/'
function Page() {
  const router = useRouter()

  useEffect(() => {
    // Always do navigations after the first render
    router.push('/post/[...slug]', undefined, { shallow: true })
  }, [])

  useEffect(() => {
    // The pathname changed!
  }, [router.pathname ])
}

export default Page
DevLoverUmar
  • 11,809
  • 11
  • 68
  • 98
1

Okay, So this answer based on my first answer(and question clarification):

#1 Question was: This does not trigger shallow routing, the browser refreshes, why? => Check my last answer for understand how next.js dealing with file structures.

#2 If you want to deal with unlimited URL params:

You will have to follow this file structure:

.
├── pages
│   ├── post
│   │   └──[...slug].js // or [...slug]/index.js
│   ├── about.js
│   └── index.js
├── public
│   └── ...
├── .gitignore
├── package.json
├── package-lock.json
└── README.md

[...slug].js:

export default function Post({ post }) {
  return (
    <div>
      <h1>Post is here{}</h1>
      {JSON.stringify(post)}
    </div>
  );
}

export async function getStaticPaths() {
  return {
    paths: [
      { params: { slug: ["*"] } },
    ],
    fallback: true
  };
}
    
export async function getStaticProps(context) {

  console.log("SLUG: ", context);
  const { params = [] } = context || {};
  const res = await fetch(`https://api.icndb.com/jokes/${params && params.slug[0] || 3}`);
  const post = await res.json()
  return { props: { post } }
}

./index.js:

import Link from 'next/link';
import Router from 'next/router'

export default function Home() {

  const handleClick = e => {
    e.preventDefault();

    Router.push(
      '/post/[...slug]',
      '/post/2/2/4',
      { shallow: true }
    );
  }

  return (
    <div className="container">
      <div>
        <Link href="/about">
          <a>About</a>
        </Link>
      </div>
      <hr />
      <div>
        <Link
          href={`/post/[...slug]?slug=${2}`}
          as={`/post/${2}`}
        >
          <a>
            <span>
              Post:
              </span>
          </a>
        </Link>
        <hr />
        <div>
          <button onClick={handleClick}>
            Push me
            </button>
        </div>
      </div>
    </div>
  )
}

Any deep /post/any/url/long/will_return_slugfile

Again!!! in next.js you have to take care about the file system structure in routing. (As I mentioned in my last answer)

pages/post/[[...slug]].js will match /post, /post/a, /post/a/b, and so on.

Peter
  • 1,280
  • 8
  • 15
1

If avoiding rerenders in nested routes is your problem, the solution could be nesting your pages in a layout component as explained here:

https://nextjs.org/docs/basic-features/layouts

https://adamwathan.me/2019/10/17/persistent-layout-patterns-in-nextjs/

lami
  • 1,410
  • 12
  • 16
0

Actually, based on the docs description, I believe you used this push function wrongly. See the following codes that come from docs:

import Router from 'next/router'

Router.push(url, as, options)

And the docs said:

  • url - The URL to navigate to. This is usually the name of a page
  • as - Optional decorator for the URL that will be shown in the browser. Defaults to url
  • options - Optional object with the following configuration options: shallow: Update the path of the current page without rerunning getStaticProps, getServerSideProps or getInitialProps. Defaults to false

It means you should pass the exact URL as the first parameter and if you wanna show it as decorating name pass the second parameter and for the third just pass the option, so you should write like below:

router.push(
  '/post/2020/01/01/hello-world',
  undefined,
  undefined
);

For the shallow routing you should use the exact example:

router.push('/?counter=10', undefined, { shallow: true });

In fact, with your code, you create a new route and it is inevitable to refreshing.

AmerllicA
  • 29,059
  • 15
  • 130
  • 154