Skip to content

Commit 218cb57

Browse files
committed
[dev-overlay] Parse stacks in reducer not during dispatch
In the reducer we'll later inject the implementation of `getOwnerStack`. The reducer will live in a different module that `stitched-errors` so we need to make sure `getOwnerStack` is a singletone. Dependency injection makes this easier to achieve. `getOwnerStack` will be injected in a follow-up
1 parent 3a0af14 commit 218cb57

File tree

6 files changed

+105
-144
lines changed

6 files changed

+105
-144
lines changed

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

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
reportInvalidHmrMessage,
2121
useErrorOverlayReducer,
2222
} from '../shared'
23-
import { parseStack } from '../utils/parse-stack'
2423
import { AppDevOverlay } from './app-dev-overlay'
2524
import { useErrorHandler } from '../../errors/use-error-handler'
2625
import { RuntimeErrorHandler } from '../../errors/runtime-error-handler'
@@ -30,7 +29,6 @@ import {
3029
useWebsocket,
3130
useWebsocketPing,
3231
} from '../utils/use-websocket'
33-
import { parseComponentStack } from '../utils/parse-component-stack'
3432
import type { VersionInfo } from '../../../../server/dev/parse-version-info'
3533
import { HMR_ACTIONS_SENT_TO_BROWSER } from '../../../../server/dev/hot-reloader-types'
3634
import type {
@@ -40,7 +38,6 @@ import type {
4038
import { REACT_REFRESH_FULL_RELOAD_FROM_ERROR } from '../shared'
4139
import type { DebugInfo } from '../types'
4240
import { useUntrackedPathname } from '../../navigation-untracked'
43-
import { getComponentStack, getOwnerStack } from '../../errors/stitched-error'
4441
import { handleDevBuildIndicatorHmrEvents } from '../../../dev/dev-build-indicator/internal/handle-dev-build-indicator-hmr-events'
4542
import type { GlobalErrorComponent } from '../../global-error'
4643
import type { DevIndicatorServerState } from '../../../../server/dev/dev-indicator-server-state'
@@ -514,26 +511,15 @@ export default function HotReload({
514511
})
515512
},
516513
onUnhandledError(error) {
517-
// Component stack is added to the error in use-error-handler in case there was a hydration error
518-
const componentStack = getComponentStack(error)
519-
const ownerStack = getOwnerStack(error)
520-
521514
dispatch({
522515
type: ACTION_UNHANDLED_ERROR,
523516
reason: error,
524-
frames: parseStack((error.stack || '') + (ownerStack || '')),
525-
componentStackFrames:
526-
typeof componentStack === 'string'
527-
? parseComponentStack(componentStack)
528-
: undefined,
529517
})
530518
},
531519
onUnhandledRejection(error) {
532-
const ownerStack = getOwnerStack(error)
533520
dispatch({
534521
type: ACTION_UNHANDLED_REJECTION,
535522
reason: error,
536-
frames: parseStack((error.stack || '') + (ownerStack || '')),
537523
})
538524
},
539525
}

packages/next/src/client/components/react-dev-overlay/pages/client.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import * as Bus from './bus'
2-
import { parseStack } from '../utils/parse-stack'
3-
import { parseComponentStack } from '../utils/parse-component-stack'
42
import {
53
attachHydrationErrorState,
64
storeHydrationErrorStateFromConsoleArgs,
@@ -17,7 +15,6 @@ import {
1715
ACTION_VERSION_INFO,
1816
} from '../shared'
1917
import type { VersionInfo } from '../../../../server/dev/parse-version-info'
20-
import { getComponentStack, getOwnerStack } from '../../errors/stitched-error'
2118
import type { DevIndicatorServerState } from '../../../../server/dev/dev-indicator-server-state'
2219

2320
let isRegistered = false
@@ -30,13 +27,6 @@ function handleError(error: unknown) {
3027

3128
attachHydrationErrorState(error)
3229

33-
const componentStackTrace = getComponentStack(error)
34-
const ownerStack = getOwnerStack(error)
35-
const componentStackFrames =
36-
typeof componentStackTrace === 'string'
37-
? parseComponentStack(componentStackTrace)
38-
: undefined
39-
4030
// Skip ModuleBuildError and ModuleNotFoundError, as it will be sent through onBuildError callback.
4131
// This is to avoid same error as different type showing up on client to cause flashing.
4232
if (
@@ -46,8 +36,6 @@ function handleError(error: unknown) {
4636
Bus.emit({
4737
type: ACTION_UNHANDLED_ERROR,
4838
reason: error,
49-
frames: parseStack((error.stack || '') + (ownerStack || '')),
50-
componentStackFrames,
5139
})
5240
}
5341
}
@@ -78,12 +66,9 @@ function onUnhandledRejection(ev: PromiseRejectionEvent) {
7866
return
7967
}
8068

81-
const error = reason
82-
const ownerStack = getOwnerStack(error)
8369
Bus.emit({
8470
type: ACTION_UNHANDLED_REJECTION,
8571
reason: reason,
86-
frames: parseStack((error.stack || '') + (ownerStack || '')),
8772
})
8873
}
8974

packages/next/src/client/components/react-dev-overlay/shared.ts

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { useReducer } from 'react'
22

3-
import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
43
import type { VersionInfo } from '../../../server/dev/parse-version-info'
54
import type { SupportedErrorEvent } from './ui/container/runtime-error/render-error'
6-
import type { ComponentStackFrame } from './utils/parse-component-stack'
5+
import { parseComponentStack } from './utils/parse-component-stack'
76
import type { DebugInfo } from './types'
87
import type { DevIndicatorServerState } from '../../../server/dev/dev-indicator-server-state'
98
import type { HMR_ACTION_TYPES } from '../../../server/dev/hot-reloader-types'
10-
import { getOwnerStack } from '../errors/stitched-error'
9+
import { parseStack } from './utils/parse-stack'
10+
import { getComponentStack, getOwnerStack } from '../errors/stitched-error'
1111

1212
type FastRefreshState =
1313
/** No refresh in progress. */
@@ -71,13 +71,10 @@ interface FastRefreshAction {
7171
export interface UnhandledErrorAction {
7272
type: typeof ACTION_UNHANDLED_ERROR
7373
reason: Error
74-
frames: StackFrame[]
75-
componentStackFrames?: ComponentStackFrame[]
7674
}
7775
export interface UnhandledRejectionAction {
7876
type: typeof ACTION_UNHANDLED_REJECTION
7977
reason: Error
80-
frames: StackFrame[]
8178
}
8279

8380
export interface DebugInfoAction {
@@ -134,21 +131,35 @@ function getStackIgnoringStrictMode(stack: string | undefined) {
134131
}
135132

136133
function pushErrorFilterDuplicates(
137-
errors: SupportedErrorEvent[],
138-
err: SupportedErrorEvent
134+
events: SupportedErrorEvent[],
135+
id: number,
136+
error: Error
139137
): SupportedErrorEvent[] {
140-
const pendingErrors = errors.filter((e) => {
138+
const componentStack = getComponentStack(error)
139+
const componentStackFrames =
140+
componentStack === undefined
141+
? undefined
142+
: parseComponentStack(componentStack)
143+
const ownerStack = getOwnerStack(error)
144+
const frames = parseStack((error.stack || '') + (ownerStack || ''))
145+
const pendingEvent: SupportedErrorEvent = {
146+
id,
147+
error,
148+
frames,
149+
componentStackFrames,
150+
}
151+
const pendingEvents = events.filter((event) => {
141152
// Filter out duplicate errors
142153
return (
143-
(e.event.reason.stack !== err.event.reason.stack &&
154+
(event.error.stack !== pendingEvent.error.stack &&
144155
// TODO: Let ReactDevTools control deduping instead?
145-
getStackIgnoringStrictMode(e.event.reason.stack) !==
146-
getStackIgnoringStrictMode(err.event.reason.stack)) ||
147-
getOwnerStack(e.event.reason) !== getOwnerStack(err.event.reason)
156+
getStackIgnoringStrictMode(event.error.stack) !==
157+
getStackIgnoringStrictMode(pendingEvent.error.stack)) ||
158+
getOwnerStack(event.error) !== getOwnerStack(pendingEvent.error)
148159
)
149160
})
150-
pendingErrors.push(err)
151-
return pendingErrors
161+
pendingEvents.push(pendingEvent)
162+
return pendingEvents
152163
}
153164

154165
const shouldDisableDevIndicator =
@@ -230,10 +241,11 @@ export function useErrorOverlayReducer(routerType: 'pages' | 'app') {
230241
return {
231242
...state,
232243
nextId: state.nextId + 1,
233-
errors: pushErrorFilterDuplicates(state.errors, {
234-
id: state.nextId,
235-
event: action,
236-
}),
244+
errors: pushErrorFilterDuplicates(
245+
state.errors,
246+
state.nextId,
247+
action.reason
248+
),
237249
}
238250
}
239251
case 'pending': {
@@ -242,10 +254,11 @@ export function useErrorOverlayReducer(routerType: 'pages' | 'app') {
242254
nextId: state.nextId + 1,
243255
refreshState: {
244256
...state.refreshState,
245-
errors: pushErrorFilterDuplicates(state.refreshState.errors, {
246-
id: state.nextId,
247-
event: action,
248-
}),
257+
errors: pushErrorFilterDuplicates(
258+
state.errors,
259+
state.nextId,
260+
action.reason
261+
),
249262
},
250263
}
251264
}

packages/next/src/client/components/react-dev-overlay/ui/container/runtime-error/render-error.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import type {
2-
OverlayState,
3-
UnhandledErrorAction,
4-
UnhandledRejectionAction,
5-
} from '../../../shared'
1+
import type { OverlayState } from '../../../shared'
62

73
import { useMemo, useState, useEffect } from 'react'
84
import {
95
getErrorByType,
106
type ReadyRuntimeError,
117
} from '../../../utils/get-error-by-type'
8+
import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
9+
import type { ComponentStackFrame } from '../../../utils/parse-component-stack'
1210

1311
export type SupportedErrorEvent = {
1412
id: number
15-
event: UnhandledErrorAction | UnhandledRejectionAction
13+
error: Error
14+
frames: StackFrame[]
15+
componentStackFrames?: ComponentStackFrame[]
1616
}
1717

1818
type Props = {

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

Lines changed: 29 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
ACTION_ERROR_OVERLAY_CLOSE,
1111
ACTION_ERROR_OVERLAY_OPEN,
1212
ACTION_ERROR_OVERLAY_TOGGLE,
13-
ACTION_UNHANDLED_ERROR,
1413
} from '../shared'
1514

1615
const meta: Meta<typeof DevOverlay> = {
@@ -32,50 +31,41 @@ const initialState: OverlayState = {
3231
errors: [
3332
{
3433
id: 1,
35-
event: {
36-
type: ACTION_UNHANDLED_ERROR,
37-
reason: Object.assign(new Error('First error message'), {
38-
__NEXT_ERROR_CODE: 'E001',
39-
}),
40-
componentStackFrames: [
41-
{
42-
file: 'app/page.tsx',
43-
component: 'Home',
44-
lineNumber: 10,
45-
column: 5,
46-
canOpenInEditor: true,
47-
},
48-
],
49-
frames: [
50-
{
51-
file: 'app/page.tsx',
52-
methodName: 'Home',
53-
arguments: [],
54-
lineNumber: 10,
55-
column: 5,
56-
},
57-
],
58-
},
34+
error: Object.assign(new Error('First error message'), {
35+
__NEXT_ERROR_CODE: 'E001',
36+
}),
37+
componentStackFrames: [
38+
{
39+
file: 'app/page.tsx',
40+
component: 'Home',
41+
lineNumber: 10,
42+
column: 5,
43+
canOpenInEditor: true,
44+
},
45+
],
46+
frames: [
47+
{
48+
file: 'app/page.tsx',
49+
methodName: 'Home',
50+
arguments: [],
51+
lineNumber: 10,
52+
column: 5,
53+
},
54+
],
5955
},
6056
{
6157
id: 2,
62-
event: {
63-
type: ACTION_UNHANDLED_ERROR,
64-
reason: Object.assign(new Error('Second error message'), {
65-
__NEXT_ERROR_CODE: 'E002',
66-
}),
67-
frames: [],
68-
},
58+
error: Object.assign(new Error('Second error message'), {
59+
__NEXT_ERROR_CODE: 'E002',
60+
}),
61+
frames: [],
6962
},
7063
{
7164
id: 3,
72-
event: {
73-
type: ACTION_UNHANDLED_ERROR,
74-
reason: Object.assign(new Error('Third error message'), {
75-
__NEXT_ERROR_CODE: 'E003',
76-
}),
77-
frames: [],
78-
},
65+
error: Object.assign(new Error('Third error message'), {
66+
__NEXT_ERROR_CODE: 'E003',
67+
}),
68+
frames: [],
7969
},
8070
],
8171
refreshState: { type: 'idle' },

0 commit comments

Comments
 (0)