Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor error overlay for new router #41343

Merged
merged 7 commits into from Oct 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/next/client/components/app-router.tsx
Expand Up @@ -36,11 +36,13 @@ function urlToUrlWithoutFlightMarker(url: string): URL {
return urlWithoutFlightParameters
}

const HotReloader: typeof import('./hot-reloader').default | null =
const HotReloader:
| typeof import('./react-dev-overlay/hot-reloader').default
| null =
process.env.NODE_ENV === 'production'
? null
: (require('./hot-reloader')
.default as typeof import('./hot-reloader').default)
: (require('./react-dev-overlay/hot-reloader')
.default as typeof import('./react-dev-overlay/hot-reloader').default)

/**
* Fetch the flight data for the provided url. Takes in the current router state to decide what to render server-side.
Expand Down
71 changes: 71 additions & 0 deletions packages/next/client/components/react-dev-overlay/client.ts
@@ -0,0 +1,71 @@
import { Dispatch, ReducerAction } from 'react'
import type { errorOverlayReducer } from './internal/error-overlay-reducer'
import {
ACTION_BUILD_OK,
ACTION_BUILD_ERROR,
ACTION_REFRESH,
ACTION_UNHANDLED_ERROR,
ACTION_UNHANDLED_REJECTION,
} from './internal/error-overlay-reducer'
import { parseStack } from './internal/helpers/parseStack'

export type DispatchFn = Dispatch<ReducerAction<typeof errorOverlayReducer>>

export function onUnhandledError(dispatch: DispatchFn, ev: ErrorEvent) {
const error = ev?.error
if (!error || !(error instanceof Error) || typeof error.stack !== 'string') {
// A non-error was thrown, we don't have anything to show. :-(
return
}

if (
error.message.match(/(hydration|content does not match|did not match)/i)
) {
error.message += `\n\nSee more info here: https://nextjs.org/docs/messages/react-hydration-error`
}

const e = error
dispatch({
type: ACTION_UNHANDLED_ERROR,
reason: error,
frames: parseStack(e.stack!),
})
}

export function onUnhandledRejection(
dispatch: DispatchFn,
ev: PromiseRejectionEvent
) {
const reason = ev?.reason
if (
!reason ||
!(reason instanceof Error) ||
typeof reason.stack !== 'string'
) {
// A non-error was thrown, we don't have anything to show. :-(
return
}

const e = reason
dispatch({
type: ACTION_UNHANDLED_REJECTION,
reason: reason,
frames: parseStack(e.stack!),
})
}

export function onBuildOk(dispatch: DispatchFn) {
dispatch({ type: ACTION_BUILD_OK })
}

export function onBuildError(dispatch: DispatchFn, message: string) {
dispatch({ type: ACTION_BUILD_ERROR, message })
}

export function onRefresh(dispatch: DispatchFn) {
dispatch({ type: ACTION_REFRESH })
}

export { getErrorByType } from './internal/helpers/getErrorByType'
export { getServerError } from './internal/helpers/nodeStackFrames'
export { default as ReactDevOverlay } from './internal/ReactDevOverlay'
Expand Up @@ -7,18 +7,20 @@ import React, {
// @ts-expect-error TODO-APP: startTransition exists
startTransition,
} from 'react'
import { GlobalLayoutRouterContext } from '../../shared/lib/app-router-context'
import { GlobalLayoutRouterContext } from '../../../shared/lib/app-router-context'
import {
register,
unregister,
onBuildError,
onBuildOk,
onRefresh,
onUnhandledError,
onUnhandledRejection,
ReactDevOverlay,
} from './react-dev-overlay/src/client'
} from './client'
import type { DispatchFn } from './client'
import stripAnsi from 'next/dist/compiled/strip-ansi'
import formatWebpackMessages from '../dev/error-overlay/format-webpack-messages'
import { useRouter } from './hooks-client'
import formatWebpackMessages from '../../dev/error-overlay/format-webpack-messages'
import { useRouter } from '../hooks-client'
import { errorOverlayReducer } from './internal/error-overlay-reducer'

function getSocketProtocol(assetPrefix: string): string {
let protocol = window.location.protocol
Expand All @@ -42,10 +44,10 @@ let hadRuntimeError = false

// let startLatency = undefined

function onFastRefresh(hasUpdates: boolean) {
onBuildOk()
function onFastRefresh(dispatch: DispatchFn, hasUpdates: boolean) {
onBuildOk(dispatch)
if (hasUpdates) {
onRefresh()
onRefresh(dispatch)
}

// if (startLatency) {
Expand Down Expand Up @@ -120,7 +122,11 @@ function performFullReload(err: any, sendMessage: any) {
}

// Attempt to update code on the fly, fall back to a hard reload.
function tryApplyUpdates(onHotUpdateSuccess: any, sendMessage: any) {
function tryApplyUpdates(
onHotUpdateSuccess: any,
sendMessage: any,
dispatch: DispatchFn
) {
// @ts-expect-error module.hot exists
if (!module.hot) {
// HotModuleReplacementPlugin is not in Webpack configuration.
Expand All @@ -130,7 +136,7 @@ function tryApplyUpdates(onHotUpdateSuccess: any, sendMessage: any) {
}

if (!isUpdateAvailable() || !canApplyUpdates()) {
onBuildOk()
onBuildOk(dispatch)
return
}

Expand Down Expand Up @@ -162,9 +168,13 @@ function tryApplyUpdates(onHotUpdateSuccess: any, sendMessage: any) {

if (isUpdateAvailable()) {
// While we were updating, there was a new update! Do it again.
tryApplyUpdates(hasUpdates ? onBuildOk : onHotUpdateSuccess, sendMessage)
tryApplyUpdates(
hasUpdates ? onBuildOk : onHotUpdateSuccess,
sendMessage,
dispatch
)
} else {
onBuildOk()
onBuildOk(dispatch)
// if (process.env.__NEXT_TEST_MODE) {
// afterApplyUpdates(() => {
// if (self.__NEXT_HMR_CB) {
Expand All @@ -191,7 +201,8 @@ function tryApplyUpdates(onHotUpdateSuccess: any, sendMessage: any) {
function processMessage(
e: any,
sendMessage: any,
router: ReturnType<typeof useRouter>
router: ReturnType<typeof useRouter>,
dispatch: DispatchFn
) {
const obj = JSON.parse(e.data)

Expand Down Expand Up @@ -226,7 +237,7 @@ function processMessage(
})

// Only show the first error.
onBuildError(formatted.errors[0])
onBuildError(dispatch, formatted.errors[0])

// Also log them to the console.
for (let i = 0; i < formatted.errors.length; i++) {
Expand Down Expand Up @@ -276,11 +287,15 @@ function processMessage(

// Attempt to apply hot updates or reload.
if (isHotUpdate) {
tryApplyUpdates(function onSuccessfulHotUpdate(hasUpdates: any) {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
onFastRefresh(hasUpdates)
}, sendMessage)
tryApplyUpdates(
function onSuccessfulHotUpdate(hasUpdates: any) {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
onFastRefresh(dispatch, hasUpdates)
},
sendMessage,
dispatch
)
}
return
}
Expand All @@ -299,11 +314,15 @@ function processMessage(

// Attempt to apply hot updates or reload.
if (isHotUpdate) {
tryApplyUpdates(function onSuccessfulHotUpdate(hasUpdates: any) {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
onFastRefresh(hasUpdates)
}, sendMessage)
tryApplyUpdates(
function onSuccessfulHotUpdate(hasUpdates: any) {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
onFastRefresh(dispatch, hasUpdates)
},
sendMessage,
dispatch
)
}
return
}
Expand All @@ -320,7 +339,7 @@ function processMessage(
}
startTransition(() => {
router.reload()
onRefresh()
onRefresh(dispatch)
})

return
Expand Down Expand Up @@ -405,6 +424,22 @@ export default function HotReload({
assetPrefix: string
children?: ReactNode
}) {
const stacktraceLimitRef = useRef<undefined | number>()
const [state, dispatch] = React.useReducer(errorOverlayReducer, {
nextId: 1,
buildError: null,
errors: [],
})

const handleOnUnhandledError = useCallback((ev) => {
hadRuntimeError = true
onUnhandledError(dispatch, ev)
}, [])
const handleOnUnhandledRejection = useCallback((ev) => {
hadRuntimeError = true
onUnhandledRejection(dispatch, ev)
}, [])

const { tree } = useContext(GlobalLayoutRouterContext)
const router = useRouter()

Expand All @@ -416,18 +451,29 @@ export default function HotReload({
}, [])

useEffect(() => {
register()
const onError = () => {
hadRuntimeError = true
}
window.addEventListener('error', onError)
window.addEventListener('unhandledrejection', onError)
try {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 50
stacktraceLimitRef.current = limit
} catch {}

window.addEventListener('error', handleOnUnhandledError)
window.addEventListener('unhandledrejection', handleOnUnhandledRejection)
return () => {
unregister()
window.removeEventListener('error', onError)
window.removeEventListener('unhandledrejection', onError)
if (stacktraceLimitRef.current !== undefined) {
try {
Error.stackTraceLimit = stacktraceLimitRef.current
} catch {}
stacktraceLimitRef.current = undefined
}

window.removeEventListener('error', handleOnUnhandledError)
window.removeEventListener(
'unhandledrejection',
handleOnUnhandledRejection
)
}
}, [])
}, [handleOnUnhandledError, handleOnUnhandledRejection])

useEffect(() => {
if (webSocketRef.current) {
Expand Down Expand Up @@ -474,7 +520,7 @@ export default function HotReload({
}

try {
processMessage(event, sendMessage, router)
processMessage(event, sendMessage, router, dispatch)
} catch (ex) {
console.warn('Invalid HMR message: ' + event.data + '\n', ex)
}
Expand All @@ -501,5 +547,5 @@ export default function HotReload({
// return () => clearInterval(interval)
// })

return <ReactDevOverlay globalOverlay>{children}</ReactDevOverlay>
return <ReactDevOverlay state={state}>{children}</ReactDevOverlay>
}
@@ -1,8 +1,6 @@
import React from 'react'

type ErrorBoundaryProps = {
onError: (error: Error, componentStack: string | null) => void
globalOverlay?: boolean
isMounted?: boolean
}
type ErrorBoundaryState = { error: Error | null }
Expand All @@ -17,30 +15,15 @@ class ErrorBoundary extends React.PureComponent<
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)
if (!this.props.globalOverlay) {
this.setState({ error })
}
}

render() {
// The component has to be unmounted or else it would continue to error
return this.state.error ||
(this.props.globalOverlay && this.props.isMounted) ? (
return this.state.error || this.props.isMounted ? (
// When the overlay is global for the application and it wraps a component rendering `<html>`
// we have to render the html shell otherwise the shadow root will not be able to attach
this.props.globalOverlay ? (
<html>
<head></head>
<body></body>
</html>
) : null
<html>
<head></head>
<body></body>
</html>
) : (
this.props.children
)
Expand Down