Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add handling for 404 in new router #40787

Merged
merged 2 commits into from Sep 22, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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