Skip to content

Commit

Permalink
throw error for noSSR and recover from error on client
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi committed Nov 29, 2022
1 parent a7be071 commit e49edd9
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 36 deletions.
20 changes: 17 additions & 3 deletions packages/next/client/app-index.tsx
Expand Up @@ -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'

/// <reference types="react-dom/experimental" />

Expand Down Expand Up @@ -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(
<GlobalLayoutRouterContext.Provider
Expand Down Expand Up @@ -231,11 +238,18 @@ export function hydrate() {
</StrictModeIfEnabled>
)

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)
Expand Down
61 changes: 46 additions & 15 deletions packages/next/client/components/layout-router.tsx
Expand Up @@ -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.
Expand Down Expand Up @@ -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 <DynamicErrorBoundary>{children}</DynamicErrorBoundary>
}

function RedirectBoundary({ children }: { children: React.ReactNode }) {
const router = useRouter()
return (
Expand Down Expand Up @@ -496,21 +525,23 @@ export default function OuterLayoutRouter({
notFoundStyles={notFoundStyles}
>
<RedirectBoundary>
<InnerLayoutRouter
parallelRouterKey={parallelRouterKey}
url={url}
tree={tree}
childNodes={childNodesForParallelRouter!}
childProp={
childPropSegment === preservedSegment
? childProp
: null
}
segmentPath={segmentPath}
path={preservedSegment}
isActive={currentChildSegment === preservedSegment}
rootLayoutIncluded={rootLayoutIncluded}
/>
<DynamicBoundary>
<InnerLayoutRouter
parallelRouterKey={parallelRouterKey}
url={url}
tree={tree}
childNodes={childNodesForParallelRouter!}
childProp={
childPropSegment === preservedSegment
? childProp
: null
}
segmentPath={segmentPath}
path={preservedSegment}
isActive={currentChildSegment === preservedSegment}
rootLayoutIncluded={rootLayoutIncluded}
/>
</DynamicBoundary>
</RedirectBoundary>
</NotFoundBoundary>
</LoadingBoundary>
Expand Down
9 changes: 8 additions & 1 deletion packages/next/client/index.tsx
Expand Up @@ -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'

/// <reference types="react-dom/experimental" />

Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions packages/next/export/worker.ts
Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/next/server/app-render.tsx
Expand Up @@ -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'

Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions packages/next/server/render.tsx
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
},
},
})
}

Expand Down
41 changes: 24 additions & 17 deletions 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<P = {}> = Promise<
React.ComponentType<P> | { default: React.ComponentType<P> }
>
constructor() {
super('next/dynamic with noSSR on server')
}
}

export type LoaderComponent<P = {}> = Promise<{
default: React.ComponentType<P>
}>

type LazyComponentLoader<P = {}> = () => LoaderComponent<P>

export type Loader<P = {}> = (() => LoaderComponent<P>) | LoaderComponent<P>

Expand Down Expand Up @@ -51,23 +60,21 @@ export function noSSR<P = {}>(
delete loadableOptions.webpack
delete loadableOptions.modules

const NoSSRComponent = React.lazy(
(isServerSide
? async () => ({ default: () => null })
: loadableOptions.loader) as () => Promise<{
default: React.ComponentType<P>
}>
)
const NoSSRComponent =
typeof window === 'undefined'
? ((() => {
throw new NextDynamicNoSSRError()
}) as React.FunctionComponent<P>)
: React.lazy(loadableOptions.loader as LazyComponentLoader<P>)

const Loading = loadableOptions.loading!
const fallback = (
<Loading error={null} isLoading pastDelay={false} timedOut={false} />
)

return () => (
<Suspense
fallback={
<Loading error={null} isLoading pastDelay={false} timedOut={false} />
}
>
{/* @ts-ignore */}
<Suspense fallback={fallback}>
{/* @ts-ignore TODO: fix typing */}
<NoSSRComponent />
</Suspense>
)
Expand Down

0 comments on commit e49edd9

Please sign in to comment.