diff --git a/packages/next/client/app-index.tsx b/packages/next/client/app-index.tsx index bb5f49a1304a..dd11f6a8d4b0 100644 --- a/packages/next/client/app-index.tsx +++ b/packages/next/client/app-index.tsx @@ -212,19 +212,6 @@ function ServerRoot({ cacheKey }: { cacheKey: string }) { return root } -function ErrorOverlay({ - children, -}: React.PropsWithChildren<{}>): React.ReactElement { - if (process.env.NODE_ENV === 'production') { - return <>{children} - } else { - const { - ReactDevOverlay, - } = require('next/dist/compiled/@next/react-dev-overlay/dist/client') - return {children} - } -} - function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement { if (process.env.__NEXT_TEST_MODE) { // eslint-disable-next-line react-hooks/rules-of-hooks @@ -247,12 +234,10 @@ function RSCComponent() { export function hydrate() { renderReactElement(appElement!, () => ( - - - - - - - + + + + + )) } diff --git a/packages/next/client/components/app-router.client.tsx b/packages/next/client/components/app-router.client.tsx index 913bbfde7bee..7dc6e8bffd93 100644 --- a/packages/next/client/components/app-router.client.tsx +++ b/packages/next/client/components/app-router.client.tsx @@ -36,6 +36,19 @@ export function fetchServerResponse( return createFromFetch(fetchFlight(url, flightRouterStateData)) } +function ErrorOverlay({ + children, +}: React.PropsWithChildren<{}>): React.ReactElement { + if (process.env.NODE_ENV === 'production') { + return <>{children} + } else { + const { + ReactDevOverlay, + } = require('next/dist/compiled/@next/react-dev-overlay/dist/client') + return {children} + } +} + // TODO: move this back into AppRouter let initialParallelRoutes: CacheNode['parallelRoutes'] = typeof window === 'undefined' ? null! : new Map() @@ -248,7 +261,7 @@ export default function AppRouter({ url: canonicalUrl, }} > - {cache.subTreeData} + {cache.subTreeData} {hotReloader} diff --git a/packages/next/client/components/hot-reloader.client.tsx b/packages/next/client/components/hot-reloader.client.tsx index 30cc653b12c7..a431c723d598 100644 --- a/packages/next/client/components/hot-reloader.client.tsx +++ b/packages/next/client/components/hot-reloader.client.tsx @@ -1,7 +1,15 @@ -import { useCallback, useContext, useEffect, useRef } from 'react' +import { + useCallback, + useContext, + useEffect, + useRef, + // @ts-expect-error TODO: startTransition exists + startTransition, +} from 'react' import { FullAppTreeContext } from '../../shared/lib/app-router-context' import { register, + unregister, onBuildError, onBuildOk, onRefresh, @@ -305,7 +313,15 @@ function processMessage( clientId: __nextDevClientId, }) ) - return router.reload() + if (hadRuntimeError) { + return window.location.reload() + } + startTransition(() => { + router.reload() + onRefresh() + }) + + return } case 'reloadPage': { sendMessage( @@ -393,6 +409,16 @@ export default function HotReload({ assetPrefix }: { assetPrefix: string }) { useEffect(() => { register() + const onError = () => { + hadRuntimeError = true + } + window.addEventListener('error', onError) + window.addEventListener('unhandledrejection', onError) + return () => { + unregister() + window.removeEventListener('error', onError) + window.removeEventListener('unhandledrejection', onError) + } }, []) useEffect(() => { diff --git a/packages/next/client/dev/error-overlay/hot-dev-client.js b/packages/next/client/dev/error-overlay/hot-dev-client.js index d6b0918952bc..6153a6f848c1 100644 --- a/packages/next/client/dev/error-overlay/hot-dev-client.js +++ b/packages/next/client/dev/error-overlay/hot-dev-client.js @@ -260,6 +260,16 @@ function processMessage(e) { ) return handleSuccess() } + + case 'serverComponentChanges': { + sendMessage( + JSON.stringify({ + event: 'server-component-reload-page', + clientId: window.__nextDevClientId, + }) + ) + return window.location.reload() + } default: { if (customHmrEventHandler) { customHmrEventHandler(obj) diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index fa8f54b68e71..785ef9efcf37 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -1982,6 +1982,11 @@ export default async function (task) { opts ) await task.watch('server/**/*.+(wasm)', 'server_wasm', opts) + await task.watch( + '../react-dev-overlay/dist/**/*.js', + 'ncc_next__react_dev_overlay', + opts + ) } export async function shared(task, opts) { diff --git a/packages/react-dev-overlay/src/internal/ErrorBoundary.tsx b/packages/react-dev-overlay/src/internal/ErrorBoundary.tsx index 4c2267237dee..b2a1a6f0a984 100644 --- a/packages/react-dev-overlay/src/internal/ErrorBoundary.tsx +++ b/packages/react-dev-overlay/src/internal/ErrorBoundary.tsx @@ -2,6 +2,8 @@ import React from 'react' type ErrorBoundaryProps = { onError: (error: Error, componentStack: string | null) => void + globalOverlay?: boolean + isMounted?: boolean } type ErrorBoundaryState = { error: Error | null } @@ -10,7 +12,6 @@ class ErrorBoundary extends React.PureComponent< ErrorBoundaryState > { state = { error: null } - componentDidCatch( error: Error, // Loosely typed because it depends on the React version and was @@ -18,14 +19,26 @@ class ErrorBoundary extends React.PureComponent< errorInfo?: { componentStack?: string | null } ) { this.props.onError(error, errorInfo?.componentStack || null) - this.setState({ error }) + if (!this.props.globalOverlay) { + this.setState({ error }) + } } render() { - return this.state.error - ? // The component has to be unmounted or else it would continue to error - null - : this.props.children + // The component has to be unmounted or else it would continue to error + return this.state.error || + (this.props.globalOverlay && this.props.isMounted) ? ( + // When the overlay is global for the application and it wraps a component rendering `` + // we have to render the html shell otherwise the shadow root will not be able to attach + this.props.globalOverlay ? ( + + + + + ) : null + ) : ( + this.props.children + ) } } diff --git a/packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx b/packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx index fe2fe056ca2c..d125ccc93bf1 100644 --- a/packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx +++ b/packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx @@ -31,7 +31,13 @@ function reducer(state: OverlayState, ev: Bus.BusEvent): OverlayState { return { ...state, nextId: state.nextId + 1, - errors: [...state.errors, { id: state.nextId, event: ev }], + errors: [ + ...state.errors.filter((err) => { + // Filter out duplicate errors + return err.event.reason !== ev.reason + }), + { id: state.nextId, event: ev }, + ], } } default: { @@ -82,7 +88,11 @@ const ReactDevOverlay: React.FunctionComponent = function ReactDevOverlay({ return ( - + {children ?? null} {isMounted ? (