Skip to content

Commit c1980ce

Browse files
authored
Display only one hydration error when there's few in error overlay (#62448)
## What & Why When there's a bad nesting HTML tags occurred, we want to show the hydration mismatch with the warning of bad nesting. Multiple hydration errors emitted from `onRecoverableError` as react is trying re-render on client, and few more errors could throw from different places. But for alert the issue, we only need to know one hydration mismatch error instead of ### After Show the toast with error number first and only show one hydration error when you open <img width="200" src="https://github.com/vercel/next.js/assets/4800338/b57e73b9-60a6-4f09-9b71-e052121666f2"><img width="500" src="https://github.com/vercel/next.js/assets/4800338/9d409422-d513-4da0-ad6c-002c1ba7c497"> ### Before Mutiple hydration errors are displayed <img width="200" src="https://github.com/vercel/next.js/assets/4800338/8d6696a8-30de-496f-a9c4-bafb7d1f1d4c"><img width="500" src="https://github.com/vercel/next.js/assets/4800338/bc72bea9-85be-4bb2-b007-b88e655ade4e"> Closes NEXT-2315
1 parent 2776b43 commit c1980ce

File tree

7 files changed

+85
-30
lines changed

7 files changed

+85
-30
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import isError from '../../lib/is-error'
2+
3+
const hydrationErrorRegex =
4+
/hydration failed|while hydrating|content does not match|did not match/i
5+
6+
export function isHydrationError(error: unknown): boolean {
7+
return isError(error) && hydrationErrorRegex.test(error.message)
8+
}

packages/next/src/client/components/react-dev-overlay/internal/container/Errors.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,10 @@ export function Errors({
235235
>
236236
<small>
237237
<span>{activeIdx + 1}</span> of{' '}
238-
<span>{readyErrors.length}</span> unhandled error
238+
<span data-nextjs-dialog-header-total-count>
239+
{readyErrors.length}
240+
</span>{' '}
241+
unhandled error
239242
{readyErrors.length < 2 ? '' : 's'}
240243
</small>
241244
{versionInfo ? <VersionStalenessInfo {...versionInfo} /> : null}

packages/next/src/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,18 @@ import {
44
hydrationErrorComponentStack,
55
} from './hydration-error-info'
66
import { isNextRouterError } from '../../../is-next-router-error'
7+
import { isHydrationError } from '../../../is-hydration-error'
78

89
export type ErrorHandler = (error: Error) => void
910

10-
function isHydrationError(error: Error): boolean {
11-
return (
12-
error.message.match(/(hydration|content does not match|did not match)/i) !=
13-
null
14-
)
15-
}
16-
1711
if (typeof window !== 'undefined') {
1812
try {
1913
// Increase the number of stack frames on the client
2014
Error.stackTraceLimit = 50
2115
} catch {}
2216
}
2317

18+
let hasHydrationError = false
2419
const errorQueue: Array<Error> = []
2520
const rejectionQueue: Array<Error> = []
2621
const errorHandlers: Array<ErrorHandler> = []
@@ -46,6 +41,7 @@ if (typeof window !== 'undefined') {
4641
return
4742
}
4843

44+
const isCausedByHydrationFailure = isHydrationError(error)
4945
if (
5046
isHydrationError(error) &&
5147
!error.message.includes(
@@ -66,7 +62,13 @@ if (typeof window !== 'undefined') {
6662
}
6763

6864
const e = error
69-
errorQueue.push(e)
65+
// Only queue one hydration every time
66+
if (isCausedByHydrationFailure) {
67+
if (!hasHydrationError) {
68+
errorQueue.push(e)
69+
}
70+
hasHydrationError = true
71+
}
7072
for (const handler of errorHandlers) {
7173
handler(e)
7274
}

test/development/acceptance-app/hydration-error.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,4 +276,45 @@ describe('Error overlay for hydration errors', () => {
276276

277277
await cleanup()
278278
})
279+
280+
it('should only show one hydration error when bad nesting happened', async () => {
281+
const { cleanup, session, browser } = await sandbox(
282+
next,
283+
new Map([
284+
[
285+
'app/page.js',
286+
outdent`
287+
'use client'
288+
289+
export default function Page() {
290+
return (
291+
<p>
292+
<p>Nested p tags</p>
293+
</p>
294+
)
295+
}
296+
`,
297+
],
298+
])
299+
)
300+
// const browser = await next.browser('/hydration-mismatch/bad-nesting')
301+
// await waitForAndOpenRuntimeError(browser)
302+
await session.waitForAndOpenRuntimeError()
303+
expect(await session.hasRedbox()).toBe(true)
304+
305+
const totalErrorCount = await browser
306+
.elementByCss('[data-nextjs-dialog-header-total-count]')
307+
.text()
308+
expect(totalErrorCount).toBe('1')
309+
310+
const description = await session.getRedboxDescription()
311+
expect(description).toContain(
312+
'Error: Hydration failed because the initial UI does not match what was rendered on the server.'
313+
)
314+
expect(description).toContain(
315+
'Warning: Expected server HTML to contain a matching <p> in <p>.'
316+
)
317+
318+
await cleanup()
319+
})
279320
})

test/development/acceptance-app/rsc-runtime-errors.test.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,12 @@ import {
88
getVersionCheckerText,
99
hasRedbox,
1010
retry,
11-
shouldRunTurboDevTest,
1211
} from 'next-test-utils'
1312

1413
createNextDescribe(
1514
'Error overlay - RSC runtime errors',
1615
{
1716
files: new FileRef(path.join(__dirname, 'fixtures', 'rsc-runtime-errors')),
18-
packageJson: {
19-
scripts: {
20-
build: 'next build',
21-
dev: `next ${shouldRunTurboDevTest() ? 'dev --turbo' : 'dev'}`,
22-
start: 'next start',
23-
},
24-
},
25-
installCommand: 'pnpm i',
26-
startCommand: (global as any).isNextDev ? 'pnpm dev' : 'pnpm start',
27-
buildCommand: 'pnpm build',
2817
},
2918
({ next }) => {
3019
it('should show runtime errors if invalid client API from node_modules is executed', async () => {

test/lib/development-sandbox.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import {
2+
hasErrorToast,
23
getRedboxComponentStack,
34
getRedboxDescription,
45
getRedboxHeader,
56
getRedboxSource,
67
getVersionCheckerText,
78
hasRedbox,
89
waitFor,
10+
waitForAndOpenRuntimeError,
911
} from './next-test-utils'
1012
import webdriver from './next-webdriver'
1113
import { NextInstance } from './next-modes/base'
@@ -111,14 +113,11 @@ export async function sandbox(
111113
async hasRedbox() {
112114
return hasRedbox(browser)
113115
},
116+
async waitForAndOpenRuntimeError() {
117+
return waitForAndOpenRuntimeError(browser)
118+
},
114119
async hasErrorToast() {
115-
return browser.eval(() => {
116-
return Boolean(
117-
Array.from(document.querySelectorAll('nextjs-portal')).find((p) =>
118-
p.shadowRoot.querySelector('[data-nextjs-toast]')
119-
)
120-
)
121-
})
120+
return Boolean(await hasErrorToast(browser))
122121
},
123122
async getRedboxDescription() {
124123
return getRedboxDescription(browser)
@@ -135,9 +134,6 @@ export async function sandbox(
135134
async getRedboxComponentStack() {
136135
return getRedboxComponentStack(browser)
137136
},
138-
async waitForAndOpenRuntimeError() {
139-
return browser.waitForElementByCss('[data-nextjs-toast]').click()
140-
},
141137
async getVersionCheckerText() {
142138
return getVersionCheckerText(browser)
143139
},

test/lib/next-test-utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,22 @@ export async function hasRedbox(browser: BrowserInterface): Promise<boolean> {
755755
return result
756756
}
757757

758+
export async function hasErrorToast(
759+
browser: BrowserInterface
760+
): Promise<boolean> {
761+
return browser.eval(() => {
762+
return Boolean(
763+
Array.from(document.querySelectorAll('nextjs-portal')).find((p) =>
764+
p.shadowRoot.querySelector('[data-nextjs-toast]')
765+
)
766+
)
767+
})
768+
}
769+
770+
export async function waitForAndOpenRuntimeError(browser: BrowserInterface) {
771+
return browser.waitForElementByCss('[data-nextjs-toast]').click()
772+
}
773+
758774
export async function getRedboxHeader(browser: BrowserInterface) {
759775
return retry(
760776
() => {

0 commit comments

Comments
 (0)