diff --git a/packages/next/client/app-index.tsx b/packages/next/client/app-index.tsx index b13c0927556323..9c479f1dc32384 100644 --- a/packages/next/client/app-index.tsx +++ b/packages/next/client/app-index.tsx @@ -7,6 +7,7 @@ import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-we import { HeadManagerContext } from '../shared/lib/head-manager-context' import { GlobalLayoutRouterContext } from '../shared/lib/app-router-context' +import { NEXT_DYNAMIC_NO_SSR_CODE } from '../shared/lib/dynamic' /// @@ -190,7 +191,13 @@ export function hydrate() { if (rootLayoutMissingTagsError) { const reactRootElement = document.createElement('div') document.body.appendChild(reactRootElement) - const reactRoot = (ReactDOMClient as any).createRoot(reactRootElement) + const reactRoot = (ReactDOMClient as any).createRoot(reactRootElement, { + onRecoverableError(err: any) { + // Skip certain custom errors which are not expected to throw on client + if (err.digest === NEXT_DYNAMIC_NO_SSR_CODE) return + throw err + }, + }) reactRoot.render( ) + const options = { + onRecoverableError(err: any) { + // Skip certain custom errors which are not expected to throw on client + if (err.digest === NEXT_DYNAMIC_NO_SSR_CODE) return + throw err + }, + } const isError = document.documentElement.id === '__next_error__' const reactRoot = isError - ? (ReactDOMClient as any).createRoot(appElement) + ? (ReactDOMClient as any).createRoot(appElement, options) : (React as any).startTransition(() => - (ReactDOMClient as any).hydrateRoot(appElement, reactEl) + (ReactDOMClient as any).hydrateRoot(appElement, reactEl, options) ) if (isError) { reactRoot.render(reactEl) diff --git a/packages/next/client/components/layout-router.tsx b/packages/next/client/components/layout-router.tsx index 36bcaaf3259278..2f976bf4cad594 100644 --- a/packages/next/client/components/layout-router.tsx +++ b/packages/next/client/components/layout-router.tsx @@ -26,6 +26,7 @@ import { createInfinitePromise } from './infinite-promise' import { ErrorBoundary } from './error-boundary' import { matchSegment } from './match-segments' import { useRouter } from './navigation' +import { NEXT_DYNAMIC_NO_SSR_CODE } from '../../shared/lib/dynamic' /** * Add refetch marker to router state at the point of the current layout segment. @@ -342,6 +343,34 @@ class RedirectErrorBoundary extends React.Component< } } +class DynamicErrorBoundary extends React.Component< + { children: React.ReactNode }, + { noSSR: boolean } +> { + constructor(props: { children: React.ReactNode }) { + super(props) + this.state = { noSSR: false } + } + static getDerivedStateFromError(error: any) { + if (error.digest === NEXT_DYNAMIC_NO_SSR_CODE) { + return { noSSR: true } + } + // Re-throw if error is not for dynamic + throw error + } + + render() { + if (this.state.noSSR) { + return null + } + return this.props.children + } +} + +function DynamicBoundary({ children }: { children: React.ReactNode }) { + return {children} +} + function RedirectBoundary({ children }: { children: React.ReactNode }) { const router = useRouter() return ( @@ -496,21 +525,23 @@ export default function OuterLayoutRouter({ notFoundStyles={notFoundStyles} > - + + + diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index a5baeae1648c63..6610370d0a861b 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -43,6 +43,7 @@ import { PathnameContextProviderAdapter, } from '../shared/lib/router/adapters' import { SearchParamsContext } from '../shared/lib/hooks-client-context' +import { NEXT_DYNAMIC_NO_SSR_CODE } from '../shared/lib/dynamic' /// @@ -510,7 +511,13 @@ function renderReactElement( const reactEl = fn(shouldHydrate ? markHydrateComplete : markRenderComplete) if (!reactRoot) { // Unlike with createRoot, you don't need a separate root.render() call here - reactRoot = ReactDOM.hydrateRoot(domEl, reactEl) + reactRoot = ReactDOM.hydrateRoot(domEl, reactEl, { + onRecoverableError(err: any) { + // Skip certain custom errors which are not expected to throw on client + if (err.message === NEXT_DYNAMIC_NO_SSR_CODE) return + throw err + }, + }) // TODO: Remove shouldHydrate variable when React 18 is stable as it can depend on `reactRoot` existing shouldHydrate = false } else { diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index 9ee4a4d7f62cf9..a4224396a184de 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -32,6 +32,7 @@ import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' import { REDIRECT_ERROR_CODE } from '../client/components/redirect' import { DYNAMIC_ERROR_CODE } from '../client/components/hooks-server-context' import { NOT_FOUND_ERROR_CODE } from '../client/components/not-found' +import { NEXT_DYNAMIC_NO_SSR_CODE } from '../shared/lib/dynamic' loadRequireHook() @@ -343,6 +344,7 @@ export default async function exportPage({ if ( err.digest !== DYNAMIC_ERROR_CODE && err.digest !== NOT_FOUND_ERROR_CODE && + err.digest !== NEXT_DYNAMIC_NO_SSR_CODE && !err.digest?.startsWith(REDIRECT_ERROR_CODE) ) { throw err diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index cfa39d4708cf0b..1884e98f0e9424 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -45,6 +45,7 @@ import { FLIGHT_PARAMETERS, } from '../client/components/app-router-headers' import type { StaticGenerationStore } from '../client/components/static-generation-async-storage' +import { NEXT_DYNAMIC_NO_SSR_CODE } from '../shared/lib/dynamic' const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge' @@ -215,6 +216,7 @@ function createErrorHandler( if ( err.digest === DYNAMIC_ERROR_CODE || err.digest === NOT_FOUND_ERROR_CODE || + err.digest === NEXT_DYNAMIC_NO_SSR_CODE || err.digest?.startsWith(REDIRECT_ERROR_CODE) ) { return err.digest diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index c5b2686a6e2b5f..eb9b74dece7062 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -89,6 +89,7 @@ import { } from '../shared/lib/router/adapters' import { AppRouterContext } from '../shared/lib/app-router-context' import { SearchParamsContext } from '../shared/lib/hooks-client-context' +import { NEXT_DYNAMIC_NO_SSR_CODE } from '../shared/lib/dynamic' let tryGetPreviewData: typeof import('./api-utils/node').tryGetPreviewData let warn: typeof import('../build/output/log').warn @@ -1244,6 +1245,13 @@ export async function renderToHTML( return await renderToInitialStream({ ReactDOMServer, element: content, + streamOptions: { + onError(e: any) { + if (e.digest === NEXT_DYNAMIC_NO_SSR_CODE) { + return e.digest + } + }, + }, }) } diff --git a/packages/next/shared/lib/dynamic.tsx b/packages/next/shared/lib/dynamic.tsx index b3a5adeb253c66..b34bef807bd1f2 100644 --- a/packages/next/shared/lib/dynamic.tsx +++ b/packages/next/shared/lib/dynamic.tsx @@ -1,11 +1,20 @@ import React, { Suspense } from 'react' import Loadable from './loadable' -const isServerSide = typeof window === 'undefined' +export const NEXT_DYNAMIC_NO_SSR_CODE = 'DYNAMIC_SERVER_USAGE' +export class NextDynamicNoSSRError extends Error { + digest: typeof NEXT_DYNAMIC_NO_SSR_CODE = NEXT_DYNAMIC_NO_SSR_CODE -export type LoaderComponent

= Promise< - React.ComponentType

| { default: React.ComponentType

} -> + constructor() { + super('next/dynamic with noSSR on server') + } +} + +export type LoaderComponent

= Promise<{ + default: React.ComponentType

+}> + +type LazyComponentLoader

= () => LoaderComponent

export type Loader

= (() => LoaderComponent

) | LoaderComponent

@@ -51,23 +60,21 @@ export function noSSR

( delete loadableOptions.webpack delete loadableOptions.modules - const NoSSRComponent = React.lazy( - (isServerSide - ? async () => ({ default: () => null }) - : loadableOptions.loader) as () => Promise<{ - default: React.ComponentType

- }> - ) + const NoSSRComponent = + typeof window === 'undefined' + ? ((() => { + throw new NextDynamicNoSSRError() + }) as React.FunctionComponent

) + : React.lazy(loadableOptions.loader as LazyComponentLoader

) const Loading = loadableOptions.loading! + const fallback = ( + + ) return () => ( - - } - > - {/* @ts-ignore */} + + {/* @ts-ignore TODO: fix typing */} )