diff --git a/packages/react-router/src/CatchBoundary.tsx b/packages/react-router/src/CatchBoundary.tsx index 68197b2eb6..b7ccb77a62 100644 --- a/packages/react-router/src/CatchBoundary.tsx +++ b/packages/react-router/src/CatchBoundary.tsx @@ -1,4 +1,6 @@ import * as React from 'react' +import { useRouter } from './useRouter' +import { isRedirect } from './redirects' import type { ErrorRouteComponent } from './route' import type { ErrorInfo } from 'react' @@ -9,6 +11,7 @@ export function CatchBoundary(props: { onCatch?: (error: Error, errorInfo: ErrorInfo) => void }) { const errorComponent = props.errorComponent ?? ErrorComponent + const router = useRouter() return ( { if (error) { + if (isRedirect(error)) { + const redirect = router.resolveRedirect({ + ...error, + _fromLocation: router.state.location, + }) + router.navigate(redirect) + return null + } return React.createElement(errorComponent, { error, reset, diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts index 6873cf0d98..dd3bab94ce 100644 --- a/packages/react-router/src/router.ts +++ b/packages/react-router/src/router.ts @@ -2696,7 +2696,11 @@ export class Router< })) } catch (err) { if (isResolvedRedirect(err)) { - await this.navigate(err) + await this.navigate({ + ...err, + replace: true, + ignoreBlocker: true, + }) } } })() diff --git a/packages/react-router/tests/redirect.test.tsx b/packages/react-router/tests/redirect.test.tsx index 679158c2c0..91067f2358 100644 --- a/packages/react-router/tests/redirect.test.tsx +++ b/packages/react-router/tests/redirect.test.tsx @@ -252,6 +252,84 @@ describe('redirect', () => { expect(await screen.findByText('Final')).toBeInTheDocument() expect(window.location.pathname).toBe('/final') }) + + test('when `redirect` is thrown in route component', async () => { + const nestedLoaderMock = vi.fn() + const nestedFooLoaderMock = vi.fn() + + const rootRoute = createRootRoute({}) + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () => { + return ( +
+

Index page

+ link to about +
+ ) + }, + }) + const aboutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/about', + component: () => { + throw redirect({ + to: '/nested/foo', + hash: 'some-hash', + search: { someSearch: 'hello123' }, + }) + }, + }) + const nestedRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/nested', + loader: async () => { + await sleep(WAIT_TIME) + nestedLoaderMock('nested') + }, + }) + const fooRoute = createRoute({ + validateSearch: (search) => { + return { + someSearch: search.someSearch as string, + } + }, + getParentRoute: () => nestedRoute, + path: '/foo', + loader: async () => { + await sleep(WAIT_TIME) + nestedFooLoaderMock('foo') + }, + component: () =>
Nested Foo page
, + }) + const routeTree = rootRoute.addChildren([ + nestedRoute.addChildren([fooRoute]), + aboutRoute, + indexRoute, + ]) + const router = createRouter({ routeTree }) + + render() + + const linkToAbout = await screen.findByText('link to about') + + expect(linkToAbout).toBeInTheDocument() + + fireEvent.click(linkToAbout) + + const fooElement = await screen.findByText('Nested Foo page') + + expect(fooElement).toBeInTheDocument() + + expect(router.state.location.href).toBe( + '/nested/foo?someSearch=hello123#some-hash', + ) + expect(window.location.pathname).toBe('/nested/foo') + + expect(nestedLoaderMock).toHaveBeenCalled() + expect(nestedFooLoaderMock).toHaveBeenCalled() + }) }) describe('SSR', () => {