Skip to content

Commit

Permalink
add error boundary to dynamic component
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi committed Nov 30, 2022
1 parent 711959f commit 74ebe8d
Show file tree
Hide file tree
Showing 10 changed files with 102 additions and 89 deletions.
10 changes: 2 additions & 8 deletions packages/next/client/app-index.tsx
Expand Up @@ -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'

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

Expand Down Expand Up @@ -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(
<GlobalLayoutRouterContext.Provider
Expand Down
61 changes: 15 additions & 46 deletions packages/next/client/components/layout-router.tsx
Expand Up @@ -26,7 +26,6 @@ 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 @@ -343,34 +342,6 @@ 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 @@ -525,23 +496,21 @@ export default function OuterLayoutRouter({
notFoundStyles={notFoundStyles}
>
<RedirectBoundary>
<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>
<InnerLayoutRouter
parallelRouterKey={parallelRouterKey}
url={url}
tree={tree}
childNodes={childNodesForParallelRouter!}
childProp={
childPropSegment === preservedSegment
? childProp
: null
}
segmentPath={segmentPath}
path={preservedSegment}
isActive={currentChildSegment === preservedSegment}
rootLayoutIncluded={rootLayoutIncluded}
/>
</RedirectBoundary>
</NotFoundBoundary>
</LoadingBoundary>
Expand Down
2 changes: 1 addition & 1 deletion packages/next/client/index.tsx
Expand Up @@ -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'

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

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

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/next/server/app-render.tsx
Expand Up @@ -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'
Expand All @@ -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'

Expand Down
39 changes: 39 additions & 0 deletions 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 <DynamicErrorBoundary>{children}</DynamicErrorBoundary>
}
37 changes: 18 additions & 19 deletions 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<P = {}> = Promise<{
default: React.ComponentType<P>
}>

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

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

export type LoaderMap = { [module: string]: () => Loader<any> }

Expand Down Expand Up @@ -52,6 +44,12 @@ export type LoadableFn<P = {}> = (

export type LoadableComponent<P = {}> = React.ComponentType<P>

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<P = {}>(
_LoadableInitializer: LoadableFn<P>,
loadableOptions: DynamicOptions<P>
Expand All @@ -60,12 +58,12 @@ export function noSSR<P = {}>(
delete loadableOptions.webpack
delete loadableOptions.modules

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

const NoSSRComponent = React.lazy<React.ComponentType>(loader as Loader)

const Loading = loadableOptions.loading!
const fallback = (
Expand All @@ -74,8 +72,9 @@ export function noSSR<P = {}>(

return () => (
<Suspense fallback={fallback}>
{/* @ts-ignore TODO: fix typing */}
<NoSSRComponent />
<DynamicBoundary>
<NoSSRComponent />
</DynamicBoundary>
</Suspense>
)
}
Expand Down
7 changes: 6 additions & 1 deletion packages/next/shared/lib/loadable.js
Expand Up @@ -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 = []
Expand Down Expand Up @@ -129,7 +130,11 @@ function createLoadableComponent(loadFn, options) {
{
fallback: fallbackElement,
},
React.createElement(opts.lazy, props)
React.createElement(
DynamicBoundary,
null,
React.createElement(opts.lazy, props)
)
)
}

Expand Down
Expand Up @@ -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 <Dynamic />
Expand Down
@@ -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 <Dynamic />
Expand Down

0 comments on commit 74ebe8d

Please sign in to comment.