From dca75ea651da10bf6d279f0cba84bb25f6359050 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Fri, 17 Jan 2025 04:29:52 +0900 Subject: [PATCH 1/6] [DevOverlay] Decouple Error Overlay with DevTools Indicator --- .../_experimental/app/error-boundary.tsx | 86 +++++++++ .../app/react-dev-overlay.stories.tsx | 85 +++++++++ .../_experimental/app/react-dev-overlay.tsx | 141 ++++---------- .../dev-tools-indicator.stories.tsx | 69 +++---- .../dev-tools-indicator.tsx | 59 +++--- .../errors/error-overlay/error-overlay.tsx | 63 +++++++ .../internal/container/errors.stories.tsx | 142 +++++++------- .../internal/container/errors.tsx | 177 +++--------------- .../container/runtime-error/use-error-hook.ts | 103 ++++++++++ .../internal/helpers/get-error-by-type.ts | 2 +- .../pages/react-dev-overlay.stories.tsx | 46 +++++ .../_experimental/pages/react-dev-overlay.tsx | 58 +++--- .../components/react-dev-overlay/shared.ts | 4 +- 13 files changed, 615 insertions(+), 420 deletions(-) create mode 100644 packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx create mode 100644 packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.stories.tsx create mode 100644 packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/error-overlay/error-overlay.tsx create mode 100644 packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/runtime-error/use-error-hook.ts create mode 100644 packages/next/src/client/components/react-dev-overlay/_experimental/pages/react-dev-overlay.stories.tsx diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx new file mode 100644 index 000000000000..c86f6273f267 --- /dev/null +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx @@ -0,0 +1,86 @@ +import type { GlobalErrorComponent } from '../../../error-boundary' + +import { PureComponent } from 'react' +import { RuntimeErrorHandler } from '../internal/helpers/runtime-error-handler' + +type ReactDevOverlayProps = { + children: React.ReactNode[] + onError: (value: boolean) => void + globalError: [GlobalErrorComponent, React.ReactNode] +} + +type ReactDevOverlayState = { + isReactError: boolean + reactError: unknown +} + +function ErroredHtml({ + globalError: [GlobalError, globalErrorStyles], + error, +}: { + globalError: [GlobalErrorComponent, React.ReactNode] + error: unknown +}) { + if (!error) { + return ( + + + + + ) + } + return ( + <> + {globalErrorStyles} + + + ) +} + +export class ErrorBoundary extends PureComponent< + ReactDevOverlayProps, + ReactDevOverlayState +> { + state = { isReactError: false, reactError: null } + + componentDidUpdate( + _prevProps: ReactDevOverlayProps, + prevState: ReactDevOverlayState + ) { + if (prevState.isReactError !== this.state.isReactError) { + this.props.onError(this.state.isReactError) + } + } + + static getDerivedStateFromError(error: Error): ReactDevOverlayState { + if (!error.stack) { + return { isReactError: false, reactError: null } + } + + RuntimeErrorHandler.hadRuntimeError = true + + return { + isReactError: true, + reactError: error, + } + } + + render() { + const { children } = this.props + const [content, devtools] = children + + const fallback = ( + + ) + + return ( + <> + {this.state.isReactError ? fallback : content} + {devtools} + + ) + } +} diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.stories.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.stories.tsx new file mode 100644 index 000000000000..4da058f9efa0 --- /dev/null +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.stories.tsx @@ -0,0 +1,85 @@ +import type { Meta, StoryObj } from '@storybook/react' +import type { OverlayState } from '../../shared' + +import ReactDevOverlay from './react-dev-overlay' +import { ACTION_UNHANDLED_ERROR } from '../../shared' + +const meta: Meta = { + component: ReactDevOverlay, + parameters: { + layout: 'fullscreen', + }, +} + +export default meta +type Story = StoryObj + +const state: OverlayState = { + nextId: 0, + buildError: null, + errors: [ + { + id: 1, + event: { + type: ACTION_UNHANDLED_ERROR, + reason: Object.assign(new Error('First error message'), { + __NEXT_ERROR_CODE: 'E001', + }), + componentStackFrames: [ + { + file: 'app/page.tsx', + component: 'Home', + lineNumber: 10, + column: 5, + canOpenInEditor: true, + }, + ], + frames: [ + { + file: 'app/page.tsx', + methodName: 'Home', + arguments: [], + lineNumber: 10, + column: 5, + }, + ], + }, + }, + { + id: 2, + event: { + type: ACTION_UNHANDLED_ERROR, + reason: Object.assign(new Error('Second error message'), { + __NEXT_ERROR_CODE: 'E002', + }), + frames: [], + }, + }, + { + id: 3, + event: { + type: ACTION_UNHANDLED_ERROR, + reason: Object.assign(new Error('Third error message'), { + __NEXT_ERROR_CODE: 'E003', + }), + frames: [], + }, + }, + ], + refreshState: { type: 'idle' }, + rootLayoutMissingTags: [], + notFound: false, + staticIndicator: false, + debugInfo: { devtoolsFrontendUrl: undefined }, + versionInfo: { + installed: '14.2.0', + staleness: 'fresh', + }, +} + +export const Default: Story = { + args: { + state, + children:
Application Content
, + }, +} diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx index 55f9e9cb7c31..b72cb020f435 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx @@ -1,115 +1,52 @@ import type { OverlayState } from '../../shared' -import type { Dispatcher } from '../../app/hot-reloader-client' - -import React from 'react' +import type { GlobalErrorComponent } from '../../../error-boundary' +import { useState } from 'react' +import { ErrorBoundary } from './error-boundary' import { ShadowPortal } from '../internal/components/shadow-portal' -import { BuildError } from '../internal/container/build-error' -import { Errors } from '../internal/container/errors' import { Base } from '../internal/styles/base' import { ComponentStyles } from '../internal/styles/component-styles' import { CssReset } from '../internal/styles/css-reset' -import { RootLayoutMissingTagsError } from '../internal/container/root-layout-missing-tags-error' -import { RuntimeErrorHandler } from '../internal/helpers/runtime-error-handler' import { Colors } from '../internal/styles/colors' -import type { GlobalErrorComponent } from '../../../error-boundary' - -function ErroredHtml({ - globalError: [GlobalError, globalErrorStyles], - error, +import { ErrorOverlay } from '../internal/components/errors/error-overlay/error-overlay' +import { DevToolsIndicator } from '../internal/components/errors/dev-tools-indicator/dev-tools-indicator' +import { useErrorHook } from '../internal/container/runtime-error/use-error-hook' + +export default function ReactDevOverlay({ + state, + globalError, + children, }: { + state: OverlayState globalError: [GlobalErrorComponent, React.ReactNode] - error: unknown + children: React.ReactNode }) { - if (!error) { - return ( - - - - - ) - } + const [isErrorOverlayOpen, setIsErrorOverlayOpen] = useState(false) + const { readyErrors } = useErrorHook({ errors: state.errors, isAppDir: true }) + return ( - <> - {globalErrorStyles} - - + + {children} + + + + + + + + + + + + ) } - -interface ReactDevOverlayState { - reactError?: unknown - isReactError: boolean -} -export default class ReactDevOverlay extends React.PureComponent< - { - state: OverlayState - dispatcher?: Dispatcher - globalError: [GlobalErrorComponent, React.ReactNode] - children: React.ReactNode - }, - ReactDevOverlayState -> { - state = { - reactError: null, - isReactError: false, - } - - static getDerivedStateFromError(error: Error): ReactDevOverlayState { - if (!error.stack) return { isReactError: false } - - RuntimeErrorHandler.hadRuntimeError = true - return { - isReactError: true, - } - } - - render() { - const { state, children, globalError } = this.props - const { isReactError, reactError } = this.state - - const hasBuildError = state.buildError != null - const hasStaticIndicator = state.staticIndicator - const debugInfo = state.debugInfo - - const isTurbopack = !!process.env.TURBOPACK - - return ( - <> - {isReactError ? ( - - ) : ( - children - )} - - - - - - {state.rootLayoutMissingTags?.length ? ( - - ) : hasBuildError ? ( - - ) : ( - - )} - - - ) - } -} diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/dev-tools-indicator/dev-tools-indicator.stories.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/dev-tools-indicator/dev-tools-indicator.stories.tsx index 1525b1539fb9..16660b354ac8 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/dev-tools-indicator/dev-tools-indicator.stories.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/dev-tools-indicator/dev-tools-indicator.stories.tsx @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react' import { DevToolsIndicator } from './dev-tools-indicator' import { withShadowPortal } from '../../../storybook/with-shadow-portal' import type { VersionInfo } from '../../../../../../../../server/dev/parse-version-info' +import type { OverlayState } from '../../../../../shared' const meta: Meta = { component: DevToolsIndicator, @@ -42,65 +43,51 @@ const mockVersionInfo: VersionInfo = { staleness: 'stale-major', } -// Mock error for stories -const mockError = { - id: 1, - runtime: true as const, - error: new Error('Test error'), - frames: [ - { - error: true, - reason: null, - external: false, - ignored: false, - sourceStackFrame: { - file: 'test.js', - methodName: '', - arguments: [], - lineNumber: 1, - column: 1, - }, - }, - ], +const state: OverlayState = { + nextId: 1, + buildError: null, + errors: [], + refreshState: { type: 'idle' }, + rootLayoutMissingTags: [], + versionInfo: mockVersionInfo, + notFound: false, + staticIndicator: false, + debugInfo: { devtoolsFrontendUrl: undefined }, } export const NoErrors: Story = { args: { - hasStaticIndicator: false, - readyErrors: [], - fullscreen: () => console.log('Fullscreen clicked'), - hide: () => console.log('Hide clicked'), - versionInfo: mockVersionInfo, - isTurbopack: false, + readyErrorsLength: 0, + state, + setIsErrorOverlayOpen: () => {}, }, } export const SingleError: Story = { args: { - hasStaticIndicator: false, - readyErrors: [mockError], - fullscreen: () => console.log('Fullscreen clicked'), - hide: () => console.log('Hide clicked'), - versionInfo: mockVersionInfo, + readyErrorsLength: 1, + state, + setIsErrorOverlayOpen: () => {}, }, } export const MultipleErrors: Story = { args: { - hasStaticIndicator: false, - readyErrors: [mockError, { ...mockError, id: 2 }, { ...mockError, id: 3 }], - fullscreen: () => console.log('Fullscreen clicked'), - hide: () => console.log('Hide clicked'), - versionInfo: mockVersionInfo, + readyErrorsLength: 3, + state, + setIsErrorOverlayOpen: () => {}, }, } export const WithStaticIndicator: Story = { args: { - hasStaticIndicator: true, - readyErrors: [mockError], - fullscreen: () => console.log('Fullscreen clicked'), - hide: () => console.log('Hide clicked'), - versionInfo: mockVersionInfo, + readyErrorsLength: 3, + state: { + ...state, + staticIndicator: true, + }, + setIsErrorOverlayOpen: () => { + console.log('setIsErrorOverlayOpen called') + }, }, } diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/dev-tools-indicator/dev-tools-indicator.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/dev-tools-indicator/dev-tools-indicator.tsx index 3e80ee2bcc09..23145883d633 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/dev-tools-indicator/dev-tools-indicator.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/dev-tools-indicator/dev-tools-indicator.tsx @@ -1,39 +1,52 @@ -import type { VersionInfo } from '../../../../../../../../server/dev/parse-version-info' -import type { ReadyRuntimeError } from '../../../helpers/get-error-by-type' +import type { OverlayState } from '../../../../../shared' + +import { useState, useEffect, useRef } from 'react' import { Toast } from '../../toast' -import React, { useState, useEffect, useRef } from 'react' import { NextLogo } from './internal/next-logo' import { useIsDevBuilding } from '../../../../../../../dev/dev-build-indicator/internal/initialize-for-new-overlay' import { useIsDevRendering } from './internal/dev-render-indicator' import { useDelayedRender } from './internal/use-delayed-render' +import { useKeyboardShortcut } from '../../../hooks/use-keyboard-shortcut' +import { MODIFIERS } from '../../../hooks/use-keyboard-shortcut' // TODO: test a11y // TODO: add E2E tests to cover different scenarios export function DevToolsIndicator({ - versionInfo, - hasStaticIndicator, - readyErrors, - fullscreen, - hide, - isTurbopack, + state, + readyErrorsLength, + setIsErrorOverlayOpen, }: { - versionInfo: VersionInfo | undefined - readyErrors: ReadyRuntimeError[] - fullscreen: () => void - hide: () => void - hasStaticIndicator?: boolean - isTurbopack: boolean + state: OverlayState + readyErrorsLength: number + setIsErrorOverlayOpen: (value: boolean) => void }) { + const [isDevToolsIndicatorOpen, setIsDevToolsIndicatorOpen] = useState(true) + // Register `(cmd|ctrl) + .` to show/hide the error indicator. + useKeyboardShortcut({ + key: '.', + modifiers: [MODIFIERS.CTRL_CMD], + callback: () => { + setIsDevToolsIndicatorOpen(!isDevToolsIndicatorOpen) + setIsErrorOverlayOpen(!isDevToolsIndicatorOpen) + }, + }) + return ( - + isDevToolsIndicatorOpen && ( + { + setIsErrorOverlayOpen(true) + }} + issueCount={readyErrorsLength} + isStaticRoute={state.staticIndicator} + hide={() => { + setIsDevToolsIndicatorOpen(false) + }} + isTurbopack={!!process.env.TURBOPACK} + /> + ) ) } diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/error-overlay/error-overlay.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/error-overlay/error-overlay.tsx new file mode 100644 index 000000000000..3cb7574bc8fd --- /dev/null +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/error-overlay/error-overlay.tsx @@ -0,0 +1,63 @@ +import type { OverlayState } from '../../../../../shared' +import type { ReadyRuntimeError } from '../../../helpers/get-error-by-type' + +import { BuildError } from '../../../container/build-error' +import { Errors } from '../../../container/errors' +import { RootLayoutMissingTagsError } from '../../../container/root-layout-missing-tags-error' + +export function ErrorOverlay({ + state, + readyErrors, + isErrorOverlayOpen, + setIsErrorOverlayOpen, +}: { + state: OverlayState + readyErrors: ReadyRuntimeError[] + isErrorOverlayOpen: boolean + setIsErrorOverlayOpen: (value: boolean) => void +}) { + const isTurbopack = !!process.env.TURBOPACK + + if (!!state.rootLayoutMissingTags?.length) { + return ( + + ) + } + + if (state.buildError !== null) { + return ( + + ) + } + + // No Runtime Errors. + if (!state.errors.length) { + return null + } + + if (!isErrorOverlayOpen) { + return null + } + + return ( + { + setIsErrorOverlayOpen(false) + }} + /> + ) +} diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/errors.stories.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/errors.stories.tsx index 1aa85b2329a0..c65058931d2c 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/errors.stories.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/errors.stories.tsx @@ -1,4 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react' +import type { SupportedErrorEvent } from '../../../internal/container/Errors' +import type { ReadyRuntimeError } from '../helpers/get-error-by-type' + import { Errors } from './errors' import { withShadowPortal } from '../storybook/with-shadow-portal' import { ACTION_UNHANDLED_ERROR } from '../../../shared' @@ -14,75 +17,87 @@ const meta: Meta = { export default meta type Story = StoryObj -export const Default: Story = { - args: { - isAppDir: true, - errors: [ - { - id: 1, - event: { - type: ACTION_UNHANDLED_ERROR, - reason: Object.assign(new Error('First error message'), { - __NEXT_ERROR_CODE: 'E001', - }), - componentStackFrames: [ - { - file: 'app/page.tsx', - component: 'Home', - lineNumber: 10, - column: 5, - canOpenInEditor: true, - }, - ], - frames: [ - { - file: 'app/page.tsx', - methodName: 'Home', - arguments: [], - lineNumber: 10, - column: 5, - }, - ], - }, - }, - { - id: 2, - event: { - type: ACTION_UNHANDLED_ERROR, - reason: Object.assign(new Error('Second error message'), { - __NEXT_ERROR_CODE: 'E002', - }), - frames: [], - }, - }, - { - id: 3, - event: { - type: ACTION_UNHANDLED_ERROR, - reason: Object.assign(new Error('Third error message'), { - __NEXT_ERROR_CODE: 'E003', - }), - frames: [], +const errors: SupportedErrorEvent[] = [ + { + id: 1, + event: { + type: ACTION_UNHANDLED_ERROR, + reason: Object.assign(new Error('First error message'), { + __NEXT_ERROR_CODE: 'E001', + }), + componentStackFrames: [ + { + file: 'app/page.tsx', + component: 'Home', + lineNumber: 10, + column: 5, + canOpenInEditor: true, }, - }, - { - id: 4, - event: { - type: ACTION_UNHANDLED_ERROR, - reason: Object.assign(new Error('Fourth error message'), { - __NEXT_ERROR_CODE: 'E004', - }), - frames: [], + ], + frames: [ + { + file: 'app/page.tsx', + methodName: 'Home', + arguments: [], + lineNumber: 10, + column: 5, }, - }, - ], + ], + }, + }, + { + id: 2, + event: { + type: ACTION_UNHANDLED_ERROR, + reason: Object.assign(new Error('Second error message'), { + __NEXT_ERROR_CODE: 'E002', + }), + frames: [], + }, + }, + { + id: 3, + event: { + type: ACTION_UNHANDLED_ERROR, + reason: Object.assign(new Error('Third error message'), { + __NEXT_ERROR_CODE: 'E003', + }), + frames: [], + }, + }, + { + id: 4, + event: { + type: ACTION_UNHANDLED_ERROR, + reason: Object.assign(new Error('Fourth error message'), { + __NEXT_ERROR_CODE: 'E004', + }), + frames: [], + }, + }, +] + +const readyErrors: ReadyRuntimeError[] = [ + { + id: 1, + runtime: true, + error: errors[0].event.reason, + frames: [], + }, +] + +export const Default: Story = { + args: { + errors, + readyErrors, versionInfo: { installed: '15.0.0', staleness: 'fresh', }, - initialDisplayState: 'fullscreen', hasStaticIndicator: true, isTurbopack: true, + debugInfo: { devtoolsFrontendUrl: undefined }, + onClose: () => {}, }, } @@ -96,13 +111,11 @@ export const Turbopack: Story = { export const Minimized: Story = { args: { ...Default.args, - initialDisplayState: 'minimized', }, } export const WithHydrationWarning: Story = { args: { - isAppDir: true, errors: [ { id: 1, @@ -140,5 +153,8 @@ export const WithHydrationWarning: Story = { }, }, ], + readyErrors: [], + debugInfo: { devtoolsFrontendUrl: undefined }, + onClose: () => {}, }, } diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/errors.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/errors.tsx index 60294c556799..7be1332d6f83 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/errors.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/errors.tsx @@ -1,13 +1,6 @@ -import { useState, useEffect, useMemo, useCallback } from 'react' -import { - ACTION_UNHANDLED_ERROR, - ACTION_UNHANDLED_REJECTION, - type UnhandledErrorAction, - type UnhandledRejectionAction, -} from '../../../shared' +import { useState, useMemo, useEffect } from 'react' import type { DebugInfo } from '../../../types' import { Overlay } from '../components/overlay' -import { getErrorByType } from '../helpers/get-error-by-type' import type { ReadyRuntimeError } from '../helpers/get-error-by-type' import { noop as css } from '../helpers/noop-template' import { RuntimeError } from './runtime-error' @@ -24,29 +17,21 @@ import { isUnhandledConsoleOrRejection, } from '../../../../errors/console-error' import { extractNextErrorCode } from '../../../../../../lib/error-telemetry-utils' -import { DevToolsIndicator } from '../components/errors/dev-tools-indicator/dev-tools-indicator' import { ErrorOverlayLayout } from '../components/errors/error-overlay-layout/error-overlay-layout' -import { useKeyboardShortcut } from '../hooks/use-keyboard-shortcut' -import { MODIFIERS } from '../hooks/use-keyboard-shortcut' +import type { SupportedErrorEvent } from './runtime-error/use-error-hook' -export type SupportedErrorEvent = { - id: number - event: UnhandledErrorAction | UnhandledRejectionAction -} export type ErrorsProps = { - isAppDir: boolean errors: SupportedErrorEvent[] - initialDisplayState: DisplayState + readyErrors: ReadyRuntimeError[] isTurbopack: boolean - versionInfo?: VersionInfo - hasStaticIndicator?: boolean - debugInfo?: DebugInfo + versionInfo: VersionInfo + hasStaticIndicator: boolean + debugInfo: DebugInfo + onClose: () => void } type ReadyErrorEvent = ReadyRuntimeError -type DisplayState = 'minimized' | 'fullscreen' | 'hidden' - function isNextjsLink(text: string): boolean { return text.startsWith('https://nextjs.org') } @@ -89,96 +74,30 @@ function ErrorDescription({ ) } -function getErrorSignature(ev: SupportedErrorEvent): string { - const { event } = ev - switch (event.type) { - case ACTION_UNHANDLED_ERROR: - case ACTION_UNHANDLED_REJECTION: { - return `${event.reason.name}::${event.reason.message}::${event.reason.stack}` - } - default: { - } - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _: never = event as never - return '' -} - export function Errors({ - isAppDir, errors, - initialDisplayState, - hasStaticIndicator, + readyErrors, debugInfo, versionInfo, isTurbopack, + onClose, }: ErrorsProps) { - const [lookups, setLookups] = useState( - {} as { [eventId: string]: ReadyErrorEvent } - ) - - const [readyErrors, nextError] = useMemo< - [ReadyErrorEvent[], SupportedErrorEvent | null] - >(() => { - let ready: ReadyErrorEvent[] = [] - let next: SupportedErrorEvent | null = null - - // Ensure errors are displayed in the order they occurred in: - for (let idx = 0; idx < errors.length; ++idx) { - const e = errors[idx] - const { id } = e - if (id in lookups) { - ready.push(lookups[id]) - continue - } - - // Check for duplicate errors - if (idx > 0) { - const prev = errors[idx - 1] - if (getErrorSignature(prev) === getErrorSignature(e)) { - continue - } + useEffect(() => { + // Close the error overlay when pressing escape + function handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape') { + onClose() } - - next = e - break } - return [ready, next] - }, [errors, lookups]) + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [onClose]) const isLoading = useMemo(() => { return readyErrors.length < 1 && Boolean(errors.length) }, [errors.length, readyErrors.length]) - useEffect(() => { - if (nextError == null) { - return - } - let mounted = true - - getErrorByType(nextError, isAppDir).then( - (resolved) => { - // We don't care if the desired error changed while we were resolving, - // thus we're not tracking it using a ref. Once the work has been done, - // we'll store it. - if (mounted) { - setLookups((m) => ({ ...m, [resolved.id]: resolved })) - } - }, - () => { - // TODO: handle this, though an edge case - } - ) - - return () => { - mounted = false - } - }, [nextError, isAppDir]) - - const [displayState, setDisplayState] = - useState(initialDisplayState) const [activeIdx, setActiveIndex] = useState(0) const activeError = useMemo( @@ -186,67 +105,15 @@ export function Errors({ [activeIdx, readyErrors] ) - const minimize = useCallback(() => setDisplayState('minimized'), []) - - // Reset component state when there are no errors to be displayed. - // Note: We show the dev tools indicator in minimized state even with no errors - // as it serves as a persistent development tools access point - useEffect(() => { - if (errors.length < 1) { - setLookups({}) - minimize() - setActiveIndex(0) - } - }, [errors.length, minimize]) - - useEffect(() => { - // Close the error overlay when pressing escape - function handleKeyDown(event: KeyboardEvent) { - if (event.key === 'Escape') { - setDisplayState('minimized') - } - } - - document.addEventListener('keydown', handleKeyDown) - return () => document.removeEventListener('keydown', handleKeyDown) - }, []) - - const hide = useCallback(() => setDisplayState('hidden'), []) - const fullscreen = useCallback(() => setDisplayState('fullscreen'), []) - - // Register `(cmd|ctrl) + .` to show/hide the error indicator. - useKeyboardShortcut({ - key: '.', - modifiers: [MODIFIERS.CTRL_CMD], - callback: () => { - setDisplayState((prev) => (prev === 'hidden' ? 'minimized' : 'hidden')) - }, - }) - - if (displayState === 'hidden') { - return null - } - - const noIssues = errors.length < 1 || activeError == null - - if (noIssues || displayState === 'minimized') { - return ( - - ) - } - if (isLoading) { // TODO: better loading state return } + if (!activeError) { + return null + } + const error = activeError.error const isServerError = ['server', 'edge-server'].includes( getErrorSource(error) || '' @@ -287,7 +154,7 @@ export function Errors({ errorMessage={ } - onClose={isServerError ? undefined : minimize} + onClose={isServerError ? undefined : onClose} debugInfo={debugInfo} error={error} readyErrors={readyErrors} diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/runtime-error/use-error-hook.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/runtime-error/use-error-hook.ts new file mode 100644 index 000000000000..907e7888e14a --- /dev/null +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/runtime-error/use-error-hook.ts @@ -0,0 +1,103 @@ +import type { ReadyRuntimeError } from '../../helpers/get-error-by-type' +import type { + UnhandledErrorAction, + UnhandledRejectionAction, +} from '../../../../shared' + +import { useMemo, useState, useEffect } from 'react' +import { getErrorByType } from '../../helpers/get-error-by-type' +import { + ACTION_UNHANDLED_ERROR, + ACTION_UNHANDLED_REJECTION, +} from '../../../../shared' + +export type SupportedErrorEvent = { + id: number + event: UnhandledErrorAction | UnhandledRejectionAction +} + +function getErrorSignature(ev: SupportedErrorEvent): string { + const { event } = ev + switch (event.type) { + case ACTION_UNHANDLED_ERROR: + case ACTION_UNHANDLED_REJECTION: { + return `${event.reason.name}::${event.reason.message}::${event.reason.stack}` + } + default: { + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _: never = event as never + return '' +} + +export function useErrorHook({ + errors, + isAppDir, +}: { + errors: SupportedErrorEvent[] + isAppDir: boolean +}) { + const [lookups, setLookups] = useState<{ + [eventId: string]: ReadyRuntimeError + }>({}) + const [readyErrors, nextError] = useMemo< + [ReadyRuntimeError[], SupportedErrorEvent | null] + >(() => { + let ready: ReadyRuntimeError[] = [] + let next: SupportedErrorEvent | null = null + + // Ensure errors are displayed in the order they occurred in: + for (let idx = 0; idx < errors.length; ++idx) { + const e = errors[idx] + const { id } = e + if (id in lookups) { + ready.push(lookups[id]) + continue + } + + // Check for duplicate errors + if (idx > 0) { + const prev = errors[idx - 1] + if (getErrorSignature(prev) === getErrorSignature(e)) { + continue + } + } + + next = e + break + } + + return [ready, next] + }, [errors, lookups]) + + useEffect(() => { + if (nextError == null) { + return + } + let mounted = true + + getErrorByType(nextError, isAppDir).then( + (resolved) => { + // We don't care if the desired error changed while we were resolving, + // thus we're not tracking it using a ref. Once the work has been done, + // we'll store it. + if (mounted) { + setLookups((m) => ({ ...m, [resolved.id]: resolved })) + } + }, + () => { + // TODO: handle this, though an edge case + } + ) + + return () => { + mounted = false + } + }, [nextError, isAppDir]) + + return { + readyErrors, + } +} diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/get-error-by-type.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/get-error-by-type.ts index d3a9d2eb8809..50b9c45a71c5 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/get-error-by-type.ts +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/get-error-by-type.ts @@ -2,7 +2,7 @@ import { ACTION_UNHANDLED_ERROR, ACTION_UNHANDLED_REJECTION, } from '../../../shared' -import type { SupportedErrorEvent } from '../container/errors' +import type { SupportedErrorEvent } from '../container/runtime-error/use-error-hook' import { getOriginalStackFrames } from './stack-frame' import type { OriginalStackFrame } from './stack-frame' import type { ComponentStackFrame } from './parse-component-stack' diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/pages/react-dev-overlay.stories.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/pages/react-dev-overlay.stories.tsx new file mode 100644 index 000000000000..a11d1b8b700a --- /dev/null +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/pages/react-dev-overlay.stories.tsx @@ -0,0 +1,46 @@ +import type { Meta, StoryObj } from '@storybook/react' +import ReactDevOverlay from './react-dev-overlay' +import { useEffect } from 'react' +import { ACTION_UNHANDLED_ERROR, type UnhandledErrorAction } from '../../shared' + +const meta: Meta = { + component: ReactDevOverlay, + parameters: { + layout: 'fullscreen', + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + children:
Application Content
, + }, +} + +export const WithRuntimeError: Story = { + args: { + children:
Application Content
, + }, + decorators: [ + (Story) => { + // Simulate dispatching an error event + useEffect(() => { + const event: UnhandledErrorAction = { + type: ACTION_UNHANDLED_ERROR, + reason: new Error('Runtime error example'), + frames: [], + } + // Dispatch the error event after a short delay to ensure component is mounted + setTimeout(() => { + window.dispatchEvent( + new CustomEvent('unhandledError', { detail: event }) + ) + }, 100) + }, []) + + return + }, + ], +} diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/pages/react-dev-overlay.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/pages/react-dev-overlay.tsx index 9f0cd44097e0..40e1684ec061 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/pages/react-dev-overlay.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/pages/react-dev-overlay.tsx @@ -1,8 +1,7 @@ import * as React from 'react' +import { useState } from 'react' import { ShadowPortal } from '../internal/components/shadow-portal' -import { BuildError } from '../internal/container/build-error' -import { Errors } from '../internal/container/errors' import { Base } from '../internal/styles/base' import { ComponentStyles } from '../internal/styles/component-styles' import { CssReset } from '../internal/styles/css-reset' @@ -10,6 +9,9 @@ import { CssReset } from '../internal/styles/css-reset' import { ErrorBoundary } from '../../pages/error-boundary' import { usePagesReactDevOverlay } from '../../pages/hooks' import { Colors } from '../internal/styles/colors' +import { ErrorOverlay } from '../internal/components/errors/error-overlay/error-overlay' +import { DevToolsIndicator } from '../internal/components/errors/dev-tools-indicator/dev-tools-indicator' +import { useErrorHook } from '../internal/container/runtime-error/use-error-hook' export type ErrorType = 'runtime' | 'build' @@ -18,50 +20,40 @@ interface ReactDevOverlayProps { } export default function ReactDevOverlay({ children }: ReactDevOverlayProps) { - const { - isMounted, - hasBuildError, - hasRuntimeErrors, - state, - onComponentError, - } = usePagesReactDevOverlay() + const { isMounted, state, onComponentError, hasRuntimeErrors } = + usePagesReactDevOverlay() - const isTurbopack = !!process.env.TURBOPACK + const [isErrorOverlayOpen, setIsErrorOverlayOpen] = useState(hasRuntimeErrors) + + const { readyErrors } = useErrorHook({ + errors: state.errors, + isAppDir: false, + }) return ( <> {children ?? null} + - {hasBuildError ? ( - - ) : hasRuntimeErrors ? ( - - ) : ( - - )} + + + ) diff --git a/packages/next/src/client/components/react-dev-overlay/shared.ts b/packages/next/src/client/components/react-dev-overlay/shared.ts index b465a8f661c3..77540bb7e6ba 100644 --- a/packages/next/src/client/components/react-dev-overlay/shared.ts +++ b/packages/next/src/client/components/react-dev-overlay/shared.ts @@ -21,7 +21,7 @@ export interface OverlayState { versionInfo: VersionInfo notFound: boolean staticIndicator: boolean - debugInfo: DebugInfo | undefined + debugInfo: DebugInfo } export const ACTION_STATIC_INDICATOR = 'static-indicator' @@ -109,7 +109,7 @@ export const INITIAL_OVERLAY_STATE: OverlayState = { refreshState: { type: 'idle' }, rootLayoutMissingTags: [], versionInfo: { installed: '0.0.0', staleness: 'unknown' }, - debugInfo: undefined, + debugInfo: { devtoolsFrontendUrl: undefined }, } export function useErrorOverlayReducer() { From d74d12a7ef9607855e984859026ecce423f88040 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Fri, 17 Jan 2025 18:15:34 +0900 Subject: [PATCH 2/6] unable to do story yet --- .../pages/react-dev-overlay.stories.tsx | 46 ------------------- 1 file changed, 46 deletions(-) delete mode 100644 packages/next/src/client/components/react-dev-overlay/_experimental/pages/react-dev-overlay.stories.tsx diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/pages/react-dev-overlay.stories.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/pages/react-dev-overlay.stories.tsx deleted file mode 100644 index a11d1b8b700a..000000000000 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/pages/react-dev-overlay.stories.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react' -import ReactDevOverlay from './react-dev-overlay' -import { useEffect } from 'react' -import { ACTION_UNHANDLED_ERROR, type UnhandledErrorAction } from '../../shared' - -const meta: Meta = { - component: ReactDevOverlay, - parameters: { - layout: 'fullscreen', - }, -} - -export default meta -type Story = StoryObj - -export const Default: Story = { - args: { - children:
Application Content
, - }, -} - -export const WithRuntimeError: Story = { - args: { - children:
Application Content
, - }, - decorators: [ - (Story) => { - // Simulate dispatching an error event - useEffect(() => { - const event: UnhandledErrorAction = { - type: ACTION_UNHANDLED_ERROR, - reason: new Error('Runtime error example'), - frames: [], - } - // Dispatch the error event after a short delay to ensure component is mounted - setTimeout(() => { - window.dispatchEvent( - new CustomEvent('unhandledError', { detail: event }) - ) - }, 100) - }, []) - - return - }, - ], -} From 53225cb08b3987bf1d3250db4f9e6afbde79ee57 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Sat, 18 Jan 2025 23:55:45 +0900 Subject: [PATCH 3/6] fix error boundary use component did catch --- .../_experimental/app/error-boundary.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx index c86f6273f267..7616b5be6184 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx @@ -43,16 +43,7 @@ export class ErrorBoundary extends PureComponent< > { state = { isReactError: false, reactError: null } - componentDidUpdate( - _prevProps: ReactDevOverlayProps, - prevState: ReactDevOverlayState - ) { - if (prevState.isReactError !== this.state.isReactError) { - this.props.onError(this.state.isReactError) - } - } - - static getDerivedStateFromError(error: Error): ReactDevOverlayState { + static getDerivedStateFromError(error: Error) { if (!error.stack) { return { isReactError: false, reactError: null } } @@ -65,6 +56,10 @@ export class ErrorBoundary extends PureComponent< } } + componentDidCatch() { + this.props.onError(this.state.isReactError) + } + render() { const { children } = this.props const [content, devtools] = children From 13dbcea29beaeb0d86343dbcc5ff23f96c9243ae Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Mon, 20 Jan 2025 21:55:24 +0900 Subject: [PATCH 4/6] runtime error handler shouldnt be forked --- .../react-dev-overlay/_experimental/app/error-boundary.tsx | 2 +- .../_experimental/internal/helpers/runtime-error-handler.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/runtime-error-handler.ts diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx index 7616b5be6184..932ecdc1916e 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx @@ -1,7 +1,7 @@ import type { GlobalErrorComponent } from '../../../error-boundary' import { PureComponent } from 'react' -import { RuntimeErrorHandler } from '../internal/helpers/runtime-error-handler' +import { RuntimeErrorHandler } from '../../../errors/runtime-error-handler' type ReactDevOverlayProps = { children: React.ReactNode[] diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/runtime-error-handler.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/runtime-error-handler.ts deleted file mode 100644 index 36622efd1cb1..000000000000 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/runtime-error-handler.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const RuntimeErrorHandler = { - hadRuntimeError: false, -} From ef75f375c3583ada8b0747a6fc8b2eae1f5011cb Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Tue, 21 Jan 2025 11:30:17 +0900 Subject: [PATCH 5/6] rename as DevToolsErrorBoundary Co-authored-by: Jiachi Liu --- .../_experimental/app/error-boundary.tsx | 10 +++--- .../_experimental/app/react-dev-overlay.tsx | 9 +++-- .../_experimental/pages/error-boundary.tsx | 35 +++++++++++++++++++ .../_experimental/pages/react-dev-overlay.tsx | 6 ++-- 4 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 packages/next/src/client/components/react-dev-overlay/_experimental/pages/error-boundary.tsx diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx index 932ecdc1916e..65107f9c8265 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx @@ -3,13 +3,13 @@ import type { GlobalErrorComponent } from '../../../error-boundary' import { PureComponent } from 'react' import { RuntimeErrorHandler } from '../../../errors/runtime-error-handler' -type ReactDevOverlayProps = { +type DevToolsErrorBoundaryProps = { children: React.ReactNode[] onError: (value: boolean) => void globalError: [GlobalErrorComponent, React.ReactNode] } -type ReactDevOverlayState = { +type DevToolsErrorBoundaryState = { isReactError: boolean reactError: unknown } @@ -37,9 +37,9 @@ function ErroredHtml({ ) } -export class ErrorBoundary extends PureComponent< - ReactDevOverlayProps, - ReactDevOverlayState +export class DevToolsErrorBoundary extends PureComponent< + DevToolsErrorBoundaryProps, + DevToolsErrorBoundaryState > { state = { isReactError: false, reactError: null } diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx index b72cb020f435..97c73c2c4821 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx @@ -2,7 +2,7 @@ import type { OverlayState } from '../../shared' import type { GlobalErrorComponent } from '../../../error-boundary' import { useState } from 'react' -import { ErrorBoundary } from './error-boundary' +import { DevToolsErrorBoundary } from './error-boundary' import { ShadowPortal } from '../internal/components/shadow-portal' import { Base } from '../internal/styles/base' import { ComponentStyles } from '../internal/styles/component-styles' @@ -25,7 +25,10 @@ export default function ReactDevOverlay({ const { readyErrors } = useErrorHook({ errors: state.errors, isAppDir: true }) return ( - + {children} @@ -47,6 +50,6 @@ export default function ReactDevOverlay({ setIsErrorOverlayOpen={setIsErrorOverlayOpen} /> - + ) } diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/pages/error-boundary.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/pages/error-boundary.tsx new file mode 100644 index 000000000000..8eda7feccdfa --- /dev/null +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/pages/error-boundary.tsx @@ -0,0 +1,35 @@ +import * as React from 'react' + +type DevToolsErrorBoundaryProps = { + children?: React.ReactNode + onError: (error: Error, componentStack: string | null) => void + isMounted?: boolean +} +type DevToolsErrorBoundaryState = { error: Error | null } + +export class DevToolsErrorBoundary extends React.PureComponent< + DevToolsErrorBoundaryProps, + DevToolsErrorBoundaryState +> { + state = { error: null } + + static getDerivedStateFromError(error: Error) { + return { error } + } + + componentDidCatch( + error: Error, + // Loosely typed because it depends on the React version and was + // accidentally excluded in some versions. + errorInfo?: { componentStack?: string | null } + ) { + this.props.onError(error, errorInfo?.componentStack || null) + this.setState({ error }) + } + + // Explicit type is needed to avoid the generated `.d.ts` having a wide return type that could be specific to the `@types/react` version. + render(): React.ReactNode { + // The component has to be unmounted or else it would continue to error + return this.state.error ? null : this.props.children + } +} diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/pages/react-dev-overlay.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/pages/react-dev-overlay.tsx index 40e1684ec061..7cf676c91ff1 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/pages/react-dev-overlay.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/pages/react-dev-overlay.tsx @@ -6,7 +6,7 @@ import { Base } from '../internal/styles/base' import { ComponentStyles } from '../internal/styles/component-styles' import { CssReset } from '../internal/styles/css-reset' -import { ErrorBoundary } from '../../pages/error-boundary' +import { DevToolsErrorBoundary } from './error-boundary' import { usePagesReactDevOverlay } from '../../pages/hooks' import { Colors } from '../internal/styles/colors' import { ErrorOverlay } from '../internal/components/errors/error-overlay/error-overlay' @@ -32,9 +32,9 @@ export default function ReactDevOverlay({ children }: ReactDevOverlayProps) { return ( <> - + {children ?? null} - + From 22d1e8ac175239df3dbf0c0850381cd9d71223b6 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Tue, 21 Jan 2025 11:49:58 +0900 Subject: [PATCH 6/6] pass children only to the boundary Co-authored-by: Jude Gao --- .../_experimental/app/error-boundary.tsx | 12 ++---------- .../_experimental/app/react-dev-overlay.tsx | 14 ++++++++------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx index 65107f9c8265..804918cda20a 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx @@ -4,7 +4,7 @@ import { PureComponent } from 'react' import { RuntimeErrorHandler } from '../../../errors/runtime-error-handler' type DevToolsErrorBoundaryProps = { - children: React.ReactNode[] + children: React.ReactNode onError: (value: boolean) => void globalError: [GlobalErrorComponent, React.ReactNode] } @@ -61,9 +61,6 @@ export class DevToolsErrorBoundary extends PureComponent< } render() { - const { children } = this.props - const [content, devtools] = children - const fallback = ( ) - return ( - <> - {this.state.isReactError ? fallback : content} - {devtools} - - ) + return this.state.isReactError ? fallback : this.props.children } } diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx index 97c73c2c4821..589d0a56a157 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx @@ -25,11 +25,13 @@ export default function ReactDevOverlay({ const { readyErrors } = useErrorHook({ errors: state.errors, isAppDir: true }) return ( - - {children} + <> + + {children} + @@ -50,6 +52,6 @@ export default function ReactDevOverlay({ setIsErrorOverlayOpen={setIsErrorOverlayOpen} /> - + ) }