Skip to content

Commit

Permalink
re-add hydration support to React 18 errors
Browse files Browse the repository at this point in the history
  • Loading branch information
ztanner committed Sep 5, 2024
1 parent 15be2c6 commit f20e3c5
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 19 deletions.
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
}
matcher={isNextjsLink}
/>
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,45 @@ type NullableText = string | null | undefined
export const hydrationErrorState: HydrationErrorState = {}

// https://github.com/facebook/react/blob/main/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js used as a reference
const htmlTagsWarnings = new Set([
const htmlTagsWarnings19 = new Set([
'In HTML, %s cannot be a child of <%s>.%s\nThis will cause a hydration error.%s',
'In HTML, %s cannot be a descendant of <%s>.\nThis will cause a hydration error.%s',
'In HTML, text nodes cannot be a child of <%s>.\nThis will cause a hydration error.',
"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): 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))
Boolean(msg && htmlTagsWarnings19.has(normalizeWarningMessage(msg)))

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 +74,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
}
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
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.
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

0 comments on commit f20e3c5

Please sign in to comment.