Skip to content

Commit

Permalink
[error overlay] move missing tags error inside error overlay (#62993)
Browse files Browse the repository at this point in the history
### What

* Move missing html tags error into error overlay, from outside we don't
have to manually determine when to render a dummy component with runtime
missing tag error or error overlay.
* Add brackets `<>` to the html tags in the error



![image](https://github.com/vercel/next.js/assets/4800338/cd3467b7-74c2-477e-8516-c31761adb064)


### Why

In #62815, we're having throwing an missing required error, this will
trigger another runtime error. Then when error overlay caught it through
error event listener, it will render it as an unhandled runtime error:

You will see the below message in the overlay.
```
Unhandled Runtime Error
Error: The following tas are missing...

[Error stack]
```

This error message will bring a message that the error is happened on
client during runtime, but actually we already know that is a user side
mistake which doesn't have a error trace. This couldn't hmr as you fix
the error as well.

This PR moves the rendering into error overlay that we're aware of the
errors and can render the correct html on client, with the `html` tag
attached with error id and `body` wrapping the error overlay. We tell
overlay that there're missing tags through props, let it handle
everything inside.

It can also hmr once you fix the error. One drawback is that when you
re-introduce the error, it might trigger react DOM updates exception
(`Failed to execute 'removeChild' on 'Node': The node to be removed is
not a child of this node.`) instead of the "missing tags" message again.
Besides that the HMR works properly.

Closes NEXT-2741
  • Loading branch information
huozhi authored Mar 7, 2024
1 parent 5740ef3 commit 27ed782
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 83 deletions.
36 changes: 22 additions & 14 deletions packages/next/src/client/app-index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,12 @@ export function hydrate() {
</StrictModeIfEnabled>
)

const rootLayoutMissingTags = window.__next_root_layout_missing_tags
const rootLayoutMissingTags = window.__next_root_layout_missing_tags || null
const hasMissingTags = !!rootLayoutMissingTags?.length

const options = { onRecoverableError } satisfies ReactDOMClient.RootOptions
const isError =
document.documentElement.id === '__next_error__' || rootLayoutMissingTags
document.documentElement.id === '__next_error__' || hasMissingTags

if (process.env.NODE_ENV !== 'production') {
// Patch console.error to collect information about hydration errors
Expand All @@ -196,18 +197,25 @@ export function hydrate() {
require('./components/react-dev-overlay/internal/helpers/get-socket-url')
.getSocketUrl as typeof import('./components/react-dev-overlay/internal/helpers/get-socket-url').getSocketUrl

const MissingTags = () => {
throw new Error(
`The following tags are missing in the Root Layout: ${rootLayoutMissingTags?.join(
', '
)}.\nRead more at https://nextjs.org/docs/messages/missing-root-layout-tags`
)
}

let errorTree = (
<ReactDevOverlay state={INITIAL_OVERLAY_STATE} onReactError={() => {}}>
{rootLayoutMissingTags ? <MissingTags /> : reactEl}
</ReactDevOverlay>
const FallbackLayout = hasMissingTags
? ({ children }: { children: React.ReactNode }) => (
<html id="__next_error__">
<body>{children}</body>
</html>
)
: React.Fragment
const errorTree = (
<FallbackLayout>
<ReactDevOverlay
state={{
...INITIAL_OVERLAY_STATE,
rootLayoutMissingTags,
}}
onReactError={() => {}}
>
{reactEl}
</ReactDevOverlay>
</FallbackLayout>
)
const socketUrl = getSocketUrl(process.env.__NEXT_ASSET_PREFIX || '')
const socket = new window.WebSocket(`${socketUrl}/_next/webpack-hmr`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { parseStack } from '../internal/helpers/parseStack'
import { Base } from '../internal/styles/Base'
import { ComponentStyles } from '../internal/styles/ComponentStyles'
import { CssReset } from '../internal/styles/CssReset'
import { RootLayoutMissingTagsError } from '../internal/container/root-layout-missing-tags-error'

interface ReactDevOverlayState {
reactError: SupportedErrorEvent | null
Expand Down Expand Up @@ -51,7 +52,9 @@ class ReactDevOverlay extends React.PureComponent<

const hasBuildError = state.buildError != null
const hasRuntimeErrors = Boolean(state.errors.length)
const isMounted = hasBuildError || hasRuntimeErrors || reactError
const hasMissingTags = Boolean(state.rootLayoutMissingTags)
const isMounted =
hasBuildError || hasRuntimeErrors || reactError || hasMissingTags

return (
<>
Expand All @@ -68,8 +71,11 @@ class ReactDevOverlay extends React.PureComponent<
<CssReset />
<Base />
<ComponentStyles />

{hasBuildError ? (
{state.rootLayoutMissingTags ? (
<RootLayoutMissingTagsError
missingTags={state.rootLayoutMissingTags}
/>
) : hasBuildError ? (
<BuildError
message={state.buildError!}
versionInfo={state.versionInfo}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const INITIAL_OVERLAY_STATE: OverlayState = {
notFound: false,
refreshState: { type: 'idle' },
versionInfo: { installed: '0.0.0', staleness: 'unknown' },
rootLayoutMissingTags: null,
}

interface BuildOkAction {
Expand Down Expand Up @@ -64,6 +65,7 @@ export interface OverlayState {
nextId: number
buildError: string | null
errors: SupportedErrorEvent[]
rootLayoutMissingTags: string[] | null
refreshState: FastRefreshState
versionInfo: VersionInfo
notFound: boolean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as React from 'react'
import type { VersionInfo } from '../../../../../server/dev/parse-version-info'
import { Dialog, DialogContent, DialogHeader } from '../components/Dialog'
import { Overlay } from '../components/Overlay'
import { VersionStalenessInfo } from '../components/VersionStalenessInfo'
import { HotlinkedText } from '../components/hot-linked-text'

export type RootLayoutMissingTagsErrorProps = {
missingTags: string[]
versionInfo?: VersionInfo
}

export const RootLayoutMissingTagsError: React.FC<RootLayoutMissingTagsErrorProps> =
function RootLayoutMissingTagsError({ missingTags, versionInfo }) {
const noop = React.useCallback(() => {}, [])
return (
<Overlay>
<Dialog
type="error"
aria-labelledby="nextjs__container_errors_label"
aria-describedby="nextjs__container_errors_desc"
onClose={noop}
>
<DialogContent>
<DialogHeader className="nextjs-container-errors-header">
<h3 id="nextjs__container_errors_label">
Missing required html tags
</h3>
{versionInfo ? <VersionStalenessInfo {...versionInfo} /> : null}
<p
id="nextjs__container_errors_desc"
className="nextjs__container_errors_desc nextjs__container_errors_desc--error"
>
<HotlinkedText
text={`The following tags are missing in the Root Layout: ${missingTags
.map((tagName) => `<${tagName}>`)
.join(
', '
)}.\nRead more at https://nextjs.org/docs/messages/missing-root-layout-tags`}
/>
</p>
</DialogHeader>
</DialogContent>
</Dialog>
</Overlay>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export function ComponentStyles() {
${leftRightDialogHeader}
${codeFrame}
${terminal}
${buildErrorStyles}
${containerErrorStyles}
${containerRuntimeErrorStyles}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Root({ children }) {
return children
}
64 changes: 64 additions & 0 deletions test/development/app-dir/missing-required-html-tags/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { nextTestSetup } from 'e2e-utils'
import { getRedboxDescription, hasRedbox, retry } from 'next-test-utils'

describe('app-dir - missing required html tags', () => {
const { next } = nextTestSetup({ files: __dirname })

it('should show error overlay', async () => {
const browser = await next.browser('/')

expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(`
"The following tags are missing in the Root Layout: <html>, <body>.
Read more at https://nextjs.org/docs/messages/missing-root-layout-tags"
`)
})

it('should hmr when you fix the error', async () => {
const browser = await next.browser('/')

await next.patchFile('app/layout.js', (code) =>
code.replace('return children', 'return <body>{children}</body>')
)

expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(`
"The following tags are missing in the Root Layout: <html>.
Read more at https://nextjs.org/docs/messages/missing-root-layout-tags"
`)

await next.patchFile('app/layout.js', (code) =>
code.replace(
'return <body>{children}</body>',
'return <html><body>{children}</body></html>'
)
)

expect(await hasRedbox(browser)).toBe(false)
expect(await browser.elementByCss('p').text()).toBe('hello world')

// Reintroduce the bug, but only missing html tag
await next.patchFile('app/layout.js', (code) =>
code.replace(
'return <html><body>{children}</body></html>',
'return children'
)
)

await retry(async () => {
expect(await hasRedbox(browser)).toBe(true)
})

// Fix the issue again
await next.patchFile('app/layout.js', (code) =>
code.replace(
'return children',
'return <html><body>{children}</body></html>'
)
)

await retry(async () => {
expect(await hasRedbox(browser)).toBe(false)
})
})
})

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

0 comments on commit 27ed782

Please sign in to comment.