Skip to content

Commit

Permalink
Refactor error overlay for new router (#41343)
Browse files Browse the repository at this point in the history
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
timneutkens and kodiakhq[bot] committed Oct 12, 2022
1 parent e0bb258 commit bf630e8
Show file tree
Hide file tree
Showing 50 changed files with 330 additions and 394 deletions.
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

0 comments on commit bf630e8

Please sign in to comment.