Repository: TanStack/router (ts)

Issue:
Redirects in loaders do not redirect correctly after invalidating the router
### Which project does this relate to?

Router

### Describe the bug

After calling `router.invalidate()`, any `throw redirect()`'s in `loader` functions will not redirect correctly.

### Your Example Website or App

https://stackblitz.com/edit/tanstack-router-tpf14b?file=app%2Froutes%2Fposts.tsx

### Steps to Reproduce the Bug or Issue

1. Go to the "Posts" route.
2. Click on the blue "Invalidate Router" button.
3. The `loader()` function will run again, however the user will not be redirected. User gets stuck on the "Posts" route.
4. Check the console for logging statements that verify this.

### Expected behavior

The user should be redirected back to the root `/` route after clicking the invalidate router button.

### Screenshots or Videos

_No response_

### Platform

- OS: macOS
- Browser: Chrome
- Version: 129


### Additional context

_No response_

Pull request description:
fix(react-router): handle redirects in loaders after invalidate
fixes #2504

Patch:
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index e06457fb7..7d82ca071 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -2481,7 +2481,11 @@ export class Router<
                       ;(async () => {
                         try {
                           await runLoader()
-                        } catch (err) {}
+                        } catch (err) {
+                          if (isResolvedRedirect(err)) {
+                            await this.navigate(err)
+                          }
+                        }
                       })()
                     } else if (status !== 'success') {
                       await runLoader()


Test patch:
diff --git a/packages/react-router/tests/redirect.test.tsx b/packages/react-router/tests/redirect.test.tsx
index fccd856d5..895bb33bc 100644
--- a/packages/react-router/tests/redirect.test.tsx
+++ b/packages/react-router/tests/redirect.test.tsx
@@ -16,6 +16,7 @@ import {
   createRoute,
   createRouter,
   redirect,
+  useRouter,
 } from '../src'
 
 import { sleep } from './utils'
@@ -178,6 +179,79 @@ describe('redirect', () => {
       expect(nestedLoaderMock).toHaveBeenCalled()
       expect(nestedFooLoaderMock).toHaveBeenCalled()
     })
+
+    test('when `redirect` is thrown in `loader` after `router.invalidate()`', async () => {
+      let shouldRedirect = false
+
+      const rootRoute = createRootRoute({})
+      const indexRoute = createRoute({
+        getParentRoute: () => rootRoute,
+        path: '/',
+        component: () => {
+          return (
+            <div>
+              <h1>Index page</h1>
+              <Link data-testid="link-to-about" to="/about">
+                link to about
+              </Link>
+            </div>
+          )
+        },
+      })
+      const aboutRoute = createRoute({
+        getParentRoute: () => rootRoute,
+        path: '/about',
+        loader: async () => {
+          await sleep(WAIT_TIME)
+          if (shouldRedirect) {
+            throw redirect({
+              to: '/final',
+            })
+          }
+        },
+        component: () => {
+          const router = useRouter()
+          return (
+            <button
+              data-testid="button-invalidate"
+              onClick={() => {
+                shouldRedirect = true
+                router.invalidate()
+              }}
+            >
+              invalidate
+            </button>
+          )
+        },
+      })
+      const finalRoute = createRoute({
+        getParentRoute: () => rootRoute,
+        path: '/final',
+        component: () => <div>Final</div>,
+      })
+
+      const routeTree = rootRoute.addChildren([
+        aboutRoute,
+        indexRoute,
+        finalRoute,
+      ])
+      const router = createRouter({ routeTree })
+
+      render(<RouterProvider router={router} />)
+
+      const linkToAbout = await screen.findByTestId('link-to-about')
+      expect(linkToAbout).toBeInTheDocument()
+
+      fireEvent.click(linkToAbout)
+
+      const invalidateButton = await screen.findByTestId('button-invalidate')
+      expect(invalidateButton).toBeInTheDocument()
+
+      fireEvent.click(invalidateButton)
+
+      expect(await screen.findByText('Final')).toBeInTheDocument()
+      expect(window.location.pathname).toBe('/final')
+    })
   })
 
   describe('SSR', () => {