Skip to content
New issue

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

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

Already on GitHub? # to your account

re-add hydration support to React 18 errors #69757

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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, '', '']

Expand All @@ -252,6 +251,7 @@ export function Errors({
.replace(/^Warning: /, '')
.replace(/^Error: /, '')
: null
const notes = isAppDir ? errorDetails.notes || '' : hydrationWarning

return (
<Overlay>
Expand Down Expand Up @@ -307,7 +307,9 @@ export function Errors({
{/* If there's hydration warning, skip displaying the error name */}
{hydrationWarning ? '' : error.name + ': '}
<HotlinkedText
text={hydrationWarning || error.message}
text={
isAppDir ? hydrationWarning || error.message : error.message
Copy link
Member Author

Choose a reason for hiding this comment

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

Reverts the Errors.tsx changes from:

to match the React 18 behavior when not in app dir

}
matcher={isNextjsLink}
/>
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLPreElement>) {
const isHtmlTagsWarning = hydrationMismatchType === 'tag'
const isReactHydrationDiff = !!reactOutputComponentDiff
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Copy link
Member Author

Choose a reason for hiding this comment

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

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) {
Expand All @@ -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`,
Copy link
Member Author

Choose a reason for hiding this comment

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

]
}

return args
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion test/development/acceptance/hydration-error-react-19.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)', () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

This can't run because we throw in pages if 18 is used

const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
skipStart: true,
Expand Down
5 changes: 2 additions & 3 deletions test/development/acceptance/hydration-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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.
Copy link
Member Author

Choose a reason for hiding this comment

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

Slight change in behavior but showing Error: in an error overlay feels a bit redundant

See more info here: https://nextjs.org/docs/messages/react-hydration-error"
`)

Expand Down
7 changes: 6 additions & 1 deletion test/development/basic/hmr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand All @@ -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',
},
Expand Down
16 changes: 15 additions & 1 deletion test/integration/auto-export/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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',
},
Expand Down
Loading