vercel / next.js

The React Framework
https://nextjs.org
MIT License
125.58k stars 26.82k forks source link

Client Side Transitions Resolve to the Wrong Bundle When Using Multiple Overlapping Catch All Routes #38921

Open henriqueinonhe opened 2 years ago

henriqueinonhe commented 2 years ago

Verify canary release

Provide environment information

Operating System:
      Platform: linux
      Arch: x64
      Version: #1 SMP Tue Jun 23 12:58:10 UTC 2020
    Binaries:
      Node: 14.18.2
      npm: 6.14.15
      Yarn: N/A
      pnpm: N/A
    Relevant packages:
      next: 12.2.3-canary.17
      eslint-config-next: 12.2.2
      react: 18.2.0
      react-dom: 18.2.0

What browser are you using? (if relevant)

Version 103.0.5060.134 (Official Build) (64-bit)

How are you deploying your application? (if relevant)

Not Relevant

Describe the Bug

Summary

When using overlapping catch-all routes, requesting the page by entering the URL directly and by triggering a client-side transition (with <Link> or using router) renders a different page, which is caused by resolving to a different bundle.

Client-side transition resolves to the wrong bundle, while entering the URL directly resolves to the correct one.

Probably related to #37686

Setup

Consider the following page structure:

pages/
├── foo/
│   └── [...slug].js
├── [...slug].js
└── index.js

And the following build:

Page                                       Size     First Load JS
┌ ○ /                                      2.38 kB          80 kB
├ ● /[...slug]                             309 B          77.9 kB
├   └ /foo/bar
├ ○ /404                                   194 B          77.8 kB
└ ● /foo/[...slug]                         312 B          77.9 kB

Note that:

Now, let's take a look at these catch all routes implementations:

// [...slug].js
export default function Root(props) {
  console.log("Root Props", props);

  return <>Root</>
}

export const getStaticProps = async () => {
  console.log("Root Static Props")

  return {
    props: {
      RootProps: true
    }
  }
}

export const getStaticPaths = async () => {
  console.log("Root Static Paths")

  return {
    paths: [{
      params: {
        slug: ["foo", "bar"]
      }
    }],
    fallback: false
  }
}
// /foo/[...slug].js

export default function Foo(props) {
  console.log("Foo Props", props);

  return <>Foo</>
}

export const getStaticProps = async () => {
  console.log("Foo Static Props")

  return {
    props: {
      FooProps: true
    }
  }
}

export const getStaticPaths = async () => {
  console.log("Foo Static Paths")

  return {
    paths: [],
    fallback: false
  }
}

As you can see, /[...slug] generates /foo/bar due to its getStaticPaths return value, also, /foo/bar, on the other hand, is not generating any pages due to returning an empty paths array from its getStaticPaths.

Additionally, we also have an index.js file, whose implementation is the following:

// index.js

import Link from "next/link";

export default function Page() {
  return <Link href="/foo/bar">Click Me</Link>
}

It merely contains a <Link> that triggers a client-side transition to /foo/bar.

Bug

https://user-images.githubusercontent.com/20905415/180459915-52583b7d-bafa-4c2f-b6ed-78987f8ecd4a.mp4

Notice that:

When running next dev, we can even see how part of the resolution process is taking place:

On the first request:

image

Our call to /foo/bar is first handled by /foo/[...slug], due to it being more specific than /[...slug].

Then, as /foo/[...slug] getStaticPaths returns an empty paths array and it has fallback: false, it understands that /foo/[...slug] is not able to handle the request to /foo/bar, so it fallbacks to /[...slug].

When the "request reaches" /[...slug], it first computes its getStaticPaths and then realizes that it is able to handle the request to /foo/bar, and then run its getStaticProps and pass the returned props to Foo.

So, even though the request is handled correctly by the server, the problem is that the bundle that is resolved to by the client side transition, is the one that comes from /foo/[...slug].

image

Expected Behavior

I'd expect that regardless of using a client side or a server side transition, requests would resolve to the same page/bundle.

Ideally, the client-side transition behavior would match the server-side one.

Link to reproduction

https://stackblitz.com/edit/vercel-next-js-ok79yz?file=pages%2Ffoo%2F[...slug].js,pages%2F[...slug].js,pages%2Findex.tsx

To Reproduce

  1. Clone repo https://github.com/henriqueinonhe/next-catch-all-csr-bug or access https://stackblitz.com/edit/vercel-next-js-ok79yz?file=pages%2Ffoo%2F[...slug].js,pages%2F[...slug].js,pages%2Findex.tsx
  2. Navigate to index /
  3. Click the "Click Me" link
  4. Notice that: 4.1. The bundled is resolved to /foo/[...slug] 4.2. The props that were returned via AJAX come from /[...slug]
  5. Refresh the page
  6. Notice that now bundle is resolved to /[...slug]
dougwithseismic commented 2 years ago

Worth noting too that a normal link resolves to the correct (expected) page

henriqueinonhe commented 2 years ago

It seems to me that this older issue https://github.com/vercel/next.js/issues/32091 describes the same bug