Repository: vercel/next.js
Pull request: [dev-overlay] Remove indirection in app dev error boundary 

Doesn't need to be put in JSX children and can directly returned from render.

Allows for cleaner code separation with the future dedicated dev overlay bundle.

Changed files:
filename: packages/next/src/client/components/react-dev-overlay/app/app-dev-overlay.tsx
patch: @@ -5,72 +5,10 @@ import {
 } from '../shared'
 import type { GlobalErrorComponent } from '../../global-error'
 
-import { useCallback, useEffect } from 'react'
+import { useCallback } from 'react'
 import { AppDevOverlayErrorBoundary } from './app-dev-overlay-error-boundary'
 import { FontStyles } from '../font/font-styles'
 import { DevOverlay } from '../ui/dev-overlay'
-import { handleClientError } from '../../errors/use-error-handler'
-import { isNextRouterError } from '../../is-next-router-error'
-import { MISSING_ROOT_TAGS_ERROR } from '../../../../shared/lib/errors/constants'
-
-function readSsrError(): (Error & { digest?: string }) | null {
-  if (typeof document === 'undefined') {
-    return null
-  }
-
-  const ssrErrorTemplateTag = document.querySelector(
-    'template[data-next-error-message]'
-  )
-  if (ssrErrorTemplateTag) {
-    const message: string = ssrErrorTemplateTag.getAttribute(
-      'data-next-error-message'
-    )!
-    const stack = ssrErrorTemplateTag.getAttribute('data-next-error-stack')
-    const digest = ssrErrorTemplateTag.getAttribute('data-next-error-digest')
-    const error = new Error(message)
-    if (digest) {
-      ;(error as any).digest = digest
-    }
-    // Skip Next.js SSR'd internal errors that which will be handled by the error boundaries.
-    if (isNextRouterError(error)) {
-      return null
-    }
-    error.stack = stack || ''
-    return error
-  }
-
-  return null
-}
-
-// Needs to be in the same error boundary as the shell.
-// If it commits, we know we recovered from an SSR error.
-// If it doesn't commit, we errored again and React will take care of error reporting.
-function ReplaySsrOnlyErrors({
-  onBlockingError,
-}: {
-  onBlockingError: () => void
-}) {
-  if (process.env.NODE_ENV !== 'production') {
-    // Need to read during render. The attributes will be gone after commit.
-    const ssrError = readSsrError()
-    // eslint-disable-next-line react-hooks/rules-of-hooks
-    useEffect(() => {
-      if (ssrError !== null) {
-        // TODO(veil): Include original Owner Stack (NDX-905)
-        // TODO(veil): Mark as recoverable error
-        // TODO(veil): console.error
-        handleClientError(ssrError)
-
-        // If it's missing root tags, we can't recover, make it blocking.
-        if (ssrError.digest === MISSING_ROOT_TAGS_ERROR) {
-          onBlockingError()
-        }
-      }
-    }, [ssrError, onBlockingError])
-  }
-
-  return null
-}
 
 function getSquashedHydrationErrorDetails() {
   // We don't squash hydration errors in the App Router.
@@ -98,7 +36,6 @@ export function AppDevOverlay({
         globalError={globalError}
         onError={openOverlay}
       >
-        <ReplaySsrOnlyErrors onBlockingError={openOverlay} />
         {children}
       </AppDevOverlayErrorBoundary>
       <>

filename: packages/next/src/client/components/react-dev-overlay/app/hot-reloader-client.tsx
patch: @@ -11,6 +11,7 @@ import {
   ACTION_BUILD_OK,
   ACTION_DEBUG_INFO,
   ACTION_DEV_INDICATOR,
+  ACTION_ERROR_OVERLAY_OPEN,
   ACTION_REFRESH,
   ACTION_STATIC_INDICATOR,
   ACTION_UNHANDLED_ERROR,
@@ -21,6 +22,7 @@ import {
   useErrorOverlayReducer,
 } from '../shared'
 import { AppDevOverlay } from './app-dev-overlay'
+import { ReplaySsrOnlyErrors } from './replay-ssr-only-errors'
 import { useErrorHandler } from '../../errors/use-error-handler'
 import { RuntimeErrorHandler } from '../../errors/runtime-error-handler'
 import {
@@ -58,6 +60,7 @@ export interface Dispatcher {
   onDevIndicator(devIndicator: DevIndicatorServerState): void
   onUnhandledError(error: Error): void
   onUnhandledRejection(error: Error): void
+  openErrorOverlay(): void
 }
 
 let mostRecentCompilationHash: any = null
@@ -529,6 +532,9 @@ export default function HotReload({
           reason: error,
         })
       },
+      openErrorOverlay() {
+        dispatch({ type: ACTION_ERROR_OVERLAY_OPEN })
+      },
     }
   }, [dispatch])
 
@@ -618,6 +624,7 @@ export default function HotReload({
 
   return (
     <AppDevOverlay state={state} dispatch={dispatch} globalError={globalError}>
+      <ReplaySsrOnlyErrors onBlockingError={dispatcher.openErrorOverlay} />
       {children}
     </AppDevOverlay>
   )

filename: packages/next/src/client/components/react-dev-overlay/app/replay-ssr-only-errors.tsx
patch: @@ -0,0 +1,65 @@
+import { useEffect } from 'react'
+import { handleClientError } from '../../errors/use-error-handler'
+import { isNextRouterError } from '../../is-next-router-error'
+import { MISSING_ROOT_TAGS_ERROR } from '../../../../shared/lib/errors/constants'
+
+function readSsrError(): (Error & { digest?: string }) | null {
+  if (typeof document === 'undefined') {
+    return null
+  }
+
+  const ssrErrorTemplateTag = document.querySelector(
+    'template[data-next-error-message]'
+  )
+  if (ssrErrorTemplateTag) {
+    const message: string = ssrErrorTemplateTag.getAttribute(
+      'data-next-error-message'
+    )!
+    const stack = ssrErrorTemplateTag.getAttribute('data-next-error-stack')
+    const digest = ssrErrorTemplateTag.getAttribute('data-next-error-digest')
+    const error = new Error(message)
+    if (digest) {
+      ;(error as any).digest = digest
+    }
+    // Skip Next.js SSR'd internal errors that which will be handled by the error boundaries.
+    if (isNextRouterError(error)) {
+      return null
+    }
+    error.stack = stack || ''
+    return error
+  }
+
+  return null
+}
+
+/**
+ * Needs to be in the same error boundary as the shell.
+ * If it commits, we know we recovered from an SSR error.
+ * If it doesn't commit, we errored again and React will take care of error reporting.
+ */
+export function ReplaySsrOnlyErrors({
+  onBlockingError,
+}: {
+  onBlockingError: () => void
+}) {
+  if (process.env.NODE_ENV !== 'production') {
+    // Need to read during render. The attributes will be gone after commit.
+    const ssrError = readSsrError()
+    // eslint-disable-next-line react-hooks/rules-of-hooks
+    useEffect(() => {
+      if (ssrError !== null) {
+        // TODO(veil): Include original Owner Stack (NDX-905)
+        // TODO(veil): Mark as recoverable error
+        // TODO(veil): console.error
+        handleClientError(ssrError)
+
+        // If it's missing root tags, we can't recover, make it blocking.
+        if (ssrError.digest === MISSING_ROOT_TAGS_ERROR) {
+          onBlockingError()
+        }
+      }
+    }, [ssrError, onBlockingError])
+  }
+
+  return null
+}