diff --git a/.changeset/bright-cars-lie.md b/.changeset/bright-cars-lie.md
new file mode 100644
index 0000000000..689d193355
--- /dev/null
+++ b/.changeset/bright-cars-lie.md
@@ -0,0 +1,7 @@
+---
+'@tanstack/react-router': patch
+---
+
+Fix a client-side crash when a root `beforeLoad` redirect races with pending UI and a lazy target route while `defaultViewTransition` is enabled.
+
+React now handles stale redirected matches more safely during the transition, and a dedicated `e2e/react-router/issue-7120` fixture covers this regression.
diff --git a/e2e/react-router/issue-7120/index.html b/e2e/react-router/issue-7120/index.html
new file mode 100644
index 0000000000..de92cad2a3
--- /dev/null
+++ b/e2e/react-router/issue-7120/index.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Issue 7120</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.tsx"></script>
+  </body>
+</html>
diff --git a/e2e/react-router/issue-7120/package.json b/e2e/react-router/issue-7120/package.json
new file mode 100644
index 0000000000..6fdada5615
--- /dev/null
+++ b/e2e/react-router/issue-7120/package.json
@@ -0,0 +1,29 @@
+{
+  "name": "tanstack-router-e2e-react-issue-7120",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite --port 3000",
+    "dev:e2e": "vite",
+    "build": "vite build && tsc --noEmit",
+    "preview": "vite preview",
+    "start": "vite",
+    "test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
+  },
+  "dependencies": {
+    "@tailwindcss/vite": "^4.2.2",
+    "@tanstack/react-router": "workspace:^",
+    "react": "^19.0.0",
+    "react-dom": "^19.0.0",
+    "redaxios": "^0.5.1",
+    "tailwindcss": "^4.2.2"
+  },
+  "devDependencies": {
+    "@playwright/test": "^1.50.1",
+    "@tanstack/router-e2e-utils": "workspace:^",
+    "@types/react": "^19.0.8",
+    "@types/react-dom": "^19.0.3",
+    "@vitejs/plugin-react": "^6.0.1",
+    "vite": "^8.0.0"
+  }
+}
diff --git a/e2e/react-router/issue-7120/playwright.config.ts b/e2e/react-router/issue-7120/playwright.config.ts
new file mode 100644
index 0000000000..b8f97fc2ce
--- /dev/null
+++ b/e2e/react-router/issue-7120/playwright.config.ts
@@ -0,0 +1,33 @@
+import { defineConfig, devices } from '@playwright/test'
+import {
+  getDummyServerPort,
+  getTestServerPort,
+} from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = await getTestServerPort(packageJson.name)
+const EXTERNAL_PORT = await getDummyServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+export default defineConfig({
+  testDir: './tests',
+  workers: 1,
+  reporter: [['line']],
+  globalSetup: './tests/setup/global.setup.ts',
+  globalTeardown: './tests/setup/global.teardown.ts',
+  use: {
+    baseURL,
+  },
+  webServer: {
+    command: `VITE_NODE_ENV="test" VITE_SERVER_PORT=${PORT} VITE_EXTERNAL_PORT=${EXTERNAL_PORT} pnpm build && pnpm preview --port ${PORT}`,
+    url: baseURL,
+    reuseExistingServer: !process.env.CI,
+    stdout: 'pipe',
+  },
+  projects: [
+    {
+      name: 'chromium',
+      use: { ...devices['Desktop Chrome'] },
+    },
+  ],
+})
diff --git a/e2e/react-router/issue-7120/src/main.tsx b/e2e/react-router/issue-7120/src/main.tsx
new file mode 100644
index 0000000000..46823c831f
--- /dev/null
+++ b/e2e/react-router/issue-7120/src/main.tsx
@@ -0,0 +1,64 @@
+import ReactDOM from 'react-dom/client'
+import {
+  Outlet,
+  RouterProvider,
+  createRootRoute,
+  createRoute,
+  createRouter,
+  redirect,
+} from '@tanstack/react-router'
+import { fetchPosts } from './posts'
+import './styles.css'
+
+const rootRoute = createRootRoute({
+  component: RootComponent,
+  pendingMs: 0,
+  pendingComponent: () => <div data-testid="root-pending">loading</div>,
+  beforeLoad: async ({ matches }) => {
+    if (matches.find((match) => match.routeId === '/posts')) {
+      return
+    }
+
+    await new Promise((resolve) => setTimeout(resolve, 1000))
+    throw redirect({ to: '/posts' })
+  },
+})
+
+function RootComponent() {
+  return <Outlet />
+}
+
+const indexRoute = createRoute({
+  getParentRoute: () => rootRoute,
+  path: '/',
+  component: () => <div>Home</div>,
+})
+
+const postsRoute = createRoute({
+  getParentRoute: () => rootRoute,
+  path: 'posts',
+  loader: async () => {
+    await new Promise((resolve) => setTimeout(resolve, 10))
+    return fetchPosts()
+  },
+}).lazy(() => import('./posts.lazy').then((d) => d.Route))
+
+const routeTree = rootRoute.addChildren([indexRoute, postsRoute])
+
+const router = createRouter({
+  routeTree,
+  defaultViewTransition: true,
+})
+
+declare module '@tanstack/react-router' {
+  interface Register {
+    router: typeof router
+  }
+}
+
+const rootElement = document.getElementById('app')!
+
+if (!rootElement.innerHTML) {
+  const root = ReactDOM.createRoot(rootElement)
+  root.render(<RouterProvider router={router} />)
+}
diff --git a/e2e/react-router/issue-7120/src/posts.lazy.tsx b/e2e/react-router/issue-7120/src/posts.lazy.tsx
new file mode 100644
index 0000000000..6f554b11d2
--- /dev/null
+++ b/e2e/react-router/issue-7120/src/posts.lazy.tsx
@@ -0,0 +1,28 @@
+import { Link, createLazyRoute } from '@tanstack/react-router'
+
+export const Route = createLazyRoute('/posts')({
+  component: PostsComponent,
+})
+
+function PostsComponent() {
+  const posts = Route.useLoaderData()
+
+  return (
+    <div className="p-2">
+      <ul className="list-disc pl-4">
+        {posts.map((post) => {
+          return (
+            <li key={post.id} className="whitespace-nowrap">
+              <Link
+                to="/posts"
+                className="block py-1 px-2 text-blue-600 hover:opacity-75"
+              >
+                <div>{post.title.substring(0, 20)}</div>
+              </Link>
+            </li>
+          )
+        })}
+      </ul>
+    </div>
+  )
+}
diff --git a/e2e/react-router/issue-7120/src/posts.ts b/e2e/react-router/issue-7120/src/posts.ts
new file mode 100644
index 0000000000..cf44f32db5
--- /dev/null
+++ b/e2e/react-router/issue-7120/src/posts.ts
@@ -0,0 +1,19 @@
+import axios from 'redaxios'
+
+type PostType = {
+  id: string
+  title: string
+  body: string
+}
+
+let queryURL = 'https://jsonplaceholder.typicode.com'
+
+if (import.meta.env.VITE_NODE_ENV === 'test') {
+  queryURL = `http://localhost:${import.meta.env.VITE_EXTERNAL_PORT}`
+}
+
+export const fetchPosts = async () => {
+  return axios
+    .get<Array<PostType>>(`${queryURL}/posts`)
+    .then((r) => r.data.slice(0, 10))
+}
diff --git a/e2e/react-router/issue-7120/src/styles.css b/e2e/react-router/issue-7120/src/styles.css
new file mode 100644
index 0000000000..5d14b07533
--- /dev/null
+++ b/e2e/react-router/issue-7120/src/styles.css
@@ -0,0 +1,23 @@
+@import 'tailwindcss' source('../');
+
+@layer base {
+  *,
+  ::after,
+  ::before,
+  ::backdrop,
+  ::file-selector-button {
+    border-color: var(--color-gray-200, currentcolor);
+  }
+}
+
+html {
+  color-scheme: light dark;
+}
+
+* {
+  @apply border-gray-200 dark:border-gray-800;
+}
+
+body {
+  @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/e2e/react-router/issue-7120/tests/issue-7120.repro.spec.ts b/e2e/react-router/issue-7120/tests/issue-7120.repro.spec.ts
new file mode 100644
index 0000000000..b53f0fe68f
--- /dev/null
+++ b/e2e/react-router/issue-7120/tests/issue-7120.repro.spec.ts
@@ -0,0 +1,18 @@
+import { expect, test } from '@playwright/test'
+
+test('root beforeLoad redirect does not blank when pending UI and view transitions are enabled', async ({
+  page,
+}) => {
+  const pageErrors: Array<string> = []
+
+  page.on('pageerror', (error) => {
+    pageErrors.push(error.message)
+  })
+
+  await page.goto('/')
+
+  await expect(page).toHaveURL(/\/posts$/)
+  await expect(page.getByText('sunt aut facere repe')).toBeVisible()
+  await expect(page.getByTestId('root-pending')).not.toBeVisible()
+  expect(pageErrors).toEqual([])
+})
diff --git a/e2e/react-router/issue-7120/tests/setup/global.setup.ts b/e2e/react-router/issue-7120/tests/setup/global.setup.ts
new file mode 100644
index 0000000000..3593d10ab9
--- /dev/null
+++ b/e2e/react-router/issue-7120/tests/setup/global.setup.ts
@@ -0,0 +1,6 @@
+import { e2eStartDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function setup() {
+  await e2eStartDummyServer(packageJson.name)
+}
diff --git a/e2e/react-router/issue-7120/tests/setup/global.teardown.ts b/e2e/react-router/issue-7120/tests/setup/global.teardown.ts
new file mode 100644
index 0000000000..62fd79911c
--- /dev/null
+++ b/e2e/react-router/issue-7120/tests/setup/global.teardown.ts
@@ -0,0 +1,6 @@
+import { e2eStopDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function teardown() {
+  await e2eStopDummyServer(packageJson.name)
+}
diff --git a/e2e/react-router/issue-7120/tsconfig.json b/e2e/react-router/issue-7120/tsconfig.json
new file mode 100644
index 0000000000..4f6089bc08
--- /dev/null
+++ b/e2e/react-router/issue-7120/tsconfig.json
@@ -0,0 +1,15 @@
+{
+  "compilerOptions": {
+    "strict": true,
+    "esModuleInterop": true,
+    "jsx": "react-jsx",
+    "target": "ESNext",
+    "moduleResolution": "Bundler",
+    "module": "ESNext",
+    "resolveJsonModule": true,
+    "allowJs": true,
+    "skipLibCheck": true,
+    "types": ["vite/client"]
+  },
+  "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/react-router/issue-7120/vite.config.js b/e2e/react-router/issue-7120/vite.config.js
new file mode 100644
index 0000000000..0616e59599
--- /dev/null
+++ b/e2e/react-router/issue-7120/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
+
+export default defineConfig({
+  plugins: [react(), tailwindcss()],
+})
diff --git a/packages/react-router/src/Match.tsx b/packages/react-router/src/Match.tsx
index fefd256bf6..ae38b6b4e0 100644
--- a/packages/react-router/src/Match.tsx
+++ b/packages/react-router/src/Match.tsx
@@ -8,6 +8,7 @@ import {
   invariant,
   isNotFound,
   isRedirect,
+  markMatchPendingVisible,
   rootRouteId,
 } from '@tanstack/router-core'
 import { isServer } from '@tanstack/router-core/isServer'
@@ -20,7 +21,11 @@ import { renderRouteNotFound } from './renderRouteNotFound'
 import { ScrollRestoration } from './scroll-restoration'
 import { ClientOnly } from './ClientOnly'
 import { useLayoutEffect } from './utils'
-import type { AnyRoute, RootRouteOptions } from '@tanstack/router-core'
+import type {
+  AnyRoute,
+  AnyRouteMatch,
+  RootRouteOptions,
+} from '@tanstack/router-core'
 
 export const Match = React.memo(function MatchImpl({
   matchId,
@@ -160,10 +165,18 @@ function MatchView({
   const ShellComponent = route.isRoot
     ? ((route.options as RootRouteOptions).shellComponent ?? SafeFragment)
     : SafeFragment
+
   return (
     <ShellComponent>
       <matchContext.Provider value={matchId}>
-        <ResolvedSuspenseBoundary fallback={pendingElement}>
+        <ResolvedSuspenseBoundary
+          fallback={
+            <PendingRouteMatch
+              matchId={matchId}
+              pendingElement={pendingElement}
+            />
+          }
+        >
           <ResolvedCatchBoundary
             getResetKey={() => resetKey}
             errorComponent={routeErrorComponent || ErrorComponent}
@@ -196,7 +209,14 @@ function MatchView({
               }}
             >
               {resolvedNoSsr || matchState._displayPending ? (
-                <ClientOnly fallback={pendingElement}>
+                <ClientOnly
+                  fallback={
+                    <PendingRouteMatch
+                      matchId={matchId}
+                      pendingElement={pendingElement}
+                    />
+                  }
+                >
                   <MatchInner matchId={matchId} />
                 </ClientOnly>
               ) : (
@@ -218,6 +238,25 @@ function MatchView({
   )
 }
 
+function PendingRouteMatch({
+  matchId,
+  pendingElement,
+}: {
+  matchId: string
+  pendingElement: React.ReactNode
+}) {
+  const router = useRouter()
+
+  useLayoutEffect(() => {
+    const match = router.getMatch(matchId)
+    if (match) {
+      markMatchPendingVisible(match)
+    }
+  }, [matchId, router])
+
+  return pendingElement
+}
+
 // On Rendered can't happen above the root layout because it needs to run after
 // the route subtree has committed below the root layout. Keeping it here lets
 // us fire onRendered even after a hydration mismatch above the root layout
@@ -261,23 +300,18 @@ export const MatchInner = React.memo(function MatchInnerImpl({
 }): any {
   const router = useRouter()
 
-  const getMatchPromise = (
-    match: {
-      id: string
-      _nonReactive: {
-        displayPendingPromise?: Promise<void>
-        minPendingPromise?: Promise<void>
-        loadPromise?: Promise<void>
-      }
-    },
-    key: 'displayPendingPromise' | 'minPendingPromise' | 'loadPromise',
-  ) => {
-    return (
-      router.getMatch(match.id)?._nonReactive[key] ?? match._nonReactive[key]
-    )
-  }
-
   if (isServer ?? router.isServer) {
+    const throwMatchPromise = (
+      match: AnyRouteMatch,
+      key: 'displayPendingPromise' | 'minPendingPromise' | 'loadPromise',
+    ) => {
+      throw (
+        router.getMatch(match.id)?._nonReactive[key] ??
+        match._nonReactive[key] ??
+        router.latestLoadPromise
+      )
+    }
+
     const match = router.stores.matchStores.get(matchId)?.get()
     if (!match) {
       if (process.env.NODE_ENV !== 'production') {
@@ -305,15 +339,15 @@ export const MatchInner = React.memo(function MatchInnerImpl({
     const out = Comp ? <Comp key={key} /> : <Outlet />
 
     if (match._displayPending) {
-      throw getMatchPromise(match, 'displayPendingPromise')
+      throwMatchPromise(match, 'displayPendingPromise')
     }
 
     if (match._forcePending) {
-      throw getMatchPromise(match, 'minPendingPromise')
+      throwMatchPromise(match, 'minPendingPromise')
     }
 
     if (match.status === 'pending') {
-      throw getMatchPromise(match, 'loadPromise')
+      throwMatchPromise(match, 'loadPromise')
     }
 
     if (match.status === 'notFound') {
@@ -335,7 +369,7 @@ export const MatchInner = React.memo(function MatchInnerImpl({
 
         invariant()
       }
-      throw getMatchPromise(match, 'loadPromise')
+      throwMatchPromise(match, 'loadPromise')
     }
 
     if (match.status === 'error') {
@@ -401,12 +435,39 @@ export const MatchInner = React.memo(function MatchInnerImpl({
     return <Outlet />
   }, [key, route.options.component, router.options.defaultComponent])
 
-  if (match._displayPending) {
-    throw getMatchPromise(match, 'displayPendingPromise')
+  const pendingKey = match._displayPending
+    ? 'displayPendingPromise'
+    : match._forcePending
+      ? 'minPendingPromise'
+      : undefined
+
+  const suspendOrKeepPending = (
+    key: 'displayPendingPromise' | 'minPendingPromise' | 'loadPromise',
+  ) => {
+    const routerMatch = router.getMatch(match.id)
+
+    const promise =
+      routerMatch?._nonReactive[key] ??
+      match._nonReactive[key] ??
+      router.latestLoadPromise
+
+    if (promise) {
+      throw promise
+    }
+
+    const retainedPendingPromise =
+      routerMatch?._nonReactive.retainedPendingPromise ??
+      match._nonReactive.retainedPendingPromise
+
+    if (retainedPendingPromise) {
+      throw retainedPendingPromise
+    }
+
+    return null
   }
 
-  if (match._forcePending) {
-    throw getMatchPromise(match, 'minPendingPromise')
+  if (pendingKey) {
+    return suspendOrKeepPending(pendingKey)
   }
 
   // see also hydrate() in packages/router-core/src/ssr/ssr-client.ts
@@ -431,7 +492,7 @@ export const MatchInner = React.memo(function MatchInnerImpl({
         }
       }
     }
-    throw getMatchPromise(match, 'loadPromise')
+    return suspendOrKeepPending('loadPromise')
   }
 
   if (match.status === 'notFound') {
@@ -458,7 +519,7 @@ export const MatchInner = React.memo(function MatchInnerImpl({
       invariant()
     }
 
-    throw getMatchPromise(match, 'loadPromise')
+    return suspendOrKeepPending('loadPromise')
   }
 
   if (match.status === 'error') {
diff --git a/packages/react-router/tests/redirect.test.tsx b/packages/react-router/tests/redirect.test.tsx
index cc15f0da36..cc47d563eb 100644
--- a/packages/react-router/tests/redirect.test.tsx
+++ b/packages/react-router/tests/redirect.test.tsx
@@ -146,10 +146,12 @@ describe('redirect', () => {
       const router = createRouter({
         routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
         history,
+        defaultViewTransition: true,
       })
 
       render(<RouterProvider router={router} />)
 
+      expect(await screen.findByTestId('pending')).toBeInTheDocument()
       // The lazy target route adds the async boundary that exposes the stale
       // redirected-match render path this regression is guarding.
       expect(await screen.findByTestId('lazy-route-page')).toBeInTheDocument()
diff --git a/packages/router-core/src/Matches.ts b/packages/router-core/src/Matches.ts
index 852d186b67..54cdc233b3 100644
--- a/packages/router-core/src/Matches.ts
+++ b/packages/router-core/src/Matches.ts
@@ -105,6 +105,12 @@ export const isMatch = <TMatch, TPath extends string>(
   return value != null
 }
 
+const retainedPendingPromise = new Promise<void>(() => {})
+
+export const markMatchPendingVisible = (match: AnyRouteMatch) => {
+  match._nonReactive.retainedPendingPromise ??= retainedPendingPromise
+}
+
 export interface DefaultRouteMatchExtensions {
   scripts?: unknown
   links?: unknown
@@ -147,6 +153,7 @@ export interface RouteMatch<
     loadPromise?: ControlledPromise<void>
     displayPendingPromise?: Promise<void>
     minPendingPromise?: ControlledPromise<void>
+    retainedPendingPromise?: Promise<void>
     dehydrated?: boolean
     /** @internal */
     error?: unknown
diff --git a/packages/router-core/src/index.ts b/packages/router-core/src/index.ts
index 9a1373d9e6..3ce04bd807 100644
--- a/packages/router-core/src/index.ts
+++ b/packages/router-core/src/index.ts
@@ -76,7 +76,7 @@ export type {
   ManifestAssetLink,
 } from './manifest'
 export { getAssetCrossOrigin, resolveManifestAssetLink } from './manifest'
-export { isMatch } from './Matches'
+export { isMatch, markMatchPendingVisible } from './Matches'
 export type {
   AnyMatchAndValue,
   FindValueByIndex,
diff --git a/packages/router-core/src/load-matches.ts b/packages/router-core/src/load-matches.ts
index a06715de43..b583bf67b4 100644
--- a/packages/router-core/src/load-matches.ts
+++ b/packages/router-core/src/load-matches.ts
@@ -44,6 +44,10 @@ const triggerOnReady = (inner: InnerLoadContext): void | Promise<void> => {
   }
 }
 
+const clearRetainedPending = (match: AnyRouteMatch) => {
+  match._nonReactive.retainedPendingPromise = undefined
+}
+
 const hasForcePendingActiveMatch = (router: AnyRouter): boolean => {
   return router.stores.matchesId.get().some((matchId) => {
     return router.stores.matchStores.get(matchId)?.get()._forcePending
@@ -125,6 +129,7 @@ const handleRedirectAndNotFound = (
 
   // in case of a redirecting match during preload, the match does not exist
   if (match) {
+    clearRetainedPending(match)
     match._nonReactive.beforeLoadPromise?.resolve()
     match._nonReactive.loaderPromise?.resolve()
     match._nonReactive.beforeLoadPromise = undefined
@@ -229,6 +234,7 @@ const handleSerialError = (
   }
 
   inner.updateMatch(matchId, (prev) => {
+    clearRetainedPending(prev)
     prev._nonReactive.beforeLoadPromise?.resolve()
     prev._nonReactive.beforeLoadPromise = undefined
     prev._nonReactive.loadPromise?.resolve()
@@ -836,6 +842,7 @@ const loadRouteMatch = async (
         try {
           await runLoader(inner, matchPromises, matchId, index, route)
           const match = inner.router.getMatch(matchId)!
+          clearRetainedPending(match)
           match._nonReactive.loaderPromise?.resolve()
           match._nonReactive.loadPromise?.resolve()
           match._nonReactive.loaderPromise = undefined
@@ -935,6 +942,7 @@ const loadRouteMatch = async (
   }
   const match = inner.router.getMatch(matchId)!
   if (!loaderIsRunningAsync) {
+    clearRetainedPending(match)
     match._nonReactive.loaderPromise?.resolve()
     match._nonReactive.loadPromise?.resolve()
     match._nonReactive.loadPromise = undefined
diff --git a/packages/solid-router/src/Match.tsx b/packages/solid-router/src/Match.tsx
index e04d2877c8..3f07c8889b 100644
--- a/packages/solid-router/src/Match.tsx
+++ b/packages/solid-router/src/Match.tsx
@@ -5,6 +5,7 @@ import {
   invariant,
   isNotFound,
   isRedirect,
+  markMatchPendingVisible,
   rootRouteId,
 } from '@tanstack/router-core'
 import { isServer } from '@tanstack/router-core/isServer'
@@ -16,7 +17,29 @@ import { nearestMatchContext } from './matchContext'
 import { SafeFragment } from './SafeFragment'
 import { renderRouteNotFound } from './renderRouteNotFound'
 import { ScrollRestoration } from './scroll-restoration'
-import type { AnyRoute, RootRouteOptions } from '@tanstack/router-core'
+import type {
+  AnyRoute,
+  AnyRouteMatch,
+  RootRouteOptions,
+} from '@tanstack/router-core'
+
+const PendingRouteMatch = (props: {
+  matchId: string
+  pendingComponent: unknown
+}) => {
+  const router = useRouter()
+
+  Solid.onMount(() => {
+    const match = router.getMatch(props.matchId)
+    if (match) {
+      markMatchPendingVisible(match)
+    }
+  })
+
+  return props.pendingComponent ? (
+    <Dynamic component={props.pendingComponent as any} />
+  ) : null
+}
 
 export const Match = (props: { matchId: string }) => {
   const router = useRouter()
@@ -69,6 +92,16 @@ export const Match = (props: { matchId: string }) => {
           route().options.pendingComponent ??
           router.options.defaultPendingComponent
 
+        const renderPending = () => {
+          const PendingComponent = resolvePendingComponent()
+          return (
+            <PendingRouteMatch
+              matchId={currentMatchState().matchId}
+              pendingComponent={PendingComponent}
+            />
+          )
+        }
+
         const routeErrorComponent = () =>
           route().options.errorComponent ?? router.options.defaultErrorComponent
 
@@ -107,7 +140,10 @@ export const Match = (props: { matchId: string }) => {
                 fallback={
                   // Don't show fallback on server when using no-ssr mode to avoid hydration mismatch
                   (isServer ?? router.isServer) && resolvedNoSsr ? undefined : (
-                    <Dynamic component={resolvePendingComponent()} />
+                    <PendingRouteMatch
+                      matchId={currentMatchState().matchId}
+                      pendingComponent={resolvePendingComponent()}
+                    />
                   )
                 }
               >
@@ -273,19 +309,37 @@ export const MatchInner = (): any => {
           return <Outlet />
         }
 
+        const renderPending = () => {
+          const PendingComponent =
+            route().options.pendingComponent ??
+            router.options.defaultPendingComponent
+
+          return (
+            <PendingRouteMatch
+              matchId={currentMatch().id}
+              pendingComponent={PendingComponent}
+            />
+          )
+        }
+
         const getLoadPromise = (
           matchId: string,
-          fallbackMatch:
-            | {
-                _nonReactive: {
-                  loadPromise?: Promise<void>
-                }
-              }
-            | undefined,
+          fallbackMatch: AnyRouteMatch | undefined,
         ) => {
           return (
             router.getMatch(matchId)?._nonReactive.loadPromise ??
-            fallbackMatch?._nonReactive.loadPromise
+            fallbackMatch?._nonReactive.loadPromise ??
+            router.latestLoadPromise
+          )
+        }
+
+        const getRetainedPendingPromise = (
+          matchId: string,
+          fallbackMatch: AnyRouteMatch | undefined,
+        ) => {
+          return (
+            router.getMatch(matchId)?._nonReactive.retainedPendingPromise ??
+            fallbackMatch?._nonReactive.retainedPendingPromise
           )
         }
 
@@ -305,7 +359,7 @@ export const MatchInner = (): any => {
                       .displayPendingPromise,
                 )
 
-                return <>{displayPendingResult()}</>
+                return <>{displayPendingResult() ?? renderPending()}</>
               }}
             </Solid.Match>
             <Solid.Match when={currentMatch()._forcePending}>
@@ -316,7 +370,7 @@ export const MatchInner = (): any => {
                       .minPendingPromise,
                 )
 
-                return <>{minPendingResult()}</>
+                return <>{minPendingResult() ?? renderPending()}</>
               }}
             </Solid.Match>
             <Solid.Match when={currentMatch().status === 'pending'}>
@@ -353,18 +407,7 @@ export const MatchInner = (): any => {
                     .loadPromise
                 })
 
-                const FallbackComponent =
-                  route().options.pendingComponent ??
-                  router.options.defaultPendingComponent
-
-                return (
-                  <>
-                    {FallbackComponent && pendingMinMs > 0 ? (
-                      <Dynamic component={FallbackComponent} />
-                    ) : null}
-                    {loaderResult()}
-                  </>
-                )
+                return <>{loaderResult() ?? renderPending()}</>
               }}
             </Solid.Match>
             <Solid.Match when={currentMatch().status === 'notFound'}>
@@ -409,7 +452,14 @@ export const MatchInner = (): any => {
                   return getLoadPromise(matchId, routerMatch)
                 })
 
-                return <>{loaderResult()}</>
+                const [retainedPendingResult] = Solid.createResource(
+                  async () => {
+                    await new Promise((r) => setTimeout(r, 0))
+                    return getRetainedPendingPromise(matchId, routerMatch)
+                  },
+                )
+
+                return <>{loaderResult() ?? retainedPendingResult()}</>
               }}
             </Solid.Match>
             <Solid.Match when={currentMatch().status === 'error'}>
diff --git a/packages/solid-router/tests/redirect.test.tsx b/packages/solid-router/tests/redirect.test.tsx
index 81bd1f6bc6..f975304450 100644
--- a/packages/solid-router/tests/redirect.test.tsx
+++ b/packages/solid-router/tests/redirect.test.tsx
@@ -138,10 +138,12 @@ describe('redirect', () => {
       const router = createRouter({
         routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
         history,
+        defaultViewTransition: true,
       })
 
       render(() => <RouterProvider router={router} />)
 
+      expect(await screen.findByTestId('pending')).toBeInTheDocument()
       // The lazy target route adds the async boundary that exposes the stale
       // redirected-match render path this regression is guarding.
       expect(await screen.findByTestId('lazy-route-page')).toBeInTheDocument()
diff --git a/packages/vue-router/src/Match.tsx b/packages/vue-router/src/Match.tsx
index 7bd17682b0..99e86d08c0 100644
--- a/packages/vue-router/src/Match.tsx
+++ b/packages/vue-router/src/Match.tsx
@@ -5,6 +5,7 @@ import {
   invariant,
   isNotFound,
   isRedirect,
+  markMatchPendingVisible,
   rootRouteId,
 } from '@tanstack/router-core'
 import { isServer } from '@tanstack/router-core/isServer'
@@ -21,7 +22,11 @@ import {
 import { renderRouteNotFound } from './renderRouteNotFound'
 import { ScrollRestoration } from './scroll-restoration'
 import type { VNode } from 'vue'
-import type { AnyRoute, RootRouteOptions } from '@tanstack/router-core'
+import type {
+  AnyRoute,
+  AnyRouteMatch,
+  RootRouteOptions,
+} from '@tanstack/router-core'
 
 export const Match = Vue.defineComponent({
   name: 'Match',
@@ -93,10 +98,6 @@ export const Match = Vue.defineComponent({
         router?.options?.defaultPendingComponent,
     )
 
-    const pendingElement = Vue.computed(() =>
-      PendingComponent.value ? Vue.h(PendingComponent.value) : undefined,
-    )
-
     const routeErrorComponent = Vue.computed(
       () =>
         route.value?.options?.errorComponent ??
@@ -154,7 +155,10 @@ export const Match = Vue.defineComponent({
           ? Vue.h(
               ClientOnly,
               {
-                fallback: pendingElement.value,
+                fallback: Vue.h(PendingRouteMatch, {
+                  matchId: actualMatchId,
+                  pendingComponent: PendingComponent.value,
+                }),
               },
               {
                 default: () => matchInner,
@@ -281,6 +285,33 @@ const OnRendered = Vue.defineComponent({
   },
 })
 
+const PendingRouteMatch = Vue.defineComponent({
+  name: 'PendingRouteMatch',
+  props: {
+    matchId: {
+      type: String,
+      required: true,
+    },
+    pendingComponent: {
+      type: [Object, Function] as Vue.PropType<any>,
+      required: false,
+      default: undefined,
+    },
+  },
+  setup(props) {
+    const router = useRouter()
+
+    Vue.onMounted(() => {
+      const match = router.getMatch(props.matchId)
+      if (match) {
+        markMatchPendingVisible(match)
+      }
+    })
+
+    return () => (props.pendingComponent ? Vue.h(props.pendingComponent) : null)
+  },
+})
+
 export const MatchInner = Vue.defineComponent({
   name: 'MatchInner',
   props: {
@@ -328,15 +359,7 @@ export const MatchInner = Vue.defineComponent({
 
       return {
         routeId: matchRouteId,
-        match: {
-          id: match.id,
-          status: match.status,
-          error: match.error,
-          ssr: match.ssr,
-          _forcePending: match._forcePending,
-          _displayPending: match._displayPending,
-          _nonReactive: match._nonReactive,
-        },
+        match,
         remountKey,
       }
     })
@@ -350,18 +373,20 @@ export const MatchInner = Vue.defineComponent({
     const remountKey = Vue.computed(() => combinedState.value?.remountKey)
 
     const getMatchPromise = (
-      match: {
-        id: string
-        _nonReactive: {
-          displayPendingPromise?: Promise<void>
-          minPendingPromise?: Promise<void>
-          loadPromise?: Promise<void>
-        }
-      },
+      match: AnyRouteMatch,
       key: 'displayPendingPromise' | 'minPendingPromise' | 'loadPromise',
     ) => {
       return (
-        router.getMatch(match.id)?._nonReactive[key] ?? match._nonReactive[key]
+        router.getMatch(match.id)?._nonReactive[key] ??
+        match._nonReactive[key] ??
+        router.latestLoadPromise
+      )
+    }
+
+    const getRetainedPendingPromise = (match: AnyRouteMatch) => {
+      return (
+        router.getMatch(match.id)?._nonReactive.retainedPendingPromise ??
+        match._nonReactive.retainedPendingPromise
       )
     }
 
@@ -375,7 +400,10 @@ export const MatchInner = Vue.defineComponent({
           route.value.options.pendingComponent ??
           router.options.defaultPendingComponent
 
-        return PendingComponent ? Vue.h(PendingComponent) : null
+        return Vue.h(PendingRouteMatch, {
+          matchId: match.value.id,
+          pendingComponent: PendingComponent,
+        })
       }
 
       if (match.value._forcePending) {
@@ -383,7 +411,10 @@ export const MatchInner = Vue.defineComponent({
           route.value.options.pendingComponent ??
           router.options.defaultPendingComponent
 
-        return PendingComponent ? Vue.h(PendingComponent) : null
+        return Vue.h(PendingRouteMatch, {
+          matchId: match.value.id,
+          pendingComponent: PendingComponent,
+        })
       }
 
       if (match.value.status === 'notFound') {
@@ -405,7 +436,19 @@ export const MatchInner = Vue.defineComponent({
 
           invariant()
         }
-        throw getMatchPromise(match.value, 'loadPromise')
+
+        const promise = getMatchPromise(match.value, 'loadPromise')
+        if (promise) {
+          throw promise
+        }
+
+        const retainedPendingPromise = getRetainedPendingPromise(match.value)
+
+        if (retainedPendingPromise) {
+          throw retainedPendingPromise
+        }
+
+        return null
       }
 
       if (match.value.status === 'error') {
@@ -464,7 +507,10 @@ export const MatchInner = Vue.defineComponent({
           router.options.defaultPendingComponent
 
         if (PendingComponent) {
-          return Vue.h(PendingComponent)
+          return Vue.h(PendingRouteMatch, {
+            matchId: match.value.id,
+            pendingComponent: PendingComponent,
+          })
         }
 
         // If no pending component, return null while loading
diff --git a/packages/vue-router/tests/redirect.test.tsx b/packages/vue-router/tests/redirect.test.tsx
index b7ba2e1af3..55cb031668 100644
--- a/packages/vue-router/tests/redirect.test.tsx
+++ b/packages/vue-router/tests/redirect.test.tsx
@@ -128,10 +128,12 @@ describe('redirect', () => {
       const router = createRouter({
         routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
         history: createMemoryHistory({ initialEntries: ['/'] }),
+        defaultViewTransition: true,
       })
 
       render(<RouterProvider router={router} />)
 
+      expect(await screen.findByTestId('pending')).toBeInTheDocument()
       // The lazy target route adds the async boundary that exposes the stale
       // redirected-match render path this regression is guarding.
       expect(await screen.findByTestId('lazy-route-page')).toBeInTheDocument()
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 629e810114..212176cb1a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -997,6 +997,46 @@ importers:
         specifier: ^8.0.0
         version: 8.0.0(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
 
+  e2e/react-router/issue-7120:
+    dependencies:
+      '@tailwindcss/vite':
+        specifier: ^4.2.2
+        version: 4.2.2(vite@8.0.0(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+      '@tanstack/react-router':
+        specifier: workspace:*
+        version: link:../../../packages/react-router
+      react:
+        specifier: ^19.2.3
+        version: 19.2.3
+      react-dom:
+        specifier: ^19.2.3
+        version: 19.2.3(react@19.2.3)
+      redaxios:
+        specifier: ^0.5.1
+        version: 0.5.1
+      tailwindcss:
+        specifier: ^4.2.2
+        version: 4.2.2
+    devDependencies:
+      '@playwright/test':
+        specifier: ^1.57.0
+        version: 1.58.0
+      '@tanstack/router-e2e-utils':
+        specifier: workspace:^
+        version: link:../../e2e-utils
+      '@types/react':
+        specifier: ^19.2.8
+        version: 19.2.9
+      '@types/react-dom':
+        specifier: ^19.2.3
+        version: 19.2.3(@types/react@19.2.9)
+      '@vitejs/plugin-react':
+        specifier: ^6.0.1
+        version: 6.0.1(vite@8.0.0(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+      vite:
+        specifier: ^8.0.0
+        version: 8.0.0(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.97.2)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+
   e2e/react-router/js-only-file-based:
     dependencies:
       '@tailwindcss/vite':
