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

Fix top-level router update forcing Suspense client fallbacks #33861

Closed
wants to merge 2 commits into from

Conversation

gaearon
Copy link
Contributor

@gaearon gaearon commented Feb 1, 2022

This top-level component recreates the public router instance when it renders. I'm hoping this is unnecessary (but I might be wrong). I'm observing this because I'm seeing dehydrated Suspense HTML content being replaced with client fallbacks. This is known (and expected) to happen when there are top-level context updates before hydration is finished facebook/react#22692. So we should avoid top-level context updates unless something meaningful actually changed.

How to test

I typed this on GH and didn't actually "test" this change.

However, I tested a similar change locally, and it solved the issue with a minimal repro. It did not solve the issue in my real project which I suspect means there's more top-level context updates somewhere (either in Next.js or in my app). But this is a start.

Locally, my test was basically returning

const Something = React.lazy(() => import('./Something').then(e =>
  new Promise(resolve => setTimeout(() => resolve(e), 5000))
);

export default function MyPage() {
  return (
    <Suspense fallback={<h2>oh no...</h2>}>
      <Something />
    </Suspense>
  )
}

I am running an experimental build of React. I verified that content from <Something /> appeared in HTML but was getting deleted soon after hydration (but not in the hydration commit). There were no mismatches. After fixing this context provider, Suspense was no longer forced into fallbacks in my minimal repro. This would be a good regression test to add to Next.js.

@ijjk
Copy link
Member

ijjk commented Feb 1, 2022

Failing test suites

Commit: 05f15cd

test/integration/auto-export-serverless/test/index.test.js

  • Auto Export Serverless > Refreshes query on mount
Expand output

● Auto Export Serverless › Refreshes query on mount

expect(received).toMatch(expected)

Expected pattern: /post.*post-1/
Received string:  "<div id=\"__next\" data-reactroot=\"\"><p>post: </p></div><script id=\"__NEXT_DATA__\" type=\"application/json\">{\"props\":{\"pageProps\":{}},\"page\":\"/[post]\",\"query\":{},\"buildId\":\"vpS07TF3Gi0WwiV-4Qzri\",\"runtimeConfig\":{},\"nextExport\":true,\"autoExport\":true,\"isFallback\":false,\"scriptLoader\":[]}</script><next-route-announcer><p aria-live=\"assertive\" id=\"__next-route-announcer__\" role=\"alert\" style=\"border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap; overflow-wrap: normal;\"></p></next-route-announcer>"

  18 |     const html = await browser.eval('document.body.innerHTML')
  19 |     expect(html).toMatch(/post.*post-1/)
> 20 |     expect(html).toMatch(/nextExport/)
     |                      ^
  21 |
  22 |     await killApp(app)
  23 |     await browser.close()

  at Object.<anonymous> (integration/auto-export-serverless/test/index.test.js:20:22)

test/integration/auto-export/test/index.test.js

  • Auto Export > dev > Refreshes query on mount
  • Auto Export > dev > should update asPath after mount
  • Auto Export > production > Refreshes query on mount
  • Auto Export > production > should update asPath after mount
Expand output

● Auto Export › production › Refreshes query on mount

TIMED OUT: /post.*post-1/

<div id="__next" data-reactroot=""><p>post: </p></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/[post]","query":{},"buildId":"atrAFQXRjQSgrHGjw14Nv","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script><next-route-announcer><p aria-live="assertive" id="__next-route-announcer__" role="alert" style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap; overflow-wrap: normal;"></p></next-route-announcer>

  479 | }
  480 |
> 481 | export class File {
      |               ^
  482 |   constructor(path) {
  483 |     this.path = path
  484 |     this.originalContent = existsSync(this.path)

  at Object.check (lib/next-test-utils.js:481:15)
  at Object.<anonymous> (integration/auto-export/test/index.test.js:30:9)

● Auto Export › production › should update asPath after mount

TIMED OUT: /\/zeit\/cmnt-2/

<head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><meta name="next-head-count" content="2"><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/_next/static/chunks/polyfills-5cd94c89d3acac5f.js"></script><script src="/_next/static/chunks/webpack-fd82975a6094609f.js" defer=""></script><script src="/_next/static/chunks/framework-0743cdcfc4bead40.js" defer=""></script><script src="/_next/static/chunks/main-bd361f33c292802f.js" defer=""></script><script src="/_next/static/chunks/pages/_app-26477814f4b13881.js" defer=""></script><script src="/_next/static/chunks/pages/%5Bpost%5D/%5Bcmnt%5D-a33f5d4d6c36a726.js" defer=""></script><script src="/_next/static/atrAFQXRjQSgrHGjw14Nv/_buildManifest.js" defer=""></script><script src="/_next/static/atrAFQXRjQSgrHGjw14Nv/_ssgManifest.js" defer=""></script><script src="/_next/static/atrAFQXRjQSgrHGjw14Nv/_middlewareManifest.js" defer=""></script></head><body><div id="__next" data-reactroot=""><p>/[post]/[cmnt]</p></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/[post]/[cmnt]","query":{},"buildId":"atrAFQXRjQSgrHGjw14Nv","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script><next-route-announcer><p aria-live="assertive" id="__next-route-announcer__" role="alert" style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap; overflow-wrap: normal;"></p></next-route-announcer></body>

  479 | }
  480 |
> 481 | export class File {
      |               ^
  482 |   constructor(path) {
  483 |     this.path = path
  484 |     this.originalContent = existsSync(this.path)

  at Object.check (lib/next-test-utils.js:481:15)
  at Object.<anonymous> (integration/auto-export/test/index.test.js:37:9)

● Auto Export › dev › Refreshes query on mount

TIMED OUT: /post.*post-1/

<div id="__next" data-reactroot=""><p>post: </p></div><script src="/_next/static/chunks/react-refresh.js?ts=1643726644987"></script><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/[post]","query":{},"buildId":"development","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script><div id="__next-build-watcher" style="position: fixed; bottom: 10px; right: 20px; width: 0px; height: 0px; z-index: 99999;"></div><next-route-announcer><p aria-live="assertive" id="__next-route-announcer__" role="alert" style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap; overflow-wrap: normal;"></p></next-route-announcer>

  479 | }
  480 |
> 481 | export class File {
      |               ^
  482 |   constructor(path) {
  483 |     this.path = path
  484 |     this.originalContent = existsSync(this.path)

  at Object.check (lib/next-test-utils.js:481:15)
  at Object.<anonymous> (integration/auto-export/test/index.test.js:30:9)

● Auto Export › dev › should update asPath after mount

TIMED OUT: /\/zeit\/cmnt-2/

<head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><meta name="next-head-count" content="2"><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/_next/static/chunks/polyfills.js?ts=1643726676169"></script><script src="/_next/static/chunks/webpack.js?ts=1643726676169" defer=""></script><script src="/_next/static/chunks/main.js?ts=1643726676169" defer=""></script><script src="/_next/static/chunks/pages/_app.js?ts=1643726676169" defer=""></script><script src="/_next/static/chunks/pages/%5Bpost%5D/%5Bcmnt%5D.js?ts=1643726676169" defer=""></script><script src="/_next/static/development/_buildManifest.js?ts=1643726676169" defer=""></script><script src="/_next/static/development/_ssgManifest.js?ts=1643726676169" defer=""></script><script src="/_next/static/development/_middlewareManifest.js?ts=1643726676169" defer=""></script><noscript id="__next_css__DO_NOT_USE__"></noscript></head><body><div id="__next" data-reactroot=""><p>/[post]/[cmnt]</p></div><script src="/_next/static/chunks/react-refresh.js?ts=1643726676169"></script><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/[post]/[cmnt]","query":{},"buildId":"development","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script><div id="__next-build-watcher" style="position: fixed; bottom: 10px; right: 20px; width: 0px; height: 0px; z-index: 99999;"></div><next-route-announcer><p aria-live="assertive" id="__next-route-announcer__" role="alert" style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap; overflow-wrap: normal;"></p></next-route-announcer></body>

  479 | }
  480 |
> 481 | export class File {
      |               ^
  482 |   constructor(path) {
  483 |     this.path = path
  484 |     this.originalContent = existsSync(this.path)

  at Object.check (lib/next-test-utils.js:481:15)
  at Object.<anonymous> (integration/auto-export/test/index.test.js:37:9)

test/e2e/basepath.test.ts

  • basePath > should navigate back correctly to a dynamic route
  • basePath > should update dynamic params after mount correctly
  • basePath > should navigate to index page with getStaticProps
  • basePath > should navigate to nested index page with getStaticProps
  • basePath > should work with normal dynamic page
  • basePath > should work with catch-all page
  • basePath > should fetch data for getStaticProps without reloading
  • basePath > should fetch data for getServerSideProps without reloading
Expand output

● basePath › should navigate back correctly to a dynamic route

TIMED OUT: /first/

parts:

  479 | }
  480 |
> 481 | export class File {
      |               ^
  482 |   constructor(path) {
  483 |     this.path = path
  484 |     this.originalContent = existsSync(this.path)

  at Object.check (lib/next-test-utils.js:481:15)
  at Object.<anonymous> (e2e/basepath.test.ts:114:13)

● basePath › should update dynamic params after mount correctly

TIMED OUT: /slug: hello-dynamic/

slug:

  479 | }
  480 |
> 481 | export class File {
      |               ^
  482 |   constructor(path) {
  483 |     this.path = path
  484 |     this.originalContent = existsSync(this.path)

  at Object.check (lib/next-test-utils.js:481:15)
  at Object.<anonymous> (e2e/basepath.test.ts:291:13)

● basePath › should navigate to index page with getStaticProps

expect(received).toBe(expected) // Object.is equality

Expected: "/"
Received: "/hello"

  301 |       const res = await fetchViaHTTP(
  302 |         next.url,
> 303 |         '/redirect-no-basepath',
      |                                 ^
  304 |         undefined,
  305 |         {
  306 |           redirect: 'manual',

  at Object.<anonymous> (e2e/basepath.test.ts:303:68)
      at runMicrotasks (<anonymous>)

● basePath › should navigate to nested index page with getStaticProps

expect(received).toBe(expected) // Object.is equality

Expected: "/index"
Received: "/hello"

  322 |     it('should not add header without basePath without disabling', async () => {
  323 |       const res = await fetchViaHTTP(next.url, '/add-header')
> 324 |       expect(res.headers.get('x-hello')).toBe(null)
      |                                                    ^
  325 |     })
  326 |
  327 |     it('should not add header with basePath when set to false', async () => {

  at Object.<anonymous> (e2e/basepath.test.ts:324:68)
      at runMicrotasks (<anonymous>)

● basePath › should work with normal dynamic page

TIMED OUT: /slug: first/

<head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><meta name="next-head-count" content="2"><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/docs/_next/static/chunks/polyfills-5cd94c89d3acac5f.js"></script><script src="/docs/_next/static/chunks/webpack-9108ffbfcab9a4df.js" defer=""></script><script src="/docs/_next/static/chunks/framework-91d7f78b5b4003c8.js" defer=""></script><script src="/docs/_next/static/chunks/main-f38332d9fdc530a7.js" defer=""></script><script src="/docs/_next/static/chunks/pages/_app-423d436f094b6ef2.js" defer=""></script><script src="/docs/_next/static/chunks/pages/hello-2dd425ef922ee25d.js" defer=""></script><script src="/docs/_next/static/Ie__caGg8TOpR1Un4YNny/_buildManifest.js" defer=""></script><script src="/docs/_next/static/Ie__caGg8TOpR1Un4YNny/_ssgManifest.js" defer=""></script><script src="/docs/_next/static/Ie__caGg8TOpR1Un4YNny/_middlewareManifest.js" defer=""></script><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/other-page-a3a9d8f9ea97e8f8.js"><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/gsp-62ee2f6c0d1dbb6a.js"><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/gssp-d2b704f8dd1e223a.js"><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/%5Bslug%5D-ce14ce5c71850032.js"><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/catchall/%5B...parts%5D-f57ad34ae1cfc974.js"><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/index-159a950b09b85de0.js"><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/index/index-cab01caa55d17ce9.js"><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/slow-route-9ba2ab94f0a660bf.js"><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/error-route-be0b95d092107255.js"></head><body><div id="__next" data-reactroot=""><p id="slug">slug: </p></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/hello","query":{},"buildId":"Ie__caGg8TOpR1Un4YNny","assetPrefix":"/docs","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script><next-route-announcer><p aria-live="assertive" id="__next-route-announcer__" role="alert" style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap; overflow-wrap: normal;"></p></next-route-announcer><script src="/docs/_next/static/chunks/pages/other-page-a3a9d8f9ea97e8f8.js"></script><script src="/docs/_next/static/chunks/pages/gsp-62ee2f6c0d1dbb6a.js"></script><script src="/docs/_next/static/chunks/pages/gssp-d2b704f8dd1e223a.js"></script><script src="/docs/_next/static/chunks/pages/%5Bslug%5D-ce14ce5c71850032.js"></script><script src="/docs/_next/static/chunks/pages/catchall/%5B...parts%5D-f57ad34ae1cfc974.js"></script><script src="/docs/_next/static/chunks/pages/index-159a950b09b85de0.js"></script><script src="/docs/_next/static/chunks/pages/index/index-cab01caa55d17ce9.js"></script><script src="/docs/_next/static/chunks/pages/slow-route-9ba2ab94f0a660bf.js"></script><script src="/docs/_next/static/chunks/pages/error-route-be0b95d092107255.js"></script></body>

  479 | }
  480 |
> 481 | export class File {
      |               ^
  482 |   constructor(path) {
  483 |     this.path = path
  484 |     this.originalContent = existsSync(this.path)

  at Object.check (lib/next-test-utils.js:481:15)
  at Object.<anonymous> (e2e/basepath.test.ts:347:13)

● basePath › should work with catch-all page

TIMED OUT: /parts: hello\/world/

<head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><meta name="next-head-count" content="2"><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/docs/_next/static/chunks/polyfills-5cd94c89d3acac5f.js"></script><script src="/docs/_next/static/chunks/webpack-9108ffbfcab9a4df.js" defer=""></script><script src="/docs/_next/static/chunks/framework-91d7f78b5b4003c8.js" defer=""></script><script src="/docs/_next/static/chunks/main-f38332d9fdc530a7.js" defer=""></script><script src="/docs/_next/static/chunks/pages/_app-423d436f094b6ef2.js" defer=""></script><script src="/docs/_next/static/chunks/pages/hello-2dd425ef922ee25d.js" defer=""></script><script src="/docs/_next/static/Ie__caGg8TOpR1Un4YNny/_buildManifest.js" defer=""></script><script src="/docs/_next/static/Ie__caGg8TOpR1Un4YNny/_ssgManifest.js" defer=""></script><script src="/docs/_next/static/Ie__caGg8TOpR1Un4YNny/_middlewareManifest.js" defer=""></script><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/other-page-a3a9d8f9ea97e8f8.js"><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/gsp-62ee2f6c0d1dbb6a.js"><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/gssp-d2b704f8dd1e223a.js"><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/%5Bslug%5D-ce14ce5c71850032.js"><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/catchall/%5B...parts%5D-f57ad34ae1cfc974.js"><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/index-159a950b09b85de0.js"><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/index/index-cab01caa55d17ce9.js"><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/slow-route-9ba2ab94f0a660bf.js"><link as="script" rel="prefetch" href="/docs/_next/static/chunks/pages/error-route-be0b95d092107255.js"></head><body><div id="__next" data-reactroot=""><p>parts: </p></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/hello","query":{},"buildId":"Ie__caGg8TOpR1Un4YNny","assetPrefix":"/docs","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script><next-route-announcer><p aria-live="assertive" id="__next-route-announcer__" role="alert" style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap; overflow-wrap: normal;"></p></next-route-announcer><script src="/docs/_next/static/chunks/pages/other-page-a3a9d8f9ea97e8f8.js"></script><script src="/docs/_next/static/chunks/pages/gsp-62ee2f6c0d1dbb6a.js"></script><script src="/docs/_next/static/chunks/pages/gssp-d2b704f8dd1e223a.js"></script><script src="/docs/_next/static/chunks/pages/%5Bslug%5D-ce14ce5c71850032.js"></script><script src="/docs/_next/static/chunks/pages/catchall/%5B...parts%5D-f57ad34ae1cfc974.js"></script><script src="/docs/_next/static/chunks/pages/index-159a950b09b85de0.js"></script><script src="/docs/_next/static/chunks/pages/index/index-cab01caa55d17ce9.js"></script><script src="/docs/_next/static/chunks/pages/slow-route-9ba2ab94f0a660bf.js"></script><script src="/docs/_next/static/chunks/pages/error-route-be0b95d092107255.js"></script></body>

  479 | }
  480 |
> 481 | export class File {
      |               ^
  482 |   constructor(path) {
  483 |     this.path = path
  484 |     this.originalContent = existsSync(this.path)

  at Object.check (lib/next-test-utils.js:481:15)
  at Object.<anonymous> (e2e/basepath.test.ts:362:13)

● basePath › should fetch data for getStaticProps without reloading

expect(received).toBe(expected) // Object.is equality

Expected: "/gsp"
Received: "/hello"

  483 |       )
  484 |     })
> 485 |
      | ^
  486 |     it('should redirect trailing slash correctly', async () => {
  487 |       const res = await fetchViaHTTP(
  488 |         next.url,

  at Object.<anonymous> (e2e/basepath.test.ts:485:30)
      at runMicrotasks (<anonymous>)

● basePath › should fetch data for getServerSideProps without reloading

expect(received).toBe(expected) // Object.is equality

Expected: "/gssp"
Received: "/hello"

  495 |       expect(pathname).toBe(`${basePath}/hello`)
  496 |       const text = await res.text()
> 497 |       expect(text).toEqual(`${basePath}/hello`)
      |                              ^
  498 |     })
  499 |
  500 |     it('should redirect trailing slash on root correctly', async () => {

  at Object.<anonymous> (e2e/basepath.test.ts:497:30)
      at runMicrotasks (<anonymous>)

test/integration/i18n-support-fallback-rewrite-legacy/test/index.test.js

  • i18n Support > dev mode > should not rewrite for index page
  • i18n Support > dev mode > should not rewrite for dynamic page
  • i18n Support > production mode > should not rewrite for index page
  • i18n Support > production mode > should not rewrite for dynamic page
Expand output

● i18n Support › dev mode › should not rewrite for index page

expect(received).toEqual(expected) // deep equality

- Expected  - 3
+ Received  + 1

  Object {
    "asPath": "/?hello=world",
    "index": true,
    "pathname": "/",
-   "query": Object {
-     "hello": "world",
-   },
+   "query": Object {},
  }

  53 |       ['/dynamic/first', {}],
  54 |       ['/en/dynamic/first', {}],
> 55 |       ['/fr/dynamic/first', {}],
     |                                 ^
  56 |       ['/dynamic/first', { hello: 'world' }],
  57 |       ['/en/dynamic/first', { hello: 'world' }],
  58 |       ['/fr/dynamic/first', { hello: 'world' }],

  at Object.<anonymous> (integration/i18n-support-fallback-rewrite-legacy/test/index.test.js:55:78)

● i18n Support › dev mode › should not rewrite for dynamic page

expect(received).toEqual(expected) // deep equality

- Expected  - 4
+ Received  + 2

  Object {
-   "asPath": "/dynamic/first",
+   "asPath": "/dynamic/[slug]",
    "dynamic": true,
    "pathname": "/dynamic/[slug]",
-   "query": Object {
-     "slug": "first",
-   },
+   "query": Object {},
  }

  112 |     })
  113 |
> 114 |     runTests()
      |               ^
  115 |   })
  116 | })
  117 |

  at Object.<anonymous> (integration/i18n-support-fallback-rewrite-legacy/test/index.test.js:114:78)
      at runMicrotasks (<anonymous>)

● i18n Support › production mode › should not rewrite for index page

expect(received).toEqual(expected) // deep equality

- Expected  - 3
+ Received  + 1

  Object {
    "asPath": "/?hello=world",
    "index": true,
    "pathname": "/",
-   "query": Object {
-     "hello": "world",
-   },
+   "query": Object {},
  }

  53 |       ['/dynamic/first', {}],
  54 |       ['/en/dynamic/first', {}],
> 55 |       ['/fr/dynamic/first', {}],
     |                                 ^
  56 |       ['/dynamic/first', { hello: 'world' }],
  57 |       ['/en/dynamic/first', { hello: 'world' }],
  58 |       ['/fr/dynamic/first', { hello: 'world' }],

  at Object.<anonymous> (integration/i18n-support-fallback-rewrite-legacy/test/index.test.js:55:78)

● i18n Support › production mode › should not rewrite for dynamic page

expect(received).toEqual(expected) // deep equality

- Expected  - 4
+ Received  + 2

  Object {
-   "asPath": "/dynamic/first",
+   "asPath": "/dynamic/[slug]",
    "dynamic": true,
    "pathname": "/dynamic/[slug]",
-   "query": Object {
-     "slug": "first",
-   },
+   "query": Object {},
  }

  112 |     })
  113 |
> 114 |     runTests()
      |               ^
  115 |   })
  116 | })
  117 |

  at Object.<anonymous> (integration/i18n-support-fallback-rewrite-legacy/test/index.test.js:114:78)
      at runMicrotasks (<anonymous>)

@ijjk
Copy link
Member

ijjk commented Feb 1, 2022

Stats from current PR

Default Build (Increase detected ⚠️)
General Overall increase ⚠️
vercel/next.js canary gaearon/next.js patch-1 Change
buildDuration 11.4s 11.3s -126ms
buildDurationCached 2.9s 2.9s ⚠️ +32ms
nodeModulesSize 358 MB 358 MB ⚠️ +153 B
Page Load Tests Overall decrease ⚠️
vercel/next.js canary gaearon/next.js patch-1 Change
/ failed reqs 0 0
/ total time (seconds) 2.641 2.728 ⚠️ +0.09
/ avg req/sec 946.78 916.36 ⚠️ -30.42
/error-in-render failed reqs 0 0
/error-in-render total time (seconds) 1.261 1.268 ⚠️ +0.01
/error-in-render avg req/sec 1981.97 1971.76 ⚠️ -10.21
Client Bundles (main, webpack, commons) Overall increase ⚠️
vercel/next.js canary gaearon/next.js patch-1 Change
450.HASH.js gzip 179 B 179 B
framework-HASH.js gzip 42.2 kB 42.2 kB
main-HASH.js gzip 27.2 kB 27.2 kB ⚠️ +6 B
webpack-HASH.js gzip 1.44 kB 1.44 kB
Overall change 71 kB 71 kB ⚠️ +6 B
Legacy Client Bundles (polyfills)
vercel/next.js canary gaearon/next.js patch-1 Change
polyfills-HASH.js gzip 31 kB 31 kB
Overall change 31 kB 31 kB
Client Pages
vercel/next.js canary gaearon/next.js patch-1 Change
_app-HASH.js gzip 1.37 kB 1.37 kB
_error-HASH.js gzip 194 B 194 B
amp-HASH.js gzip 312 B 312 B
css-HASH.js gzip 326 B 326 B
dynamic-HASH.js gzip 2.37 kB 2.37 kB
head-HASH.js gzip 350 B 350 B
hooks-HASH.js gzip 919 B 919 B
image-HASH.js gzip 4.94 kB 4.94 kB
index-HASH.js gzip 263 B 263 B
link-HASH.js gzip 2.19 kB 2.19 kB
routerDirect..HASH.js gzip 321 B 321 B
script-HASH.js gzip 383 B 383 B
withRouter-HASH.js gzip 318 B 318 B
85e02e95b279..7e3.css gzip 107 B 107 B
Overall change 14.4 kB 14.4 kB
Client Build Manifests
vercel/next.js canary gaearon/next.js patch-1 Change
_buildManifest.js gzip 459 B 459 B
Overall change 459 B 459 B
Rendered Page Sizes Overall increase ⚠️
vercel/next.js canary gaearon/next.js patch-1 Change
index.html gzip 530 B 530 B
link.html gzip 543 B 545 B ⚠️ +2 B
withRouter.html gzip 525 B 526 B ⚠️ +1 B
Overall change 1.6 kB 1.6 kB ⚠️ +3 B

Diffs

Diff for main-HASH.js
@@ -700,6 +700,7 @@
       var webpackHMR;
       var router;
       exports.router = router;
+      var publicRouterInstance;
       var CachedApp, onPerfEntry;
       headManager.getIsSsr = function() {
         return router.isSsr;
@@ -973,6 +974,8 @@
                           isPreview: isPreview
                         }
                       );
+                      publicRouterInstance = (0,
+                      _router1).makePublicRouterInstance(router);
                       renderCtx = {
                         App: CachedApp,
                         initial: true,
@@ -984,12 +987,12 @@
                       }
                       render(renderCtx);
                       return _ctx.abrupt("return", emitter);
-                    case 44:
+                    case 45:
                       return _ctx.abrupt("return", {
                         emitter: emitter,
                         renderCtx: renderCtx
                       });
-                    case 45:
+                    case 46:
                     case "end":
                       return _ctx.stop();
                   }
@@ -1226,7 +1229,7 @@
           /*#__PURE__*/ _react.default.createElement(
             _routerContext.RouterContext.Provider,
             {
-              value: (0, _router1).makePublicRouterInstance(router)
+              value: publicRouterInstance
             },
             /*#__PURE__*/ _react.default.createElement(
               _headManagerContext.HeadManagerContext.Provider,
Diff for index.html
@@ -19,7 +19,7 @@
       defer=""
     ></script>
     <script
-      src="/_next/static/chunks/main-c6fc133fe313f8ea.js"
+      src="/_next/static/chunks/main-5f632ae85152c91f.js"
       defer=""
     ></script>
     <script
Diff for link.html
@@ -19,7 +19,7 @@
       defer=""
     ></script>
     <script
-      src="/_next/static/chunks/main-c6fc133fe313f8ea.js"
+      src="/_next/static/chunks/main-5f632ae85152c91f.js"
       defer=""
     ></script>
     <script
Diff for withRouter.html
@@ -19,7 +19,7 @@
       defer=""
     ></script>
     <script
-      src="/_next/static/chunks/main-c6fc133fe313f8ea.js"
+      src="/_next/static/chunks/main-5f632ae85152c91f.js"
       defer=""
     ></script>
     <script

Default Build with SWC (Decrease detected ✓)
General Overall increase ⚠️
vercel/next.js canary gaearon/next.js patch-1 Change
buildDuration 14.5s 14.2s -266ms
buildDurationCached 2.9s 2.9s -43ms
nodeModulesSize 358 MB 358 MB ⚠️ +153 B
Page Load Tests Overall decrease ⚠️
vercel/next.js canary gaearon/next.js patch-1 Change
/ failed reqs 0 0
/ total time (seconds) 2.677 2.73 ⚠️ +0.05
/ avg req/sec 934.05 915.6 ⚠️ -18.45
/error-in-render failed reqs 0 0
/error-in-render total time (seconds) 1.253 1.255 0
/error-in-render avg req/sec 1995.07 1992.7 ⚠️ -2.37
Client Bundles (main, webpack, commons) Overall increase ⚠️
vercel/next.js canary gaearon/next.js patch-1 Change
450.HASH.js gzip 179 B 179 B
framework-HASH.js gzip 42.3 kB 42.3 kB
main-HASH.js gzip 27.3 kB 27.3 kB ⚠️ +3 B
webpack-HASH.js gzip 1.44 kB 1.44 kB
Overall change 71.3 kB 71.3 kB ⚠️ +3 B
Legacy Client Bundles (polyfills)
vercel/next.js canary gaearon/next.js patch-1 Change
polyfills-HASH.js gzip 31 kB 31 kB
Overall change 31 kB 31 kB
Client Pages
vercel/next.js canary gaearon/next.js patch-1 Change
_app-HASH.js gzip 1.35 kB 1.35 kB
_error-HASH.js gzip 180 B 180 B
amp-HASH.js gzip 305 B 305 B
css-HASH.js gzip 321 B 321 B
dynamic-HASH.js gzip 2.36 kB 2.36 kB
head-HASH.js gzip 342 B 342 B
hooks-HASH.js gzip 911 B 911 B
image-HASH.js gzip 4.98 kB 4.98 kB
index-HASH.js gzip 256 B 256 B
link-HASH.js gzip 2.21 kB 2.21 kB
routerDirect..HASH.js gzip 314 B 314 B
script-HASH.js gzip 375 B 375 B
withRouter-HASH.js gzip 309 B 309 B
85e02e95b279..7e3.css gzip 107 B 107 B
Overall change 14.3 kB 14.3 kB
Client Build Manifests
vercel/next.js canary gaearon/next.js patch-1 Change
_buildManifest.js gzip 458 B 458 B
Overall change 458 B 458 B
Rendered Page Sizes Overall increase ⚠️
vercel/next.js canary gaearon/next.js patch-1 Change
index.html gzip 530 B 531 B ⚠️ +1 B
link.html gzip 543 B 544 B ⚠️ +1 B
withRouter.html gzip 525 B 525 B
Overall change 1.6 kB 1.6 kB ⚠️ +2 B

Diffs

Diff for main-HASH.js
@@ -700,6 +700,7 @@
       var webpackHMR;
       var router;
       exports.router = router;
+      var publicRouterInstance;
       var CachedApp, onPerfEntry;
       headManager.getIsSsr = function() {
         return router.isSsr;
@@ -973,6 +974,8 @@
                           isPreview: isPreview
                         }
                       );
+                      publicRouterInstance = (0,
+                      _router1).makePublicRouterInstance(router);
                       renderCtx = {
                         App: CachedApp,
                         initial: true,
@@ -984,12 +987,12 @@
                       }
                       render(renderCtx);
                       return _ctx.abrupt("return", emitter);
-                    case 44:
+                    case 45:
                       return _ctx.abrupt("return", {
                         emitter: emitter,
                         renderCtx: renderCtx
                       });
-                    case 45:
+                    case 46:
                     case "end":
                       return _ctx.stop();
                   }
@@ -1226,7 +1229,7 @@
           /*#__PURE__*/ _react.default.createElement(
             _routerContext.RouterContext.Provider,
             {
-              value: (0, _router1).makePublicRouterInstance(router)
+              value: publicRouterInstance
             },
             /*#__PURE__*/ _react.default.createElement(
               _headManagerContext.HeadManagerContext.Provider,
Diff for index.html
@@ -19,7 +19,7 @@
       defer=""
     ></script>
     <script
-      src="/_next/static/chunks/main-c6fc133fe313f8ea.js"
+      src="/_next/static/chunks/main-5f632ae85152c91f.js"
       defer=""
     ></script>
     <script
Diff for link.html
@@ -19,7 +19,7 @@
       defer=""
     ></script>
     <script
-      src="/_next/static/chunks/main-c6fc133fe313f8ea.js"
+      src="/_next/static/chunks/main-5f632ae85152c91f.js"
       defer=""
     ></script>
     <script
Diff for withRouter.html
@@ -19,7 +19,7 @@
       defer=""
     ></script>
     <script
-      src="/_next/static/chunks/main-c6fc133fe313f8ea.js"
+      src="/_next/static/chunks/main-5f632ae85152c91f.js"
       defer=""
     ></script>
     <script
Commit: 05f15cd

@gaearon
Copy link
Contributor Author

gaearon commented Feb 1, 2022

looks like this fails a bunch of tests? i'm not sure what to make of these failures or how to run the same tests locally..

@@ -413,6 +415,7 @@ export async function initNext(opts: { webpackHMR?: any } = {}) {
domainLocales,
isPreview,
})
publicRouterInstance = makePublicRouterInstance(router)
Copy link
Member

@ijjk ijjk Feb 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to tweak how makePublicRouterInstance is handling the fields to prevent this from being re-created on every render. Currently it's creating a new instance and copying the fields to prevent them from being treated as stateful.

This also causes the router methods to change on each render, related issue here #18127

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't putting it outside of render and creating it once at initialization, like this PR does, mitigate the issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, looks like we should go with #33875 approach instead? @devknoll

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does but then the fields are never updated from the initial public router since they are copied here

for (const property of urlPropertyFields) {
if (typeof scopedRouter[property] === 'object') {
instance[property] = Object.assign(
Array.isArray(scopedRouter[property]) ? [] : {},
scopedRouter[property]
) // makes sure query is not stateful
continue
}
instance[property] = scopedRouter[property]
}
and then the underlying router instance is updated but not the public router instance. So maybe instead of copying the fields we could Proxy them in development, erroring when setting to them to prevent them from being treated as stateful and in production we don't modify the router like this 🤔

Copy link
Contributor

@devknoll devknoll Feb 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the issue is that router is a bit of a misnomer here. When used globally, it means "routing functions + a reference to the currently visible route." When used locally (via a React hook), it means "routing functions + the currently rendered route."

The router context should change for each tree IMO. Consider something like this:

function Foo() {
  const { query } = useRouter()
  return <span>{JSON.stringify(query)}</span>
}
  1. If the context didn't change when query changed, how would Foo rerender?
  2. If the value of query only pointed to the active route, how would we support startTransition, which renders the next page concurrently in the background, and presumably wants the new query value?

My approach in #33875 is working on addressing this from this perspective, i.e. just creating a new public instance for each route/navigation, instead of generating a new one each time AppContainer is rerendered.

The approach in this PR is probably fine to unblock for now, but I don't think it'll work for concurrent rendering.

@ijjk
Copy link
Member

ijjk commented Feb 2, 2022

i'm not sure what to make of these failures or how to run the same tests locally

Were there any specific issues you ran into while trying to run the tests locally? We document some ways to run them in the contributing doc and if there's any tips we can add there let us know.

You can also run yarn next test/integration/auto-export to debug test fixtures for integration tests.

@devknoll
Copy link
Contributor

@gaearon can you confirm whether this is still occurring with the latest canary? If so, could you provide a full repro case?

@ijjk
Copy link
Member

ijjk commented May 22, 2022

I'm gonna close this per above, please let us know if this is still an issue and we can investigate further!

@ijjk ijjk closed this May 22, 2022
@gaearon
Copy link
Contributor Author

gaearon commented May 23, 2022

Was there some change that would cause it to not occur? My understanding is that the problem is clear-cut: always recreating the instance in that component causes unnecessary context updates. Unnecessary context updates at the top level are bad. Which part needs a reproduction?

@gaearon
Copy link
Contributor Author

gaearon commented May 23, 2022

OK, so I can consistently reproduce it locally with a minimal repro on this branch:

https://github.com/gaearon/sc-bug-repro/pull/new/router-context-bug (disregard the repo name; it's not SC-specific)

However, that repro only works with 12.1.0. When I upgrade to 12.1.7-canary.11, it doesn't repro.

I'm not sure if the problem is gone per se or if it's just that it's harder to repro now.

@gaearon
Copy link
Contributor Author

gaearon commented May 23, 2022

The reason I was previously getting an extra route update at startup was this code:

// We need to replace the router state if:
// - the page was (auto) exported and has a query string or search (hash)
// - it was auto exported and is a dynamic route (to provide params)
// - if it is a client-side skeleton (fallback render)
if (
router.isSsr &&
// We don't update for 404 requests as this can modify
// the asPath unexpectedly e.g. adding basePath when
// it wasn't originally present
initialData.page !== '/404' &&
initialData.page !== '/_error' &&
(initialData.isFallback ||
(initialData.nextExport &&
(isDynamicRoute(router.pathname) ||
location.search ||
process.env.__NEXT_HAS_REWRITES)) ||
(initialData.props &&
initialData.props.__N_SSG &&
(location.search || process.env.__NEXT_HAS_REWRITES)))
) {
// update query on mount for exported pages
router.replace(

(I had some "rewrites" in the config.)

There's still a top-level context update now (which seems unnecessary? nothing changed in my app's state).

But at least this new startTransition call (added in 0eb9f7e) somewhat mitigates the negative impact:

const startTransition = (React as any).startTransition
startTransition(() => {
reactRoot.render(reactEl)
})

(and this also fixes the dehydration issue)

However, ideally, there wouldn't be another top-level update at all if nothing changed.

@gaearon
Copy link
Contributor Author

gaearon commented May 23, 2022

I'm going to disable the rewrites in our config because it triggers these false navigations. For some reason this still leads to the hydration error for us: https://github.com/reactjs/reactjs.org/pull/4680/files#r879849747. You can repro by uncommenting it back.

@gaearon
Copy link
Contributor Author

gaearon commented May 23, 2022

Filed #37139 for the general issue.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 23, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants