diff --git a/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx b/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx index 09d90f1fb27871f..3e7b3c85602d3c5 100644 --- a/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx +++ b/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx @@ -435,6 +435,9 @@ export default function HotReload({ frames: parseStack(reason.stack!), }) }, []) + const handleOnReactError = useCallback(() => { + RuntimeErrorHandler.hadRuntimeError = true + }, []) useErrorHandler(handleOnUnhandledError, handleOnUnhandledRejection) const webSocketRef = useWebsocket(assetPrefix) @@ -467,5 +470,9 @@ export default function HotReload({ return () => websocket && websocket.removeEventListener('message', handler) }, [sendMessage, router, webSocketRef, dispatcher]) - return {children} + return ( + + {children} + + ) } diff --git a/packages/next/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx b/packages/next/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx index 5d0c233240fb2f7..9d9eee98ea12e36 100644 --- a/packages/next/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx +++ b/packages/next/client/components/react-dev-overlay/internal/ReactDevOverlay.tsx @@ -21,6 +21,7 @@ class ReactDevOverlay extends React.PureComponent< { state: OverlayState children: React.ReactNode + onReactError: (error: Error) => void }, ReactDevOverlayState > { @@ -40,6 +41,10 @@ class ReactDevOverlay extends React.PureComponent< return { reactError: errorEvent } } + componentDidCatch(componentErr: Error) { + this.props.onReactError(componentErr) + } + render() { const { state, children } = this.props const { reactError } = this.state diff --git a/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts b/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts index fa86bc92f4434ca..f22ea0b9f7d8982 100644 --- a/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts +++ b/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts @@ -44,8 +44,6 @@ if (typeof window !== 'undefined') { return } - RuntimeErrorHandler.hadRuntimeError = true - const error = ev?.error if ( !error || @@ -69,8 +67,6 @@ if (typeof window !== 'undefined') { window.addEventListener( 'unhandledrejection', (ev: WindowEventMap['unhandledrejection']): void => { - RuntimeErrorHandler.hadRuntimeError = true - const reason = ev?.reason if ( !reason || diff --git a/test/development/acceptance-app/ReactRefreshLogBox.test.ts b/test/development/acceptance-app/ReactRefreshLogBox.test.ts index 9fa41067ff307ba..b11f6b5fa285a50 100644 --- a/test/development/acceptance-app/ReactRefreshLogBox.test.ts +++ b/test/development/acceptance-app/ReactRefreshLogBox.test.ts @@ -114,8 +114,7 @@ describe('ReactRefreshLogBox app', () => { await cleanup() }) - // TODO-APP: re-enable when error recovery doesn't reload the page. - test.skip('logbox: can recover from a event handler error', async () => { + test('logbox: can recover from a event handler error', async () => { const { session, cleanup } = await sandbox(next) await session.patch( @@ -147,7 +146,7 @@ describe('ReactRefreshLogBox app', () => { await session.evaluate(() => document.querySelector('p').textContent) ).toBe('1') - expect(await session.hasRedbox(true)).toBe(true) + await session.waitForAndOpenRuntimeError() if (process.platform === 'win32') { expect(await session.getRedboxSource()).toMatchSnapshot() } else { @@ -173,6 +172,7 @@ describe('ReactRefreshLogBox app', () => { ) expect(await session.hasRedbox()).toBe(false) + expect(await session.hasErrorToast()).toBe(false) expect( await session.evaluate(() => document.querySelector('p').textContent) @@ -183,6 +183,7 @@ describe('ReactRefreshLogBox app', () => { ).toBe('Count: 2') expect(await session.hasRedbox()).toBe(false) + expect(await session.hasErrorToast()).toBe(false) await cleanup() }) diff --git a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap index 831fe30c3b32a7d..5cf2a2113c4af11 100644 --- a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap +++ b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap @@ -66,6 +66,18 @@ exports[`ReactRefreshLogBox app logbox: can recover from a component error 1`] = 6 | " `; +exports[`ReactRefreshLogBox app logbox: can recover from a event handler error 1`] = ` +"index.js (8:18) @ eval + + 6 | const increment = useCallback(() => { + 7 | setCount(c => c + 1) +> 8 | throw new Error('oops') + | ^ + 9 | }, [setCount]) + 10 | return ( + 11 |
" +`; + exports[`ReactRefreshLogBox app logbox: can recover from a syntax error without losing state 1`] = ` "./index.js Error: diff --git a/test/development/acceptance-app/helpers.ts b/test/development/acceptance-app/helpers.ts index c97d311cb6bc74f..4fd63ce976889bb 100644 --- a/test/development/acceptance-app/helpers.ts +++ b/test/development/acceptance-app/helpers.ts @@ -100,6 +100,15 @@ export async function sandbox( async hasRedbox(expected = false) { return hasRedbox(browser, expected) }, + async hasErrorToast() { + return browser.eval(() => { + return Boolean( + Array.from(document.querySelectorAll('nextjs-portal')).find((p) => + p.shadowRoot.querySelector('[data-nextjs-toast]') + ) + ) + }) + }, async getRedboxDescription() { return getRedboxDescription(browser) },