diff --git a/packages/next/client/app-index.tsx b/packages/next/client/app-index.tsx index 9c479f1dc32384d..e15c2189741a139 100644 --- a/packages/next/client/app-index.tsx +++ b/packages/next/client/app-index.tsx @@ -7,7 +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' +import { NEXT_DYNAMIC_NO_SSR_CODE } from '../shared/lib/dynamic-error-boundary' /// @@ -191,13 +191,7 @@ export function hydrate() { if (rootLayoutMissingTagsError) { const reactRootElement = document.createElement('div') document.body.appendChild(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 - }, - }) + const reactRoot = (ReactDOMClient as any).createRoot(reactRootElement) reactRoot.render( { - 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 ( @@ -525,23 +496,21 @@ export default function OuterLayoutRouter({ notFoundStyles={notFoundStyles} > - - - + diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index 6610370d0a861b6..17737d4cae1d53e 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -43,7 +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' +import { NEXT_DYNAMIC_NO_SSR_CODE } from '../shared/lib/dynamic-error-boundary' /// diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index a4224396a184de3..b31b6bb40cda3e0 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -32,7 +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' +import { NEXT_DYNAMIC_NO_SSR_CODE } from '../shared/lib/dynamic-error-boundary' loadRequireHook() @@ -395,14 +395,21 @@ export default async function exportPage({ if (optimizeCss) { process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true) } - renderResult = await renderMethod( - req, - res, - page, - query, - // @ts-ignore - curRenderOpts - ) + try { + renderResult = await renderMethod( + req, + res, + page, + query, + // @ts-ignore + curRenderOpts + ) + } catch (err: any) { + console.log('page static', err) + if (err.digest !== NEXT_DYNAMIC_NO_SSR_CODE) { + throw err + } + } } results.ssgNotFound = (curRenderOpts as any).isNotFound diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index 1884e98f0e94242..98588b771f86c24 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -35,6 +35,7 @@ import { REDIRECT_ERROR_CODE } from '../client/components/redirect' import { RequestCookies } from './web/spec-extension/cookies' 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-error-boundary' import { HeadManagerContext } from '../shared/lib/head-manager-context' import { Writable } from 'stream' import stringHash from 'next/dist/compiled/string-hash' @@ -45,7 +46,6 @@ 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' diff --git a/packages/next/shared/lib/dynamic-error-boundary.tsx b/packages/next/shared/lib/dynamic-error-boundary.tsx new file mode 100644 index 000000000000000..53ece4798fdd1e1 --- /dev/null +++ b/packages/next/shared/lib/dynamic-error-boundary.tsx @@ -0,0 +1,39 @@ +'use client' + +import React from 'react' + +export const NEXT_DYNAMIC_NO_SSR_CODE = 'DYNAMIC_SERVER_USAGE' + +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) { + console.log('error.digest', error.digest, error.message) + if (error.digest === 'DYNAMIC_SERVER_USAGE') { + return { noSSR: true } + } + // Re-throw if error is not for dynamic + throw error + } + + render() { + if (this.state.noSSR) { + return null + } + return this.props.children + } +} + +export default function DynamicBoundary({ + children, +}: { + children: React.ReactNode +}) { + return {children} +} diff --git a/packages/next/shared/lib/dynamic.tsx b/packages/next/shared/lib/dynamic.tsx index b34bef807bd1f28..74233c9071a6166 100644 --- a/packages/next/shared/lib/dynamic.tsx +++ b/packages/next/shared/lib/dynamic.tsx @@ -1,22 +1,14 @@ import React, { Suspense } from 'react' import Loadable from './loadable' - -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 - - constructor() { - super('next/dynamic with noSSR on server') - } -} +import DynamicBoundary, { + NEXT_DYNAMIC_NO_SSR_CODE, +} from './dynamic-error-boundary' export type LoaderComponent

= Promise<{ default: React.ComponentType

}> -type LazyComponentLoader

= () => LoaderComponent

- -export type Loader

= (() => LoaderComponent

) | LoaderComponent

+export type Loader

= () => LoaderComponent

export type LoaderMap = { [module: string]: () => Loader } @@ -52,6 +44,12 @@ export type LoadableFn

= ( export type LoadableComponent

= React.ComponentType

+function DynamicThrownOnServer() { + const error = new Error('next/dynamic with noSSR on server') + ;(error as any).digest = NEXT_DYNAMIC_NO_SSR_CODE + throw error +} + export function noSSR

( _LoadableInitializer: LoadableFn

, loadableOptions: DynamicOptions

@@ -60,12 +58,12 @@ export function noSSR

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

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

) + ? async () => ({ default: DynamicThrownOnServer }) + : loadableOptions.loader + + const NoSSRComponent = React.lazy(loader as Loader) const Loading = loadableOptions.loading! const fallback = ( @@ -74,8 +72,9 @@ export function noSSR

( return () => ( - {/* @ts-ignore TODO: fix typing */} - + + + ) } diff --git a/packages/next/shared/lib/loadable.js b/packages/next/shared/lib/loadable.js index 4df5d11263e7019..94d2a248f809c1b 100644 --- a/packages/next/shared/lib/loadable.js +++ b/packages/next/shared/lib/loadable.js @@ -23,6 +23,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE import React from 'react' import { LoadableContext } from './loadable-context' +import DynamicBoundary from './dynamic-error-boundary' const ALL_INITIALIZERS = [] const READY_INITIALIZERS = [] @@ -129,7 +130,11 @@ function createLoadableComponent(loadFn, options) { { fallback: fallbackElement, }, - React.createElement(opts.lazy, props) + React.createElement( + DynamicBoundary, + null, + React.createElement(opts.lazy, props) + ) ) } diff --git a/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic-client.js b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic-client.js index bfa17f6a14b6761..e975ee3565ffb6a 100644 --- a/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic-client.js +++ b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic-client.js @@ -2,7 +2,9 @@ import dynamic from 'next/dynamic' -const Dynamic = dynamic(() => import('../text-dynamic-client')) +const Dynamic = dynamic(() => import('../text-dynamic-client'), { + ssr: false, +}) export function NextDynamicClientComponent() { return diff --git a/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic-server.js b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic-server.js index 267a1febc5daac3..665da40efa5d2a5 100644 --- a/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic-server.js +++ b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic-server.js @@ -1,8 +1,6 @@ import dynamic from 'next/dynamic' -const Dynamic = dynamic(() => import('../text-dynamic-server'), { - ssr: false, -}) +const Dynamic = dynamic(() => import('../text-dynamic-server')) export function NextDynamicServerComponent() { return