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

Reset not-found and error boundary when navigating #49855

Merged
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
42 changes: 38 additions & 4 deletions packages/next/src/client/components/error-boundary.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import React from 'react'
import { usePathname } from './navigation'

const styles = {
error: {
Expand Down Expand Up @@ -36,19 +37,50 @@ export interface ErrorBoundaryProps {
errorStyles?: React.ReactNode | undefined
}

interface ErrorBoundaryHandlerProps extends ErrorBoundaryProps {
pathname: string
}

interface ErrorBoundaryHandlerState {
error: Error | null
previousPathname: string
}

export class ErrorBoundaryHandler extends React.Component<
ErrorBoundaryProps,
{ error: Error | null }
ErrorBoundaryHandlerProps,
ErrorBoundaryHandlerState
> {
constructor(props: ErrorBoundaryProps) {
constructor(props: ErrorBoundaryHandlerProps) {
super(props)
this.state = { error: null }
this.state = { error: null, previousPathname: this.props.pathname }
}

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

static getDerivedStateFromProps(
props: ErrorBoundaryHandlerProps,
state: ErrorBoundaryHandlerState
): ErrorBoundaryHandlerState | null {
/**
* Handles reset of the error boundary when a navigation happens.
* Ensures the error boundary does not stay enabled when navigating to a new page.
* Approach of setState in render is safe as it checks the previous pathname and then overrides
* it as outlined in https://react.dev/reference/react/useState#storing-information-from-previous-renders
*/
if (props.pathname !== state.previousPathname && state.error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, didn't notice this before: you need to set previousPathname even if state.error is not set. So you can just delete that part of the condition.

Otherwise previousPathname will always be the initial value, until there's an error.

return {
error: null,
previousPathname: props.pathname,
}
}
return {
error: state.error,
previousPathname: props.pathname,
}
}

reset = () => {
this.setState({ error: null })
}
Expand Down Expand Up @@ -105,9 +137,11 @@ export function ErrorBoundary({
errorStyles,
children,
}: ErrorBoundaryProps & { children: React.ReactNode }): JSX.Element {
const pathname = usePathname()
if (errorComponent) {
return (
<ErrorBoundaryHandler
pathname={pathname}
errorComponent={errorComponent}
errorStyles={errorStyles}
>
Expand Down
45 changes: 41 additions & 4 deletions packages/next/src/client/components/not-found-boundary.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react'
import { usePathname } from './navigation'

interface NotFoundBoundaryProps {
notFound?: React.ReactNode
Expand All @@ -7,13 +8,25 @@ interface NotFoundBoundaryProps {
children: React.ReactNode
}

interface NotFoundErrorBoundaryProps extends NotFoundBoundaryProps {
pathname: string
}

interface NotFoundErrorBoundaryState {
notFoundTriggered: boolean
previousPathname: string
}

class NotFoundErrorBoundary extends React.Component<
NotFoundBoundaryProps,
{ notFoundTriggered: boolean }
NotFoundErrorBoundaryProps,
NotFoundErrorBoundaryState
> {
constructor(props: NotFoundBoundaryProps) {
constructor(props: NotFoundErrorBoundaryProps) {
super(props)
this.state = { notFoundTriggered: !!props.asNotFound }
this.state = {
notFoundTriggered: !!props.asNotFound,
previousPathname: props.pathname,
}
}

static getDerivedStateFromError(error: any) {
Expand All @@ -24,6 +37,28 @@ class NotFoundErrorBoundary extends React.Component<
throw error
}

static getDerivedStateFromProps(
props: NotFoundErrorBoundaryProps,
state: NotFoundErrorBoundaryState
): NotFoundErrorBoundaryState | null {
/**
* Handles reset of the error boundary when a navigation happens.
* Ensures the error boundary does not stay enabled when navigating to a new page.
* Approach of setState in render is safe as it checks the previous pathname and then overrides
* it as outlined in https://react.dev/reference/react/useState#storing-information-from-previous-renders
*/
if (props.pathname !== state.previousPathname && state.notFoundTriggered) {
return {
notFoundTriggered: false,
previousPathname: props.pathname,
}
}
return {
notFoundTriggered: state.notFoundTriggered,
previousPathname: props.pathname,
}
}

render() {
if (this.state.notFoundTriggered) {
return (
Expand All @@ -45,8 +80,10 @@ export function NotFoundBoundary({
asNotFound,
children,
}: NotFoundBoundaryProps) {
const pathname = usePathname()
return notFound ? (
<NotFoundErrorBoundary
pathname={pathname}
notFound={notFound}
notFoundStyles={notFoundStyles}
asNotFound={asNotFound}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use client'
import Link from 'next/link'

export default function ErrorComponent() {
return (
<>
<h1 id="error-component">Error Happened!</h1>
<Link href="/result" id="to-result">
To Result
</Link>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Link from 'next/link'

export default function NotFound() {
return (
<>
<h1 id="not-found-component">Not Found!</h1>
<Link href="/result" id="to-result">
To Result
</Link>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <h1 id="homepage">Home</h1>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <h1 id="result-page">Result Page!</h1>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { notFound } from 'next/navigation'

export const dynamic = 'force-dynamic'

export default function Page() {
notFound()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const dynamic = 'force-dynamic'

export default function Page() {
throw new Error('This is an error')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { createNextDescribe } from 'e2e-utils'

createNextDescribe(
'not-found-linking',
{
files: __dirname,
},
({ next }) => {
it('should allow navigation on not-found', async () => {
const browser = await next.browser('/trigger-404')
expect(await browser.elementByCss('#not-found-component').text()).toBe(
'Not Found!'
)

expect(
await browser
.elementByCss('#to-result')
.click()
.waitForElementByCss('#result-page')
.text()
).toBe('Result Page!')
})

it('should allow navigation on error', async () => {
const browser = await next.browser('/trigger-error')
expect(await browser.elementByCss('#error-component').text()).toBe(
'Error Happened!'
)

expect(
await browser
.elementByCss('#to-result')
.click()
.waitForElementByCss('#result-page')
.text()
).toBe('Result Page!')
})
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
typescript: {
ignoreBuildErrors: true,
},
}

module.exports = nextConfig
12 changes: 9 additions & 3 deletions test/e2e/app-dir/navigation/app/not-found/not-found.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import Link from 'next/link'
import styles from './style.module.css'

export default function NotFound() {
return (
<h1 id="not-found-component" className={styles.red}>
Not Found!
</h1>
<>
<h1 id="not-found-component" className={styles.red}>
Not Found!
</h1>
<Link href="/not-found/result" id="to-result">
To Result
</Link>
</>
)
}
3 changes: 3 additions & 0 deletions test/e2e/app-dir/navigation/app/not-found/result/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <h1 id="result-page">Result Page!</h1>
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// TODO-APP: enable when flight error serialization is implemented
import { notFound } from 'next/navigation'

export default function Page() {
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/app-dir/navigation/app/not-found/with-link/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { notFound } from 'next/navigation'

export const dynamic = 'force-dynamic'

export default function Page() {
notFound()
return <></>
}