Repository: TanStack/router (ts)

Issue:
key property on a route component forces unmount/mount 
### Describe the bug

PR [#2056](https://github.com/TanStack/router/pull/2056) added `key` property to route component

https://github.com/TanStack/router/blob/06c3a6923dc71c9f2fbd6e999c3aa93817fa89d0/packages/react-router/src/Match.tsx#L139-L142

Due to this, route component is recreated, rather then reconciled, if `matchId` contains path params.

### Your Example Website or App

no example app

### Steps to Reproduce the Bug or Issue

When I have these routes:
- `view.$id.tsx`
- `view.$id.index.tsx`
- `view.$id.edit.tsx`

And navigate from `/view/one/` to `/view/two` 

### Expected behavior

I expect route component of `view.$id` is not unmounted and mounted again, as due to this internal state is not preserved



### Screenshots or Videos

_No response_

### Platform

- OS: [e.g. macOS, Windows, Linux]
- Browser: [e.g. Chrome, Safari, Firefox]
- Version: [e.g. 91.1]


### Additional context

_No response_

Pull request description:
fix(react-router): use `routeId` to stabilize the memoized route component to prevent from remounting when on the same route
Resolves #2085 

Since the `matchId` is a unique string formed to represent the current match, when using it as a key to stabilize the route component, the `matchId` caused the page component to be remounted even on the smallest changes to a route. Whilst, rendering is expected, remounting certainly is not.

Using the `routeId` here stabilizes the component across many more renders, since it remains static.

Patch:
diff --git a/packages/react-router/src/Match.tsx b/packages/react-router/src/Match.tsx
index 54b117108..7e6b4a8da 100644
--- a/packages/react-router/src/Match.tsx
+++ b/packages/react-router/src/Match.tsx
@@ -138,8 +138,8 @@ export const MatchInner = React.memo(function MatchInnerImpl({
 
   const out = React.useMemo(() => {
     const Comp = route.options.component ?? router.options.defaultComponent
-    return Comp ? <Comp key={matchId} /> : <Outlet />
-  }, [matchId, route.options.component, router.options.defaultComponent])
+    return Comp ? <Comp key={routeId} /> : <Outlet />
+  }, [routeId, route.options.component, router.options.defaultComponent])
 
   React.useEffect(() => {
     if (match.status === 'pending') {


Test patch:
diff --git a/packages/react-router/tests/router.test.tsx b/packages/react-router/tests/router.test.tsx
index 6c00cbe24..cef0b5d68 100644
--- a/packages/react-router/tests/router.test.tsx
+++ b/packages/react-router/tests/router.test.tsx
@@ -1,14 +1,22 @@
-import { act } from 'react'
+import { act, useEffect } from 'react'
 import { afterEach, describe, expect, it, vi } from 'vitest'
-import { cleanup, render, waitFor } from '@testing-library/react'
 import {
-  type RouterHistory,
+  cleanup,
+  fireEvent,
+  render,
+  screen,
+  waitFor,
+} from '@testing-library/react'
+import {
+  Link,
+  Outlet,
   RouterProvider,
   createMemoryHistory,
   createRootRoute,
   createRoute,
   createRouter,
 } from '../src'
+import type { RouterHistory } from '../src'
 
 afterEach(() => {
   vi.resetAllMocks()
@@ -403,3 +411,74 @@ describe('router emits events during rendering', () => {
     unsub()
   })
 })
+
+describe('router rendering stability', () => {
+  it('should not remount the page component when navigating to the same route', async () => {
+    const callerMock = vi.fn()
+
+    const rootRoute = createRootRoute({
+      component: () => {
+        return (
+          <div>
+            <p>Root</p>
+            <div>
+              <Link to="/foo/$id" params={{ id: '1' }}>
+                Foo1
+              </Link>
+              <Link to="/foo/$id" params={{ id: '2' }}>
+                Foo2
+              </Link>
+            </div>
+            <Outlet />
+          </div>
+        )
+      },
+    })
+    const indexRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/',
+      component: () => {
+        return ''
+      },
+    })
+    const fooIdRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/foo/$id',
+      component: FooIdRouteComponent,
+    })
+    function FooIdRouteComponent() {
+      const id = fooIdRoute.useParams({ select: (s) => s.id })
+
+      useEffect(() => {
+        callerMock()
+      }, [])
+
+      return <div>Foo page {id}</div>
+    }
+
+    const routeTree = rootRoute.addChildren([fooIdRoute, indexRoute])
+    const router = createRouter({ routeTree })
+
+    render(<RouterProvider router={router} />)
+
+    const foo1Link = await screen.findByRole('link', { name: 'Foo1' })
+    const foo2Link = await screen.findByRole('link', { name: 'Foo2' })
+
+    expect(foo1Link).toBeInTheDocument()
+    expect(foo2Link).toBeInTheDocument()
+
+    fireEvent.click(foo1Link)
+
+    const fooPage1 = await screen.findByText('Foo page 1')
+    expect(fooPage1).toBeInTheDocument()
+
+    expect(callerMock).toBeCalledTimes(1)
+
+    fireEvent.click(foo2Link)
+
+    const fooPage2 = await screen.findByText('Foo page 2')
+    expect(fooPage2).toBeInTheDocument()
+
+    expect(callerMock).toBeCalledTimes(1)
+  })
+})