Skip to content

Commit

Permalink
Add handling for 404 in new router (#40787)
Browse files Browse the repository at this point in the history
  • Loading branch information
timneutkens committed Sep 22, 2022
1 parent cc1e35d commit c4647bb
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 18 deletions.
1 change: 1 addition & 0 deletions packages/next/build/webpack/loaders/next-app-loader.ts
Expand Up @@ -8,6 +8,7 @@ export const FILE_TYPES = {
template: 'template',
error: 'error',
loading: 'loading',
'404': '404',
} as const

// TODO-APP: check if this can be narrowed.
Expand Down
75 changes: 60 additions & 15 deletions packages/next/client/components/layout-router.client.tsx
Expand Up @@ -285,6 +285,47 @@ function LoadingBoundary({
return <>{children}</>
}

interface NotFoundBoundaryProps {
notFound?: React.ReactNode
children: React.ReactNode
}

class NotFoundErrorBoundary extends React.Component<
NotFoundBoundaryProps,
{ notFoundTriggered: boolean }
> {
constructor(props: NotFoundBoundaryProps) {
super(props)
this.state = { notFoundTriggered: false }
}

static getDerivedStateFromError(error: any) {
if (error.code === 'NEXT_NOT_FOUND') {
return { notFoundTriggered: true }
}
// Re-throw if error is not for 404
throw error
}

render() {
if (this.state.notFoundTriggered) {
return this.props.notFound
}

return this.props.children
}
}

function NotFoundBoundary({ notFound, children }: NotFoundBoundaryProps) {
return notFound ? (
<NotFoundErrorBoundary notFound={notFound}>
{children}
</NotFoundErrorBoundary>
) : (
<>{children}</>
)
}

type ErrorComponent = React.ComponentType<{ error: Error; reset: () => void }>
interface ErrorBoundaryProps {
errorComponent: ErrorComponent
Expand Down Expand Up @@ -355,6 +396,7 @@ export default function OuterLayoutRouter({
error,
loading,
template,
notFound,
rootLayoutIncluded,
}: {
parallelRouterKey: string
Expand All @@ -363,6 +405,7 @@ export default function OuterLayoutRouter({
error: ErrorComponent
template: React.ReactNode
loading: React.ReactNode | undefined
notFound: React.ReactNode | undefined
rootLayoutIncluded: boolean
}) {
const { childNodes, tree, url } = useContext(LayoutRouterContext)
Expand Down Expand Up @@ -412,21 +455,23 @@ export default function OuterLayoutRouter({
key={preservedSegment}
value={
<ErrorBoundary errorComponent={error}>
<LoadingBoundary loading={loading}>
<InnerLayoutRouter
parallelRouterKey={parallelRouterKey}
url={url}
tree={tree}
childNodes={childNodesForParallelRouter!}
childProp={
childPropSegment === preservedSegment ? childProp : null
}
segmentPath={segmentPath}
path={preservedSegment}
isActive={currentChildSegment === preservedSegment}
rootLayoutIncluded={rootLayoutIncluded}
/>
</LoadingBoundary>
<NotFoundBoundary notFound={notFound}>
<LoadingBoundary loading={loading}>
<InnerLayoutRouter
parallelRouterKey={parallelRouterKey}
url={url}
tree={tree}
childNodes={childNodesForParallelRouter!}
childProp={
childPropSegment === preservedSegment ? childProp : null
}
segmentPath={segmentPath}
path={preservedSegment}
isActive={currentChildSegment === preservedSegment}
rootLayoutIncluded={rootLayoutIncluded}
/>
</LoadingBoundary>
</NotFoundBoundary>
</ErrorBoundary>
}
>
Expand Down
8 changes: 8 additions & 0 deletions packages/next/client/components/not-found.ts
@@ -0,0 +1,8 @@
export const NOT_FOUND_ERROR_CODE = 'NEXT_NOT_FOUND'

export function notFound() {
// eslint-disable-next-line no-throw-literal
throw {
code: NOT_FOUND_ERROR_CODE,
}
}
13 changes: 12 additions & 1 deletion packages/next/server/app-render.tsx
Expand Up @@ -727,7 +727,15 @@ export async function renderToHTMLOrFlight(
loaderTree: [
segment,
parallelRoutes,
{ layoutOrPagePath, layout, template, error, loading, page },
{
layoutOrPagePath,
layout,
template,
error,
loading,
page,
'404': notFound,
},
],
parentParams,
firstItem,
Expand All @@ -750,6 +758,7 @@ export async function renderToHTMLOrFlight(
const Template = template
? await interopDefault(template())
: React.Fragment
const NotFound = notFound ? await interopDefault(notFound()) : undefined
const ErrorComponent = error ? await interopDefault(error()) : undefined
const Loading = loading ? await interopDefault(loading()) : undefined
const isLayout = typeof layout !== 'undefined'
Expand Down Expand Up @@ -844,6 +853,7 @@ export async function renderToHTMLOrFlight(
<RenderFromTemplateContext />
</Template>
}
notFound={NotFound ? <NotFound /> : undefined}
childProp={childProp}
rootLayoutIncluded={rootLayoutIncludedAtThisLevelOrAbove}
/>,
Expand Down Expand Up @@ -882,6 +892,7 @@ export async function renderToHTMLOrFlight(
<RenderFromTemplateContext />
</Template>
}
notFound={NotFound ? <NotFound /> : undefined}
childProp={childProp}
rootLayoutIncluded={rootLayoutIncludedAtThisLevelOrAbove}
/>,
Expand Down
@@ -0,0 +1,5 @@
'client'

export default function Page() {
throw new Error('Error during SSR')
}
@@ -1,5 +1,8 @@
'client'
import ClientComp from './client-component'
import { useHeaders } from 'next/dist/client/components/hooks-server'

export default function Page() {
throw new Error('Error during SSR')
// Opt-in to SSR.
useHeaders()
return <ClientComp />
}
3 changes: 3 additions & 0 deletions test/e2e/app-dir/app/app/not-found/404.js
@@ -0,0 +1,3 @@
export default function NotFound() {
return <h1 id="not-found-component">404!</h1>
}
17 changes: 17 additions & 0 deletions test/e2e/app-dir/app/app/not-found/client-side/page.js
@@ -0,0 +1,17 @@
'client'

import { notFound } from 'next/dist/client/components/not-found'
import React from 'react'

export default function Page() {
const [notFoundEnabled, enableNotFound] = React.useState(false)

if (notFoundEnabled) {
notFound()
}
return (
<button onClick={() => React.startTransition(() => enableNotFound(true))}>
Not Found!
</button>
)
}
9 changes: 9 additions & 0 deletions test/e2e/app-dir/app/app/not-found/clientcomponent/a.js
@@ -0,0 +1,9 @@
// TODO-APP: enable when flight error serialization is implemented
import ClientComp from './client-component'
import { useHeaders } from 'next/dist/client/components/hooks-server'

export default function Page() {
// Opt-in to SSR.
useHeaders()
return <ClientComp />
}
@@ -0,0 +1,7 @@
'client'
import { notFound } from 'next/dist/client/components/not-found'

export default function ClientComp() {
notFound()
return <></>
}
7 changes: 7 additions & 0 deletions test/e2e/app-dir/app/app/not-found/servercomponent/a.js
@@ -0,0 +1,7 @@
// TODO-APP: enable when flight error serialization is implemented
import { notFound } from 'next/dist/client/components/not-found'

export default function Page() {
notFound()
return <></>
}
28 changes: 28 additions & 0 deletions test/e2e/app-dir/index.test.ts
Expand Up @@ -1475,6 +1475,34 @@ describe('app dir', () => {
})
})

describe('404', () => {
it.skip('should trigger 404 in a server component', async () => {
const browser = await webdriver(next.url, '/not-found/servercomponent')

expect(
await browser.waitForElementByCss('#not-found-component').text()
).toBe('404!')
})

it.skip('should trigger 404 in a client component', async () => {
const browser = await webdriver(next.url, '/not-found/clientcomponent')
expect(
await browser.waitForElementByCss('#not-found-component').text()
).toBe('404!')
})

it('should trigger 404 client-side', async () => {
const browser = await webdriver(next.url, '/not-found/client-side')
await browser
.elementByCss('button')
.click()
.waitForElementByCss('#not-found-component')
expect(await browser.elementByCss('#not-found-component').text()).toBe(
'404!'
)
})
})

describe('redirect', () => {
describe('components', () => {
it.skip('should redirect in a server component', async () => {
Expand Down

0 comments on commit c4647bb

Please sign in to comment.