)
+ 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 */}
)