From f7a2265121549f77699197c90d953ca6cd0ef551 Mon Sep 17 00:00:00 2001
From: Zack Tanner <1939140+ztanner@users.noreply.github.com>
Date: Thu, 5 Sep 2024 13:59:20 -0700
Subject: [PATCH 1/2] re-add hydration support to React 18 errors
---
.../internal/container/Errors.tsx | 6 ++-
.../component-stack-pseudo-html.tsx | 2 +-
.../internal/helpers/hydration-error-info.ts | 51 +++++++++++++++----
.../react-dev-overlay/pages/client.ts | 8 +--
.../hydration-error-react-19.test.ts | 3 +-
.../acceptance/hydration-error.test.ts | 5 +-
test/development/basic/hmr.test.ts | 7 ++-
.../auto-export/test/index.test.js | 16 +++++-
8 files changed, 76 insertions(+), 22 deletions(-)
diff --git a/packages/next/src/client/components/react-dev-overlay/internal/container/Errors.tsx b/packages/next/src/client/components/react-dev-overlay/internal/container/Errors.tsx
index 38587213a6f59..74df140a255c3 100644
--- a/packages/next/src/client/components/react-dev-overlay/internal/container/Errors.tsx
+++ b/packages/next/src/client/components/react-dev-overlay/internal/container/Errors.tsx
@@ -238,7 +238,6 @@ export function Errors({
)
const errorDetails: HydrationErrorState = (error as any).details || {}
- const notes = errorDetails.notes || ''
const [warningTemplate, serverContent, clientContent] =
errorDetails.warning || [null, '', '']
@@ -252,6 +251,7 @@ export function Errors({
.replace(/^Warning: /, '')
.replace(/^Error: /, '')
: null
+ const notes = isAppDir ? errorDetails.notes || '' : hydrationWarning
return (
@@ -307,7 +307,9 @@ export function Errors({
{/* If there's hydration warning, skip displaying the error name */}
{hydrationWarning ? '' : error.name + ': '}
diff --git a/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/component-stack-pseudo-html.tsx b/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/component-stack-pseudo-html.tsx
index 6b2c858d6f346..58b2e2d199665 100644
--- a/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/component-stack-pseudo-html.tsx
+++ b/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/component-stack-pseudo-html.tsx
@@ -66,7 +66,7 @@ export function PseudoHtmlDiff({
firstContent: string
secondContent: string
reactOutputComponentDiff: string | undefined
- hydrationMismatchType: 'tag' | 'text'
+ hydrationMismatchType: 'tag' | 'text' | 'text-in-tag'
} & React.HTMLAttributes) {
const isHtmlTagsWarning = hydrationMismatchType === 'tag'
const isReactHydrationDiff = !!reactOutputComponentDiff
diff --git a/packages/next/src/client/components/react-dev-overlay/internal/helpers/hydration-error-info.ts b/packages/next/src/client/components/react-dev-overlay/internal/helpers/hydration-error-info.ts
index f99a6e41c55d0..413b0746008ea 100644
--- a/packages/next/src/client/components/react-dev-overlay/internal/helpers/hydration-error-info.ts
+++ b/packages/next/src/client/components/react-dev-overlay/internal/helpers/hydration-error-info.ts
@@ -23,15 +23,42 @@ const htmlTagsWarnings = new Set([
"In HTML, whitespace text nodes cannot be a child of <%s>. Make sure you don't have any extra whitespace between tags on each line of your source code.\nThis will cause a hydration error.",
])
-export const getHydrationWarningType = (msg: NullableText): 'tag' | 'text' => {
+// In React 18, the warning message is prefixed with "Warning: "
+const normalizeWarningMessage = (msg: string) => msg.replace(/^Warning: /, '')
+
+// Note: React 18 only
+const textAndTagsMismatchWarnings = new Set([
+ 'Warning: Expected server HTML to contain a matching text node for "%s" in <%s>.%s',
+ 'Warning: Did not expect server HTML to contain the text node "%s" in <%s>.%s',
+])
+
+// Note: React 18 only
+const textMismatchWarning =
+ 'Warning: Text content did not match. Server: "%s" Client: "%s"%s'
+
+const isTextMismatchWarning = (msg: NullableText) => textMismatchWarning === msg
+const isTextInTagsMismatchWarning = (msg: NullableText) =>
+ Boolean(msg && textAndTagsMismatchWarnings.has(msg))
+
+export const getHydrationWarningType = (
+ msg: NullableText
+): 'tag' | 'text' | 'text-in-tag' => {
if (isHtmlTagsWarning(msg)) return 'tag'
return 'text'
}
-const isHtmlTagsWarning = (msg: NullableText) =>
- Boolean(msg && htmlTagsWarnings.has(msg))
+const isHtmlTagsWarning = (msg: NullableText) => {
+ if (msg && typeof msg === 'string') {
+ return htmlTagsWarnings.has(normalizeWarningMessage(msg))
+ }
+
+ return false
+}
-const isKnownHydrationWarning = (msg: NullableText) => isHtmlTagsWarning(msg)
+const isKnownHydrationWarning = (msg: NullableText) =>
+ isHtmlTagsWarning(msg) ||
+ isTextInTagsMismatchWarning(msg) ||
+ isTextMismatchWarning(msg)
export const getReactHydrationDiffSegments = (msg: NullableText) => {
if (msg) {
@@ -51,14 +78,18 @@ export const getReactHydrationDiffSegments = (msg: NullableText) => {
export function storeHydrationErrorStateFromConsoleArgs(...args: any[]) {
const [msg, serverContent, clientContent, componentStack] = args
if (isKnownHydrationWarning(msg)) {
- hydrationErrorState.warning = [
- // remove the last %s from the message
- msg,
- serverContent,
- clientContent,
- ]
+ hydrationErrorState.warning = [msg, serverContent, clientContent]
hydrationErrorState.componentStack = componentStack
hydrationErrorState.serverContent = serverContent
hydrationErrorState.clientContent = clientContent
+
+ return [
+ ...args,
+ // We tack on the hydration error message to the console.error message so that
+ // it matches the error we display in the redbox overlay
+ `\nSee more info here: https://nextjs.org/docs/messages/react-hydration-error`,
+ ]
}
+
+ return args
}
diff --git a/packages/next/src/client/components/react-dev-overlay/pages/client.ts b/packages/next/src/client/components/react-dev-overlay/pages/client.ts
index e10ddbd1b1594..e60871157f4b8 100644
--- a/packages/next/src/client/components/react-dev-overlay/pages/client.ts
+++ b/packages/next/src/client/components/react-dev-overlay/pages/client.ts
@@ -52,11 +52,13 @@ function handleError(error: unknown) {
let origConsoleError = console.error
function nextJsHandleConsoleError(...args: any[]) {
+ // To support React 19, this will need to be updated as follows:
+ // const error = process.env.NODE_ENV !== 'production' ? args[1] : args[0]
// See https://github.com/facebook/react/blob/d50323eb845c5fde0d720cae888bf35dedd05506/packages/react-reconciler/src/ReactFiberErrorLogger.js#L78
- const error = process.env.NODE_ENV !== 'production' ? args[1] : args[0]
- storeHydrationErrorStateFromConsoleArgs(...args)
+ const error = args[0]
+ const errorArgs = storeHydrationErrorStateFromConsoleArgs(...args)
handleError(error)
- origConsoleError.apply(window.console, args)
+ origConsoleError.apply(window.console, errorArgs)
}
function onUnhandledError(event: ErrorEvent) {
diff --git a/test/development/acceptance/hydration-error-react-19.test.ts b/test/development/acceptance/hydration-error-react-19.test.ts
index 21ea0b97563a0..f1e5ae181acd5 100644
--- a/test/development/acceptance/hydration-error-react-19.test.ts
+++ b/test/development/acceptance/hydration-error-react-19.test.ts
@@ -4,7 +4,8 @@ import { FileRef, nextTestSetup } from 'e2e-utils'
import { outdent } from 'outdent'
import path from 'path'
-describe('Error overlay for hydration errors (React 19)', () => {
+// TODO: Enable once React 19 support is added to pages.
+describe.skip('Error overlay for hydration errors (React 19)', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
diff --git a/test/development/acceptance/hydration-error.test.ts b/test/development/acceptance/hydration-error.test.ts
index af299cf5b5228..dfcb2c7dbc196 100644
--- a/test/development/acceptance/hydration-error.test.ts
+++ b/test/development/acceptance/hydration-error.test.ts
@@ -4,8 +4,7 @@ import { FileRef, nextTestSetup } from 'e2e-utils'
import { outdent } from 'outdent'
import path from 'path'
-// TODO: Enable this test once react 18 is supported for pages router
-describe.skip('Error overlay for hydration errors (React 18)', () => {
+describe('Error overlay for hydration errors (React 18)', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
dependencies: {
@@ -38,7 +37,7 @@ describe.skip('Error overlay for hydration errors (React 18)', () => {
await session.assertHasRedbox()
expect(await session.getRedboxDescription()).toMatchInlineSnapshot(`
- "Error: Text content does not match server-rendered HTML.
+ "Text content does not match server-rendered HTML.
See more info here: https://nextjs.org/docs/messages/react-hydration-error"
`)
diff --git a/test/development/basic/hmr.test.ts b/test/development/basic/hmr.test.ts
index 3dad5302c2cb8..b065a1af3b041 100644
--- a/test/development/basic/hmr.test.ts
+++ b/test/development/basic/hmr.test.ts
@@ -18,6 +18,8 @@ import { NextInstance } from 'e2e-utils'
import { outdent } from 'outdent'
import type { NextConfig } from 'next'
+const isReact18 = true
+
describe.each([
{ basePath: '', assetPrefix: '' },
{ basePath: '', assetPrefix: '/asset-prefix' },
@@ -41,11 +43,14 @@ describe.each([
})
await retry(async () => {
const logs = await browser.log()
+
expect(logs).toEqual(
expect.arrayContaining([
{
message: expect.stringContaining(
- 'https://react.dev/link/hydration-mismatch'
+ isReact18
+ ? 'https://nextjs.org/docs/messages/react-hydration-error'
+ : 'https://react.dev/link/hydration-mismatch'
),
source: 'error',
},
diff --git a/test/integration/auto-export/test/index.test.js b/test/integration/auto-export/test/index.test.js
index e90f76052c129..6c3b87c089c25 100644
--- a/test/integration/auto-export/test/index.test.js
+++ b/test/integration/auto-export/test/index.test.js
@@ -15,6 +15,8 @@ const appDir = path.join(__dirname, '..')
let appPort
let app
+const isReact18 = true
+
const runTests = () => {
it('Supports commonjs 1', async () => {
const browser = await webdriver(appPort, '/commonjs1')
@@ -99,7 +101,19 @@ describe('Auto Export', () => {
expect.arrayContaining([
{
message: expect.stringContaining(
- 'https://react.dev/link/hydration-mismatch'
+ 'See more info here: https://nextjs.org/docs/messages/react-hydration-error'
+ ),
+ source: 'error',
+ },
+ ])
+ )
+ expect(logs).toEqual(
+ expect.arrayContaining([
+ {
+ message: expect.stringContaining(
+ isReact18
+ ? 'https://nextjs.org/docs/messages/react-hydration-error'
+ : 'https://react.dev/link/hydration-mismatch'
),
source: 'error',
},
From d2eaa44728dfb734e1fca07e0ce1ab7ac2f70bb6 Mon Sep 17 00:00:00 2001
From: Zack Tanner <1939140+ztanner@users.noreply.github.com>
Date: Thu, 5 Sep 2024 15:38:22 -0700
Subject: [PATCH 2/2] update pages-dir test for React 18 assertions (#69759)
Matches the same guard we added in [`f06d21e`
(#69484)](https://github.com/vercel/next.js/pull/69484/commits/f06d21e5de2785d24b815255b050040bebce8066)
---
.../pages-dir/production/test/index.test.ts | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/test/production/pages-dir/production/test/index.test.ts b/test/production/pages-dir/production/test/index.test.ts
index 82fd919e85e7c..8da6362cb3d8d 100644
--- a/test/production/pages-dir/production/test/index.test.ts
+++ b/test/production/pages-dir/production/test/index.test.ts
@@ -29,6 +29,8 @@ if (process.env.TEST_WASM) {
jest.setTimeout(120 * 1000)
}
+const isReact18 = true
+
describe('Production Usage', () => {
const { next } = nextTestSetup({
files: path.join(__dirname, '../fixture'),
@@ -181,7 +183,9 @@ describe('Production Usage', () => {
/webpack-runtime\.js/,
/node_modules\/react\/index\.js/,
/node_modules\/react\/package\.json/,
- /node_modules\/react\/cjs\/react\.production\.js/,
+ isReact18
+ ? /node_modules\/react\/cjs\/react\.production\.min\.js/
+ : /node_modules\/react\/cjs\/react\.production\.js/,
],
notTests: [/\0/, /\?/, /!/],
},
@@ -192,7 +196,9 @@ describe('Production Usage', () => {
/chunks\/.*?\.js/,
/node_modules\/react\/index\.js/,
/node_modules\/react\/package\.json/,
- /node_modules\/react\/cjs\/react\.production\.js/,
+ isReact18
+ ? /node_modules\/react\/cjs\/react\.production\.min\.js/
+ : /node_modules\/react\/cjs\/react\.production\.js/,
/node_modules\/next/,
],
notTests: [/\0/, /\?/, /!/],
@@ -204,7 +210,9 @@ describe('Production Usage', () => {
/chunks\/.*?\.js/,
/node_modules\/react\/index\.js/,
/node_modules\/react\/package\.json/,
- /node_modules\/react\/cjs\/react\.production\.js/,
+ isReact18
+ ? /node_modules\/react\/cjs\/react\.production\.min\.js/
+ : /node_modules\/react\/cjs\/react\.production\.js/,
/node_modules\/next/,
/node_modules\/nanoid\/index\.js/,
/node_modules\/nanoid\/url-alphabet\/index\.js/,
@@ -219,7 +227,9 @@ describe('Production Usage', () => {
/chunks\/.*?\.js/,
/node_modules\/react\/index\.js/,
/node_modules\/react\/package\.json/,
- /node_modules\/react\/cjs\/react\.production\.js/,
+ isReact18
+ ? /node_modules\/react\/cjs\/react\.production\.min\.js/
+ : /node_modules\/react\/cjs\/react\.production\.js/,
/node_modules\/next/,
],
notTests: [