Skip to content

Commit 823d7cc

Browse files
committed
[dev-overlay] Render Dev Overlay in a leaf in /app
Ensures Dev Overlay re-render will never trigger user code re-render.
1 parent cae897c commit 823d7cc

File tree

2 files changed

+178
-128
lines changed

2 files changed

+178
-128
lines changed

packages/next/src/client/components/react-dev-overlay/app/app-dev-overlay.tsx

Lines changed: 162 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,34 @@
11
import {
2+
ACTION_BEFORE_REFRESH,
3+
ACTION_BUILD_ERROR,
4+
ACTION_BUILD_OK,
5+
ACTION_DEBUG_INFO,
6+
ACTION_DEV_INDICATOR,
7+
ACTION_REFRESH,
8+
ACTION_ERROR_OVERLAY_CLOSE,
29
ACTION_ERROR_OVERLAY_OPEN,
3-
type OverlayDispatch,
4-
type OverlayState,
10+
ACTION_ERROR_OVERLAY_TOGGLE,
11+
ACTION_STATIC_INDICATOR,
12+
ACTION_UNHANDLED_ERROR,
13+
ACTION_UNHANDLED_REJECTION,
14+
ACTION_VERSION_INFO,
15+
useErrorOverlayReducer,
516
} from '../shared'
617
import type { GlobalErrorComponent } from '../../global-error'
718

8-
import { useCallback, useEffect } from 'react'
9-
import { AppDevOverlayErrorBoundary } from './app-dev-overlay-error-boundary'
19+
import { useEffect, useInsertionEffect } from 'react'
20+
import { AppDevOverlayErrorBoundary as AppDevOverlayErrorBoundaryImpl } from './app-dev-overlay-error-boundary'
1021
import { FontStyles } from '../font/font-styles'
22+
import type { DebugInfo } from '../types'
1123
import { DevOverlay } from '../ui/dev-overlay'
24+
import { parseStack } from '../utils/parse-stack'
25+
import { parseComponentStack } from '../utils/parse-component-stack'
26+
import { getComponentStack, getOwnerStack } from '../../errors/stitched-error'
1227
import { handleClientError } from '../../errors/use-error-handler'
1328
import { isNextRouterError } from '../../is-next-router-error'
1429
import { MISSING_ROOT_TAGS_ERROR } from '../../../../shared/lib/errors/constants'
30+
import type { DevIndicatorServerState } from '../../../../server/dev/dev-indicator-server-state'
31+
import type { VersionInfo } from '../../../../server/dev/parse-version-info'
1532

1633
function readSsrError(): (Error & { digest?: string }) | null {
1734
if (typeof document === 'undefined') {
@@ -72,35 +89,157 @@ function ReplaySsrOnlyErrors({
7289
return null
7390
}
7491

75-
export function AppDevOverlay({
76-
state,
77-
dispatch,
92+
export function AppDevOverlayErrorBoundary({
7893
globalError,
7994
children,
8095
}: {
81-
state: OverlayState
82-
dispatch: OverlayDispatch
8396
globalError: [GlobalErrorComponent, React.ReactNode]
8497
children: React.ReactNode
8598
}) {
86-
const openOverlay = useCallback(() => {
99+
function openOverlay() {
100+
dispatcher.openErrorOverlay()
101+
}
102+
103+
return (
104+
<AppDevOverlayErrorBoundaryImpl
105+
globalError={globalError}
106+
onError={openOverlay}
107+
>
108+
<ReplaySsrOnlyErrors onBlockingError={openOverlay} />
109+
{children}
110+
</AppDevOverlayErrorBoundaryImpl>
111+
)
112+
}
113+
114+
export interface Dispatcher {
115+
onBuildOk(): void
116+
onBuildError(message: string): void
117+
onVersionInfo(versionInfo: VersionInfo): void
118+
onDebugInfo(debugInfo: DebugInfo): void
119+
onBeforeRefresh(): void
120+
onRefresh(): void
121+
onStaticIndicator(status: boolean): void
122+
onDevIndicator(devIndicator: DevIndicatorServerState): void
123+
onUnhandledError(reason: Error): void
124+
onUnhandledRejection(reason: Error): void
125+
openErrorOverlay(): void
126+
closeErrorOverlay(): void
127+
toggleErrorOverlay(): void
128+
}
129+
130+
type Dispatch = ReturnType<typeof useErrorOverlayReducer>[1]
131+
let maybeDispatch: Dispatch | null = null
132+
const queue: Array<(dispatch: Dispatch) => void> = []
133+
134+
// Events might be dispatched before we get a `dispatch` from React (e.g. console.error during module eval).
135+
// We need to queue them until we have a `dispatch` function available.
136+
function createQueuable<Args extends any[]>(
137+
queueableFunction: (dispatch: Dispatch, ...args: Args) => void
138+
) {
139+
return (...args: Args) => {
140+
if (maybeDispatch) {
141+
queueableFunction(maybeDispatch, ...args)
142+
} else {
143+
queue.push((dispatch: Dispatch) => {
144+
queueableFunction(dispatch, ...args)
145+
})
146+
}
147+
}
148+
}
149+
150+
// TODO: Extract into separate functions that are imported
151+
export const dispatcher: Dispatcher = {
152+
onBuildOk: createQueuable((dispatch: Dispatch) => {
153+
dispatch({ type: ACTION_BUILD_OK })
154+
}),
155+
onBuildError: createQueuable((dispatch: Dispatch, message: string) => {
156+
dispatch({ type: ACTION_BUILD_ERROR, message })
157+
}),
158+
onBeforeRefresh: createQueuable((dispatch: Dispatch) => {
159+
dispatch({ type: ACTION_BEFORE_REFRESH })
160+
}),
161+
onRefresh: createQueuable((dispatch: Dispatch) => {
162+
dispatch({ type: ACTION_REFRESH })
163+
}),
164+
onVersionInfo: createQueuable(
165+
(dispatch: Dispatch, versionInfo: VersionInfo) => {
166+
dispatch({ type: ACTION_VERSION_INFO, versionInfo })
167+
}
168+
),
169+
onStaticIndicator: createQueuable((dispatch: Dispatch, status: boolean) => {
170+
dispatch({ type: ACTION_STATIC_INDICATOR, staticIndicator: status })
171+
}),
172+
onDebugInfo: createQueuable((dispatch: Dispatch, debugInfo: DebugInfo) => {
173+
dispatch({ type: ACTION_DEBUG_INFO, debugInfo })
174+
}),
175+
onDevIndicator: createQueuable(
176+
(dispatch: Dispatch, devIndicator: DevIndicatorServerState) => {
177+
dispatch({ type: ACTION_DEV_INDICATOR, devIndicator })
178+
}
179+
),
180+
onUnhandledError: createQueuable((dispatch: Dispatch, error: Error) => {
181+
// Component stack is added to the error in use-error-handler in case there was a hydration error
182+
const componentStack = getComponentStack(error)
183+
const ownerStack = getOwnerStack(error)
184+
185+
dispatch({
186+
type: ACTION_UNHANDLED_ERROR,
187+
reason: error,
188+
frames: parseStack((error.stack || '') + (ownerStack || '')),
189+
componentStackFrames:
190+
typeof componentStack === 'string'
191+
? parseComponentStack(componentStack)
192+
: undefined,
193+
})
194+
}),
195+
onUnhandledRejection: createQueuable((dispatch: Dispatch, error: Error) => {
196+
const ownerStack = getOwnerStack(error)
197+
dispatch({
198+
type: ACTION_UNHANDLED_REJECTION,
199+
reason: error,
200+
frames: parseStack((error.stack || '') + (ownerStack || '')),
201+
})
202+
}),
203+
openErrorOverlay: createQueuable((dispatch: Dispatch) => {
87204
dispatch({ type: ACTION_ERROR_OVERLAY_OPEN })
88-
}, [dispatch])
205+
}),
206+
closeErrorOverlay: createQueuable((dispatch: Dispatch) => {
207+
dispatch({ type: ACTION_ERROR_OVERLAY_CLOSE })
208+
}),
209+
toggleErrorOverlay: createQueuable((dispatch: Dispatch) => {
210+
dispatch({ type: ACTION_ERROR_OVERLAY_TOGGLE })
211+
}),
212+
}
213+
214+
function replayQueuedEvents(dispatch: NonNullable<typeof maybeDispatch>) {
215+
try {
216+
for (const queuedFunction of queue) {
217+
queuedFunction(dispatch)
218+
}
219+
} finally {
220+
// TODO: What to do with failed events?
221+
queue.length = 0
222+
}
223+
}
224+
225+
export function AppDevOverlay() {
226+
const [state, dispatch] = useErrorOverlayReducer('app')
227+
228+
useInsertionEffect(() => {
229+
maybeDispatch = dispatch
230+
231+
replayQueuedEvents(dispatch)
232+
233+
return () => {
234+
maybeDispatch = null
235+
}
236+
})
89237

90238
return (
91239
<>
92-
<AppDevOverlayErrorBoundary
93-
globalError={globalError}
94-
onError={openOverlay}
95-
>
96-
<ReplaySsrOnlyErrors onBlockingError={openOverlay} />
97-
{children}
98-
</AppDevOverlayErrorBoundary>
99-
<>
100-
{/* Fonts can only be loaded outside the Shadow DOM. */}
101-
<FontStyles />
102-
<DevOverlay state={state} dispatch={dispatch} />
103-
</>
240+
{/* Fonts can only be loaded outside the Shadow DOM. */}
241+
<FontStyles />
242+
<DevOverlay state={state} dispatch={dispatch} />
104243
</>
105244
)
106245
}

0 commit comments

Comments
 (0)