Skip to content

Commit

Permalink
Global layouts error boundary (#41305)
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi committed Oct 11, 2022
1 parent 3c8727d commit a78163d
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 108 deletions.
25 changes: 18 additions & 7 deletions packages/next/client/components/app-router.client.tsx
Expand Up @@ -28,6 +28,7 @@ import {
// LayoutSegmentsContext,
} from './hooks-client-context'
import { useReducerWithReduxDevtools } from './use-reducer-with-devtools'
import { ErrorBoundary, GlobalErrorComponent } from './error-boundary'

function urlToUrlWithoutFlightMarker(url: string): URL {
const urlWithoutFlightParameters = new URL(url, location.origin)
Expand Down Expand Up @@ -91,20 +92,22 @@ let initialParallelRoutes: CacheNode['parallelRoutes'] =

const prefetched = new Set<string>()

type AppRouterProps = {
initialTree: FlightRouterState
initialCanonicalUrl: string
children: ReactNode
assetPrefix: string
}

/**
* The global router that wraps the application components.
*/
export default function AppRouter({
function Router({
initialTree,
initialCanonicalUrl,
children,
assetPrefix,
}: {
initialTree: FlightRouterState
initialCanonicalUrl: string
children: ReactNode
assetPrefix: string
}) {
}: AppRouterProps) {
const initialState = useMemo(() => {
return {
tree: initialTree,
Expand Down Expand Up @@ -367,3 +370,11 @@ export default function AppRouter({
</PathnameContext.Provider>
)
}

export default function AppRouter(props: AppRouterProps) {
return (
<ErrorBoundary errorComponent={GlobalErrorComponent}>
<Router {...props} />
</ErrorBoundary>
)
}
108 changes: 108 additions & 0 deletions packages/next/client/components/error-boundary.tsx
@@ -0,0 +1,108 @@
import React from 'react'

export type ErrorComponent = React.ComponentType<{
error: Error
reset: () => void
}>
interface ErrorBoundaryProps {
errorComponent: ErrorComponent
}

/**
* Handles errors through `getDerivedStateFromError`.
* Renders the provided error component and provides a way to `reset` the error boundary state.
*/
class ErrorBoundaryHandler extends React.Component<
ErrorBoundaryProps,
{ error: Error | null }
> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { error: null }
}

static getDerivedStateFromError(error: Error) {
return { error }
}

reset = () => {
this.setState({ error: null })
}

render() {
if (this.state.error) {
return (
<this.props.errorComponent
error={this.state.error}
reset={this.reset}
/>
)
}

return this.props.children
}
}

/**
* Renders error boundary with the provided "errorComponent" property as the fallback.
* If no "errorComponent" property is provided it renders the children without an error boundary.
*/
export function ErrorBoundary({
errorComponent,
children,
}: ErrorBoundaryProps & { children: React.ReactNode }): JSX.Element {
if (errorComponent) {
return (
<ErrorBoundaryHandler errorComponent={errorComponent}>
{children}
</ErrorBoundaryHandler>
)
}

return <>{children}</>
}

const styles: { [k: string]: React.CSSProperties } = {
error: {
fontFamily:
'-apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", "Fira Sans", Avenir, "Helvetica Neue", "Lucida Grande", sans-serif',
height: '100vh',
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
},

desc: {
display: 'inline-block',
textAlign: 'left',
lineHeight: '49px',
height: '49px',
verticalAlign: 'middle',
},
h2: {
fontSize: '14px',
fontWeight: 'normal',
lineHeight: '49px',
margin: 0,
padding: 0,
},
}

export function GlobalErrorComponent() {
return (
<html>
<body>
<div style={styles.error}>
<div style={styles.desc}>
<h2 style={styles.h2}>
Application error: a client-side exception has occurred (see the
browser console for more information).
</h2>
</div>
</div>
</body>
</html>
)
}
62 changes: 2 additions & 60 deletions packages/next/client/components/layout-router.client.tsx
Expand Up @@ -20,6 +20,7 @@ import type {
FlightSegmentPath,
// FlightDataPath,
} from '../../server/app-render'
import type { ErrorComponent } from './error-boundary'
import {
LayoutRouterContext,
GlobalLayoutRouterContext,
Expand All @@ -28,7 +29,7 @@ import {
} from '../../shared/lib/app-router-context'
import { fetchServerResponse } from './app-router.client'
import { createInfinitePromise } from './infinite-promise'

import { ErrorBoundary } from './error-boundary'
import { matchSegment } from './match-segments'

/**
Expand Down Expand Up @@ -364,65 +365,6 @@ function NotFoundBoundary({ notFound, children }: NotFoundBoundaryProps) {
)
}

type ErrorComponent = React.ComponentType<{ error: Error; reset: () => void }>
interface ErrorBoundaryProps {
errorComponent: ErrorComponent
}

/**
* Handles errors through `getDerivedStateFromError`.
* Renders the provided error component and provides a way to `reset` the error boundary state.
*/
class ErrorBoundaryHandler extends React.Component<
ErrorBoundaryProps,
{ error: Error | null }
> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { error: null }
}

static getDerivedStateFromError(error: Error) {
return { error }
}

reset = () => {
this.setState({ error: null })
}

render() {
if (this.state.error) {
return (
<this.props.errorComponent
error={this.state.error}
reset={this.reset}
/>
)
}

return this.props.children
}
}

/**
* Renders error boundary with the provided "errorComponent" property as the fallback.
* If no "errorComponent" property is provided it renders the children without an error boundary.
*/
function ErrorBoundary({
errorComponent,
children,
}: ErrorBoundaryProps & { children: React.ReactNode }): JSX.Element {
if (errorComponent) {
return (
<ErrorBoundaryHandler errorComponent={errorComponent}>
{children}
</ErrorBoundaryHandler>
)
}

return <>{children}</>
}

/**
* OuterLayoutRouter handles the current segment as well as <Offscreen> rendering of other segments.
* It can be rendered next to each other with a different `parallelRouterKey`, allowing for Parallel routes.
Expand Down
20 changes: 20 additions & 0 deletions test/e2e/app-dir/app/app/error/global-error-boundary/page.js
@@ -0,0 +1,20 @@
'client'

import { useState } from 'react'

export default function Page() {
const [clicked, setClicked] = useState(false)
if (clicked) {
throw new Error('this is a test')
}
return (
<button
id="error-trigger-button"
onClick={() => {
setClicked(true)
}}
>
Trigger Error!
</button>
)
}

0 comments on commit a78163d

Please sign in to comment.