Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions errors/missing-suspense-with-csr-bailout.mdx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { BailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
import { throwWithNoSSR } from '../../shared/lib/lazy-dynamic/no-ssr-error'
import { staticGenerationAsyncStorage } from './static-generation-async-storage.external'

export function bailoutToClientRendering(reason: string): void | never {
export function bailoutToClientRendering(): void | never {
const staticGenerationStore = staticGenerationAsyncStorage.getStore()

if (staticGenerationStore?.forceStatic) return
if (staticGenerationStore?.forceStatic) {
return
}

if (staticGenerationStore?.isStaticGeneration)
throw new BailoutToCSRError(reason)
if (staticGenerationStore?.isStaticGeneration) {
throwWithNoSSR()
}
}
2 changes: 1 addition & 1 deletion packages/next/src/client/components/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export function useSearchParams(): ReadonlyURLSearchParams {
const { bailoutToClientRendering } =
require('./bailout-to-client-rendering') as typeof import('./bailout-to-client-rendering')
// TODO-APP: handle dynamic = 'force-static' here and on the client
bailoutToClientRendering('useSearchParams()')
bailoutToClientRendering()
}

return readonlySearchParams
Expand Down
6 changes: 3 additions & 3 deletions packages/next/src/client/on-recoverable-error.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isBailoutToCSRError } from '../shared/lib/lazy-dynamic/bailout-to-csr'
import { isBailoutCSRError } from '../shared/lib/lazy-dynamic/no-ssr-error'

export default function onRecoverableError(err: unknown) {
export default function onRecoverableError(err: any) {
// Using default react onRecoverableError
// x-ref: https://github.com/facebook/react/blob/d4bc16a7d69eb2ea38a88c8ac0b461d5f72cdcab/packages/react-dom/src/client/ReactDOMRoot.js#L83
const defaultOnRecoverableError =
Expand All @@ -13,7 +13,7 @@ export default function onRecoverableError(err: unknown) {
}

// Skip certain custom errors which are not expected to be reported on client
if (isBailoutToCSRError(err)) return
if (isBailoutCSRError(err)) return

defaultOnRecoverableError(err)
}
2 changes: 2 additions & 0 deletions packages/next/src/export/helpers/is-dynamic-usage-error.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { DYNAMIC_ERROR_CODE } from '../../client/components/hooks-server-context'
import { isNotFoundError } from '../../client/components/not-found'
import { isRedirectError } from '../../client/components/redirect'
import { isBailoutCSRError } from '../../shared/lib/lazy-dynamic/no-ssr-error'

export const isDynamicUsageError = (err: any) =>
err.digest === DYNAMIC_ERROR_CODE ||
isNotFoundError(err) ||
isBailoutCSRError(err) ||
isRedirectError(err)
6 changes: 1 addition & 5 deletions packages/next/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,11 +506,7 @@ export async function exportAppImpl(
: {}),
strictNextHead: !!nextConfig.experimental.strictNextHead,
deploymentId: nextConfig.experimental.deploymentId,
experimental: {
ppr: nextConfig.experimental.ppr === true,
missingSuspenseWithCSRBailout:
nextConfig.experimental.missingSuspenseWithCSRBailout,
},
experimental: { ppr: nextConfig.experimental.ppr === true },
}

const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig
Expand Down
14 changes: 9 additions & 5 deletions packages/next/src/export/routes/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
NEXT_DATA_SUFFIX,
SERVER_PROPS_EXPORT_ERROR,
} from '../../lib/constants'
import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
import { isBailoutCSRError } from '../../shared/lib/lazy-dynamic/no-ssr-error'
import AmpHtmlValidator from 'next/dist/compiled/amphtml-validator'
import { FileType, fileExists } from '../../lib/file-exists'
import { lazyRenderPagesPage } from '../../server/future/route-modules/pages/module.render'
Expand Down Expand Up @@ -105,8 +105,10 @@ export async function exportPages(
query,
renderOpts
)
} catch (err) {
if (!isBailoutToCSRError(err)) throw err
} catch (err: any) {
if (!isBailoutCSRError(err)) {
throw err
}
}
}

Expand Down Expand Up @@ -161,8 +163,10 @@ export async function exportPages(
{ ...query, amp: '1' },
renderOpts
)
} catch (err) {
if (!isBailoutToCSRError(err)) throw err
} catch (err: any) {
if (!isBailoutCSRError(err)) {
throw err
}
}

const ampHtml =
Expand Down
7 changes: 2 additions & 5 deletions packages/next/src/export/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import { createIncrementalCache } from './helpers/create-incremental-cache'
import { isPostpone } from '../server/lib/router-utils/is-postpone'
import { isMissingPostponeDataError } from '../server/app-render/is-missing-postpone-error'
import { isDynamicUsageError } from './helpers/is-dynamic-usage-error'
import { isBailoutToCSRError } from '../shared/lib/lazy-dynamic/bailout-to-csr'

const envConfig = require('../shared/lib/runtime-config.external')

Expand Down Expand Up @@ -319,11 +318,9 @@ async function exportPageImpl(
// if this is a postpone error, it's logged elsewhere, so no need to log it again here
if (!isMissingPostponeDataError(err)) {
console.error(
`\nError occurred prerendering page "${path}". Read more: https://nextjs.org/docs/messages/prerender-error\n`
`\nError occurred prerendering page "${path}". Read more: https://nextjs.org/docs/messages/prerender-error\n` +
(isError(err) && err.stack ? err.stack : err)
)
if (!isBailoutToCSRError(err)) {
console.error(isError(err) && err.stack ? err.stack : err)
}
}

return { error: true }
Expand Down
19 changes: 6 additions & 13 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ import { parseAndValidateFlightRouterState } from './parse-and-validate-flight-r
import { validateURL } from './validate-url'
import { createFlightRouterStateFromLoaderTree } from './create-flight-router-state-from-loader-tree'
import { handleAction } from './action-handler'
import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
import { isBailoutCSRError } from '../../shared/lib/lazy-dynamic/no-ssr-error'
import { warn, error } from '../../build/output/log'
import { appendMutableCookies } from '../web/spec-extension/adapters/request-cookies'
import { createServerInsertedHTML } from './server-inserted-html'
Expand Down Expand Up @@ -996,19 +996,12 @@ async function renderToHTMLOrFlightImpl(
throw err
}

/** True if this error was a bailout to client side rendering error. */
const shouldBailoutToCSR = isBailoutToCSRError(err)
// True if this error was a bailout to client side rendering error.
const shouldBailoutToCSR = isBailoutCSRError(err)
if (shouldBailoutToCSR) {
console.log()

if (renderOpts.experimental.missingSuspenseWithCSRBailout) {
error(
`${err.message} should be wrapped in a suspense boundary at page "${pagePath}". https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout`
)
throw err
}
warn(
`Entire page "${pagePath}" deopted into client-side rendering. https://nextjs.org/docs/messages/deopted-into-client-rendering`
`Entire page ${pagePath} deopted into client-side rendering. https://nextjs.org/docs/messages/deopted-into-client-rendering`,
pagePath
)
}

Expand Down Expand Up @@ -1219,7 +1212,7 @@ async function renderToHTMLOrFlightImpl(
renderOpts.experimental.ppr &&
staticGenerationStore.postponeWasTriggered &&
!metadata.postponed &&
(!response.err || !isBailoutToCSRError(response.err))
(!response.err || !isBailoutCSRError(response.err))
) {
// a call to postpone was made but was caught and not detected by Next.js. We should fail the build immediately
// as we won't be able to generate the static part
Expand Down
8 changes: 3 additions & 5 deletions packages/next/src/server/app-render/create-error-handler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { formatServerError } from '../../lib/format-server-error'
import { SpanStatusCode, getTracer } from '../lib/trace/tracer'
import { isAbortError } from '../pipe-readable'
import { isDynamicUsageError } from '../../export/helpers/is-dynamic-usage-error'
import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'

export type ErrorHandler = (err: any) => string | undefined

Expand Down Expand Up @@ -35,12 +34,11 @@ export function createErrorHandler({
return (err) => {
if (allCapturedErrors) allCapturedErrors.push(err)

// A formatted error is already logged for this type of error
if (isBailoutToCSRError(err)) return

// These errors are expected. We return the digest
// so that they can be properly handled.
if (isDynamicUsageError(err)) return err.digest
if (isDynamicUsageError(err)) {
return err.digest
}

// If the response was closed, we don't need to log the error.
if (isAbortError(err)) return
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/app-render/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export interface RenderOptsPartial {
}
params?: ParsedUrlQuery
isPrefetch?: boolean
experimental: { ppr: boolean; missingSuspenseWithCSRBailout?: boolean }
experimental: { ppr: boolean }
postponed?: string
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type StaticGenerationContext = {
isDraftMode?: boolean
isServerAction?: boolean
waitUntil?: Promise<any>
experimental: { ppr: boolean; missingSuspenseWithCSRBailout?: boolean }
experimental: { ppr: boolean }

/**
* A hack around accessing the store value outside the context of the
Expand Down
1 change: 0 additions & 1 deletion packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,6 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
staticWorkerRequestDeduping: z.boolean().optional(),
useWasmBinary: z.boolean().optional(),
useLightningcss: z.boolean().optional(),
missingSuspenseWithCSRBailout: z.boolean().optional(),
})
.optional(),
exportPathMap: z
Expand Down
11 changes: 0 additions & 11 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,16 +380,6 @@ export interface ExperimentalConfig {
* Use lightningcss instead of swc_css
*/
useLightningcss?: boolean

/**
* Certain methods calls like `useSearchParams()` can bail out of server-side rendering of **entire** pages to client-side rendering,
* if they are not wrapped in a suspense boundary.
*
* When this flag is set to `true`, Next.js will break the build instead of warning, to force the developer to add a suspense boundary above the method call.
*
* @default false
*/
missingSuspenseWithCSRBailout?: boolean
}

export type ExportPathMap = {
Expand Down Expand Up @@ -852,7 +842,6 @@ export const defaultConfig: NextConfig = {
? true
: false,
webpackBuildWorker: undefined,
missingSuspenseWithCSRBailout: false,
},
}

Expand Down
14 changes: 0 additions & 14 deletions packages/next/src/shared/lib/lazy-dynamic/bailout-to-csr.ts

This file was deleted.

This file was deleted.

14 changes: 14 additions & 0 deletions packages/next/src/shared/lib/lazy-dynamic/dynamic-no-ssr.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client'

import type React from 'react'
import { throwWithNoSSR } from './no-ssr-error'

type Child = React.ReactElement<any, any>

export function NoSSR({ children }: { children: Child }): Child {
if (typeof window === 'undefined') {
throwWithNoSSR()
}

return children
}
46 changes: 22 additions & 24 deletions packages/next/src/shared/lib/lazy-dynamic/loadable.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,43 @@
import { Suspense, lazy } from 'react'
import { BailoutToCSR } from './dynamic-bailout-to-csr'
import { Suspense, lazy, Fragment } from 'react'
import { NoSSR } from './dynamic-no-ssr'
import type { ComponentModule } from './types'

// Normalize loader to return the module as form { default: Component } for `React.lazy`.
// Also for backward compatible since next/dynamic allows to resolve a component directly with loader
// Client component reference proxy need to be converted to a module.
function convertModule<P>(mod: React.ComponentType<P> | ComponentModule<P>) {
return { default: (mod as ComponentModule<P>)?.default ?? mod }
return { default: (mod as ComponentModule<P>)?.default || mod }
}

const defaultOptions = {
loader: () => Promise.resolve(convertModule(() => null)),
loading: null,
ssr: true,
}
function Loadable(options: any) {
const opts = {
loader: null,
loading: null,
ssr: true,
...options,
}

interface LoadableOptions {
loader?: () => Promise<React.ComponentType<any> | ComponentModule<any>>
loading?: React.ComponentType<any> | null
ssr?: boolean
}
const loader = () =>
opts.loader != null
? opts.loader().then(convertModule)
: Promise.resolve(convertModule(() => null))

function Loadable(options: LoadableOptions) {
const opts = { ...defaultOptions, ...options }
const Lazy = lazy(() => opts.loader().then(convertModule))
const Lazy = lazy(loader)
const Loading = opts.loading
const Wrap = opts.ssr ? Fragment : NoSSR

function LoadableComponent(props: any) {
const fallbackElement = Loading ? (
<Loading isLoading={true} pastDelay={true} error={null} />
) : null

const children = opts.ssr ? (
<Lazy {...props} />
) : (
<BailoutToCSR reason="next/dynamic">
<Lazy {...props} />
</BailoutToCSR>
return (
<Suspense fallback={fallbackElement}>
<Wrap>
<Lazy {...props} />
</Wrap>
</Suspense>
)

return <Suspense fallback={fallbackElement}>{children}</Suspense>
}

LoadableComponent.displayName = 'LoadableComponent'
Expand Down
13 changes: 13 additions & 0 deletions packages/next/src/shared/lib/lazy-dynamic/no-ssr-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// This has to be a shared module which is shared between client component error boundary and dynamic component

export const NEXT_DYNAMIC_NO_SSR_CODE = 'NEXT_DYNAMIC_NO_SSR_CODE'

export function throwWithNoSSR() {
const error = new Error(NEXT_DYNAMIC_NO_SSR_CODE)
;(error as any).digest = NEXT_DYNAMIC_NO_SSR_CODE
throw error
}

export function isBailoutCSRError(err: any) {
return err?.digest === NEXT_DYNAMIC_NO_SSR_CODE
}
Loading