Skip to content

Commit

Permalink
Merge branch 'canary' into enable-test
Browse files Browse the repository at this point in the history
  • Loading branch information
kwonoj committed May 18, 2023
2 parents d86fb32 + 0a81cc0 commit 3b9e3a3
Show file tree
Hide file tree
Showing 16 changed files with 198 additions and 18 deletions.
42 changes: 38 additions & 4 deletions packages/next/src/client/components/error-boundary.tsx
@@ -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) {
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
@@ -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
6 changes: 0 additions & 6 deletions packages/next/src/export/index.ts
Expand Up @@ -228,12 +228,6 @@ export default async function exportApp(
)
}

if (nextConfig.experimental.serverActions) {
throw new ExportError(
`Server Actions are not supported with static export.`
)
}

const customRoutesDetected = ['rewrites', 'redirects', 'headers'].filter(
(config) => typeof nextConfig[config] === 'function'
)
Expand Down
@@ -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>
</>
)
}
@@ -0,0 +1,7 @@
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
@@ -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>
</>
)
}
@@ -0,0 +1,3 @@
export default function Page() {
return <h1 id="homepage">Home</h1>
}
@@ -0,0 +1,3 @@
export default function Page() {
return <h1 id="result-page">Result Page!</h1>
}
@@ -0,0 +1,7 @@
import { notFound } from 'next/navigation'

export const dynamic = 'force-dynamic'

export default function Page() {
notFound()
}
@@ -0,0 +1,5 @@
export const dynamic = 'force-dynamic'

export default function Page() {
throw new Error('This is an error')
}
@@ -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!')
})
}
)
@@ -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
@@ -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
@@ -0,0 +1,3 @@
export default function Page() {
return <h1 id="result-page">Result Page!</h1>
}
@@ -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
@@ -0,0 +1,8 @@
import { notFound } from 'next/navigation'

export const dynamic = 'force-dynamic'

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

0 comments on commit 3b9e3a3

Please sign in to comment.