-
Notifications
You must be signed in to change notification settings - Fork 27.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[DevOverlay] Decouple Error Overlay with DevTools Indicator (#74999)
### Why? As we rendered the error overlay OR dev indicator at the `container/errors` component, decouple the indicator from the overlay and let them have their own display state. ### How? - Converted `app/react-dev-overlay` to a functional component and ported the error boundary logic into a component. - Decoupled the display state for the DevTools Indicator and the Error Overlay. - Changed the display state as visible or invisible (prev: 'minimized' | 'fullscreen' | 'hidden'). #### Before https://github.com/user-attachments/assets/024697ac-ba8e-409a-bf87-dcde1c97cf0c #### After https://github.com/user-attachments/assets/30c6e034-9c56-4852-8ec7-4e8f6ef793e5 Closes NDX-643 Closes NDX-669
- Loading branch information
1 parent
dc0f46c
commit 352c315
Showing
14 changed files
with
597 additions
and
424 deletions.
There are no files selected for viewing
73 changes: 73 additions & 0 deletions
73
packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import type { GlobalErrorComponent } from '../../../error-boundary' | ||
|
||
import { PureComponent } from 'react' | ||
import { RuntimeErrorHandler } from '../../../errors/runtime-error-handler' | ||
|
||
type DevToolsErrorBoundaryProps = { | ||
children: React.ReactNode | ||
onError: (value: boolean) => void | ||
globalError: [GlobalErrorComponent, React.ReactNode] | ||
} | ||
|
||
type DevToolsErrorBoundaryState = { | ||
isReactError: boolean | ||
reactError: unknown | ||
} | ||
|
||
function ErroredHtml({ | ||
globalError: [GlobalError, globalErrorStyles], | ||
error, | ||
}: { | ||
globalError: [GlobalErrorComponent, React.ReactNode] | ||
error: unknown | ||
}) { | ||
if (!error) { | ||
return ( | ||
<html> | ||
<head /> | ||
<body /> | ||
</html> | ||
) | ||
} | ||
return ( | ||
<> | ||
{globalErrorStyles} | ||
<GlobalError error={error} /> | ||
</> | ||
) | ||
} | ||
|
||
export class DevToolsErrorBoundary extends PureComponent< | ||
DevToolsErrorBoundaryProps, | ||
DevToolsErrorBoundaryState | ||
> { | ||
state = { isReactError: false, reactError: null } | ||
|
||
static getDerivedStateFromError(error: Error) { | ||
if (!error.stack) { | ||
return { isReactError: false, reactError: null } | ||
} | ||
|
||
RuntimeErrorHandler.hadRuntimeError = true | ||
|
||
return { | ||
isReactError: true, | ||
reactError: error, | ||
} | ||
} | ||
|
||
componentDidCatch() { | ||
this.props.onError(this.state.isReactError) | ||
} | ||
|
||
render() { | ||
const fallback = ( | ||
<ErroredHtml | ||
globalError={this.props.globalError} | ||
error={this.state.reactError} | ||
/> | ||
) | ||
|
||
return this.state.isReactError ? fallback : this.props.children | ||
} | ||
} |
85 changes: 85 additions & 0 deletions
85
...t/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof ReactDevOverlay> = { | ||
component: ReactDevOverlay, | ||
parameters: { | ||
layout: 'fullscreen', | ||
}, | ||
} | ||
|
||
export default meta | ||
type Story = StoryObj<typeof ReactDevOverlay> | ||
|
||
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: <div>Application Content</div>, | ||
}, | ||
} |
142 changes: 42 additions & 100 deletions
142
...ages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,115 +1,57 @@ | ||
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 { DevToolsErrorBoundary } 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 ( | ||
<html> | ||
<head /> | ||
<body /> | ||
</html> | ||
) | ||
} | ||
const [isErrorOverlayOpen, setIsErrorOverlayOpen] = useState(false) | ||
const { readyErrors } = useErrorHook({ errors: state.errors, isAppDir: true }) | ||
|
||
return ( | ||
<> | ||
{globalErrorStyles} | ||
<GlobalError error={error} /> | ||
<DevToolsErrorBoundary | ||
onError={setIsErrorOverlayOpen} | ||
globalError={globalError} | ||
> | ||
{children} | ||
</DevToolsErrorBoundary> | ||
|
||
<ShadowPortal> | ||
<CssReset /> | ||
<Base /> | ||
<Colors /> | ||
<ComponentStyles /> | ||
|
||
<DevToolsIndicator | ||
state={state} | ||
readyErrorsLength={readyErrors.length} | ||
setIsErrorOverlayOpen={setIsErrorOverlayOpen} | ||
/> | ||
|
||
<ErrorOverlay | ||
state={state} | ||
readyErrors={readyErrors} | ||
isErrorOverlayOpen={isErrorOverlayOpen} | ||
setIsErrorOverlayOpen={setIsErrorOverlayOpen} | ||
/> | ||
</ShadowPortal> | ||
</> | ||
) | ||
} | ||
|
||
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 ? ( | ||
<ErroredHtml globalError={globalError} error={reactError} /> | ||
) : ( | ||
children | ||
)} | ||
<ShadowPortal> | ||
<CssReset /> | ||
<Base /> | ||
<Colors /> | ||
<ComponentStyles /> | ||
{state.rootLayoutMissingTags?.length ? ( | ||
<RootLayoutMissingTagsError | ||
missingTags={state.rootLayoutMissingTags} | ||
isTurbopack={isTurbopack} | ||
/> | ||
) : hasBuildError ? ( | ||
<BuildError | ||
message={state.buildError!} | ||
versionInfo={state.versionInfo} | ||
isTurbopack={isTurbopack} | ||
/> | ||
) : ( | ||
<Errors | ||
isTurbopack={isTurbopack} | ||
isAppDir={true} | ||
initialDisplayState={isReactError ? 'fullscreen' : 'minimized'} | ||
errors={state.errors} | ||
versionInfo={state.versionInfo} | ||
hasStaticIndicator={hasStaticIndicator} | ||
debugInfo={debugInfo} | ||
/> | ||
)} | ||
</ShadowPortal> | ||
</> | ||
) | ||
} | ||
} |
Oops, something went wrong.