diff --git a/packages/next/client/components/layout-router.client.tsx b/packages/next/client/components/layout-router.client.tsx index fdc9bab72821d..9ebd2a3cb6f57 100644 --- a/packages/next/client/components/layout-router.client.tsx +++ b/packages/next/client/components/layout-router.client.tsx @@ -11,7 +11,10 @@ import type { ChildProp, //Segment } from '../../server/app-render' -import type { ChildSegmentMap } from '../../shared/lib/app-router-context' +import type { + AppRouterInstance, + ChildSegmentMap, +} from '../../shared/lib/app-router-context' import type { FlightRouterState, FlightSegmentPath, @@ -21,6 +24,7 @@ import { LayoutRouterContext, GlobalLayoutRouterContext, TemplateContext, + AppRouterContext, } from '../../shared/lib/app-router-context' import { fetchServerResponse } from './app-router.client' import { createInfinitePromise } from './infinite-promise' @@ -285,6 +289,56 @@ function LoadingBoundary({ return <>{children} } +interface RedirectBoundaryProps { + router: AppRouterInstance + children: React.ReactNode +} + +function InfinitePromiseComponent() { + use(createInfinitePromise()) + return <> +} + +class RedirectErrorBoundary extends React.Component< + RedirectBoundaryProps, + { redirect: string | null } +> { + constructor(props: RedirectBoundaryProps) { + super(props) + this.state = { redirect: null } + } + + static getDerivedStateFromError(error: any) { + if (error.code === 'NEXT_REDIRECT') { + return { redirect: error.url } + } + // Re-throw if error is not for 404 + throw error + } + + render() { + const redirect = this.state.redirect + if (redirect !== null) { + setTimeout(() => { + // @ts-ignore startTransition exists + React.startTransition(() => { + this.props.router.replace(redirect, {}) + }) + }) + return + } + + return this.props.children + } +} + +function RedirectBoundary({ children }: { children: React.ReactNode }) { + const router = useContext(AppRouterContext) + return ( + {children} + ) +} + interface NotFoundBoundaryProps { notFound?: React.ReactNode children: React.ReactNode @@ -457,19 +511,23 @@ export default function OuterLayoutRouter({ - + + + diff --git a/packages/next/client/components/redirect.ts b/packages/next/client/components/redirect.ts index 05b30d5a29f6b..af126a333a93f 100644 --- a/packages/next/client/components/redirect.ts +++ b/packages/next/client/components/redirect.ts @@ -1,21 +1,6 @@ -import React, { experimental_use as use } from 'react' -import { AppRouterContext } from '../../shared/lib/app-router-context' -import { createInfinitePromise } from './infinite-promise' - export const REDIRECT_ERROR_CODE = 'NEXT_REDIRECT' export function redirect(url: string) { - if (process.browser) { - const router = use(AppRouterContext) - setTimeout(() => { - // @ts-ignore startTransition exists - React.startTransition(() => { - router.replace(url, {}) - }) - }) - // setTimeout is used to start a new transition during render, this is an intentional hack around React. - use(createInfinitePromise()) - } // eslint-disable-next-line no-throw-literal throw { url, diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index 05accb59abb65..36cda134001a0 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -1513,7 +1513,7 @@ describe('app dir', () => { ) }) - it('should redirect in a client component', async () => { + it.skip('should redirect in a client component', async () => { const browser = await webdriver(next.url, '/redirect/clientcomponent') await browser.waitForElementByCss('#result-page') expect(await browser.elementByCss('#result-page').text()).toBe(