Skip to content

Commit

Permalink
fix: hydration error display for text under tag case (#63288)
Browse files Browse the repository at this point in the history
There's a type of hydration warning is mismatch `"text content"` under a
`<tag>`, such as `Did not expect server HTML to contain the text node
"bad text" in <div>.`, we need to treate them separately from the text
diff or bad neseting tags.

### After

![image](https://github.com/vercel/next.js/assets/4800338/4dabfeae-b42a-4232-ab55-1704db36f5ce)


### Before


![image](https://github.com/vercel/next.js/assets/4800338/1da14435-cb3a-4883-84c8-7f0ce4c83b21)


Closes NEXT-2819
  • Loading branch information
huozhi authored Mar 14, 2024
1 parent 847b340 commit 6556a34
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import { getErrorSource } from '../../../../../shared/lib/error-source'
import { HotlinkedText } from '../components/hot-linked-text'
import { PseudoHtmlDiff } from './RuntimeError/component-stack-pseudo-html'
import {
isHtmlTagsWarning,
type HydrationErrorState,
getHydrationWarningType,
} from '../helpers/hydration-error-info'

export type SupportedErrorEvent = {
Expand Down Expand Up @@ -226,7 +226,7 @@ export function Errors({
const [warningTemplate, serverContent, clientContent] =
errorDetails.warning || [null, '', '']

const isHtmlTagsWarningTemplate = isHtmlTagsWarning(warningTemplate)
const hydrationErrorType = getHydrationWarningType(warningTemplate)
const hydrationWarning = warningTemplate
? warningTemplate
.replace('%s', serverContent)
Expand Down Expand Up @@ -274,12 +274,10 @@ export function Errors({
<p id="nextjs__container_errors__extra">{hydrationWarning}</p>
<PseudoHtmlDiff
className="nextjs__container_errors__extra_code"
hydrationMismatchType={
isHtmlTagsWarningTemplate ? 'tag' : 'text'
}
hydrationMismatchType={hydrationErrorType}
componentStackFrames={activeError.componentStackFrames}
serverContent={serverContent}
clientContent={clientContent}
firstContent={serverContent}
secondContent={clientContent}
/>
</>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ import { CollapseIcon } from '../../icons/CollapseIcon'
*/
export function PseudoHtmlDiff({
componentStackFrames,
serverContent,
clientContent,
firstContent,
secondContent,
hydrationMismatchType,
...props
}: {
componentStackFrames: ComponentStackFrame[]
serverContent: string
clientContent: string
hydrationMismatchType: 'tag' | 'text'
firstContent: string
secondContent: string
hydrationMismatchType: 'tag' | 'text' | 'text-in-tag'
} & React.HTMLAttributes<HTMLPreElement>) {
const isHtmlTagsWarning = hydrationMismatchType === 'tag'
// For text mismatch, mismatched text will take 2 rows, so we display 4 rows of component stack
Expand All @@ -54,7 +54,7 @@ export function PseudoHtmlDiff({
const [isHtmlCollapsed, toggleCollapseHtml] = useState(shouldCollapse)

const htmlComponents = useMemo(() => {
const tagNames = isHtmlTagsWarning ? [serverContent, clientContent] : []
const tagNames = isHtmlTagsWarning ? [firstContent, secondContent] : []
const nestedHtmlStack: React.ReactNode[] = []
let lastText = ''

Expand Down Expand Up @@ -131,27 +131,42 @@ export function PseudoHtmlDiff({
}
})

if (hydrationMismatchType === 'text') {
// Hydration mismatch: text or text-tag
if (!isHtmlTagsWarning) {
const spaces = ' '.repeat(nestedHtmlStack.length * 2)
const wrappedCodeLine = (
<Fragment key={nestedHtmlStack.length}>
<span data-nextjs-container-errors-pseudo-html--diff-remove>
{spaces + `"${serverContent}"\n`}
</span>
<span data-nextjs-container-errors-pseudo-html--diff-add>
{spaces + `"${clientContent}"\n`}
</span>
</Fragment>
)
let wrappedCodeLine
if (hydrationMismatchType === 'text') {
// hydration type is "text", represent [server content, client content]
wrappedCodeLine = (
<Fragment key={nestedHtmlStack.length}>
<span data-nextjs-container-errors-pseudo-html--diff-remove>
{spaces + `"${firstContent}"\n`}
</span>
<span data-nextjs-container-errors-pseudo-html--diff-add>
{spaces + `"${secondContent}"\n`}
</span>
</Fragment>
)
} else {
// hydration type is "text-in-tag", represent [parent tag, mismatch content]
wrappedCodeLine = (
<Fragment key={nestedHtmlStack.length}>
<span>{spaces + `<${secondContent}>\n`}</span>
<span data-nextjs-container-errors-pseudo-html--diff-remove>
{spaces + ` "${firstContent}"\n`}
</span>
</Fragment>
)
}
nestedHtmlStack.push(wrappedCodeLine)
}

return nestedHtmlStack
}, [
componentStackFrames,
isHtmlCollapsed,
clientContent,
serverContent,
firstContent,
secondContent,
isHtmlTagsWarning,
hydrationMismatchType,
MAX_NON_COLLAPSED_FRAMES,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,26 @@ export type HydrationErrorState = {

type NullableText = string | null | undefined

export const isHtmlTagsWarning = (msg: NullableText) =>
export const getHydrationWarningType = (
msg: NullableText
): 'tag' | 'text' | 'text-in-tag' => {
if (isHtmlTagsWarning(msg)) return 'tag'
if (isTextInTagsMismatchWarning(msg)) return 'text-in-tag'
return 'text'
}

const isHtmlTagsWarning = (msg: NullableText) =>
Boolean(msg && htmlTagsWarnings.has(msg))

export const isTextMismatchWarning = (msg: NullableText) =>
const isTextMismatchWarning = (msg: NullableText) =>
Boolean(msg && textMismatchWarnings.has(msg))
const isTextInTagsMismatchWarning = (msg: NullableText) =>
Boolean(msg && textInTagsMismatchWarnings.has(msg))

const isKnownHydrationWarning = (msg: NullableText) =>
isHtmlTagsWarning(msg) || isTextMismatchWarning(msg)
isHtmlTagsWarning(msg) ||
isTextInTagsMismatchWarning(msg) ||
isTextMismatchWarning(msg)

export const hydrationErrorState: HydrationErrorState = {}

Expand All @@ -25,11 +37,13 @@ const htmlTagsWarnings = new Set([
'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s',
])
const textMismatchWarnings = new Set([
'Warning: Text content did not match. Server: "%s" Client: "%s"%s',
const textInTagsMismatchWarnings = 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',
])
const textMismatchWarnings = new Set([
'Warning: Text content did not match. Server: "%s" Client: "%s"%s',
])

/**
* Patch console.error to capture hydration errors.
Expand Down
23 changes: 23 additions & 0 deletions test/development/acceptance-app/hydration-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,29 @@ describe('Error overlay for hydration errors', () => {
`"Did not expect server HTML to contain the text node "only" in <div>."`
)

const pseudoHtml = await session.getRedboxComponentStack()

if (isTurbopack) {
expect(pseudoHtml).toMatchInlineSnapshot(`
"...
<NotFoundErrorBoundary>
<RedirectBoundary>
<RedirectErrorBoundary>
<InnerLayoutRouter>
<Mismatch>
<div>
<div>
"only""
`)
} else {
expect(pseudoHtml).toMatchInlineSnapshot(`
"<Mismatch>
<div>
<div>
"only""
`)
}

await cleanup()
})

Expand Down

0 comments on commit 6556a34

Please sign in to comment.