Repository: TanStack/router (ts)

Issue:
`navigate` functions without `to` but with `search` causes a route error inside a layout route.
### Describe the bug

After version `1.31.7` the error started occurring. Related to #1548. @SeanCassiere

### Your Example Website or App

https://stackblitz.com/edit/tanstack-router-1udi7f?file=src%2Froutes%2F_test%2Ftabs-example.%24someId.tsx

### Steps to Reproduce the Bug or Issue

Go to "Tabs example" route, change the tab and the error is caused. If you put the URL manually, let's say `...?tab=tab2` it renders well, but as soon as you change the tab the app crash

### Expected behavior

Both tabs work well without crashing the app.

### Screenshots or Videos

_No response_

### Platform

- OS: [macOS]
- Browser: [Arc]
- Version: [1.31.7]


### Additional context

_No response_

Pull request description:
fix(react-router): when navigating without `to` the layout segments are not being stripped out
#1548 didn't account for layout routes as pointed out in #1560.

FIxes #1560 

Patch:
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index 49727c357..32c2d785c 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -11,6 +11,7 @@ import {
   functionalUpdate,
   last,
   pick,
+  removeLayoutSegments,
   replaceEqualDeep,
 } from './utils'
 import { getRouteMatch } from './RouterProvider'
@@ -935,12 +936,17 @@ export class Router<
         fromMatches.find((e) => e.routeId === d.routeId),
       )
 
+      const fromRouteByFromPath = stayingMatches?.find(
+        (d) => d.pathname === fromPath,
+      )
+
       let pathname = dest.to
         ? this.resolvePathWithBase(fromPath, `${dest.to}`)
         : this.resolvePathWithBase(
             fromPath,
-            stayingMatches?.find((d) => d.pathname === fromPath)?.routeId ||
-              fromPath,
+            fromRouteByFromPath
+              ? removeLayoutSegments(fromRouteByFromPath.routeId)
+              : fromPath,
           )
 
       const prevParams = { ...last(fromMatches)?.params }
diff --git a/packages/react-router/src/utils.ts b/packages/react-router/src/utils.ts
index 530a41534..22b2bb3f1 100644
--- a/packages/react-router/src/utils.ts
+++ b/packages/react-router/src/utils.ts
@@ -339,3 +339,17 @@ export function createControlledPromise<T>(onResolve?: () => void) {
 
   return controlledPromise
 }
+
+/**
+ * Removes all segments from a given path that start with an underscore ('_').
+ *
+ * @param {string} routePath - The path from which to remove segments. Defaults to '/'.
+ * @returns {string} The path with all underscore-prefixed segments removed.
+ * @example
+ * removeLayoutSegments('/workspace/_auth/foo') // '/workspace/foo'
+ */
+export function removeLayoutSegments(routePath: string): string {
+  const segments = routePath.split('/')
+  const newSegments = segments.filter((segment) => !segment.startsWith('_'))
+  return newSegments.join('/')
+}


Test patch:
diff --git a/packages/react-router/tests/redirects.test.ts b/packages/react-router/tests/redirects.test.ts
index ccc5c8d79..7c0b35cce 100644
--- a/packages/react-router/tests/redirects.test.ts
+++ b/packages/react-router/tests/redirects.test.ts
@@ -40,16 +40,33 @@ function createTestRouter(initialHistory?: RouterHistory) {
     path: '/$framework',
   })
 
+  const userRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/u',
+  })
+  const userLayoutRoute = createRoute({
+    id: '_layout',
+    getParentRoute: () => userRoute,
+  })
+  const usernameRoute = createRoute({
+    getParentRoute: () => userLayoutRoute,
+    path: '$username',
+  })
+
   const projectTree = projectRoute.addChildren([
     projectIdRoute.addChildren([
       projectVersionRoute.addChildren([projectFrameRoute]),
     ]),
   ])
+  const userTree = userRoute.addChildren([
+    userLayoutRoute.addChildren([usernameRoute]),
+  ])
 
   const routeTree = rootRoute.addChildren([
     indexRoute,
     postsRoute.addChildren([postIdRoute]),
     projectTree,
+    userTree,
   ])
   const router = createRouter({ routeTree, history })
 
@@ -354,3 +371,40 @@ describe('router.navigate navigation using multiple path params - function synta
     expect(router.state.location.pathname).toBe('/p/router/v1/vue')
   })
 })
+
+describe('router.navigate navigation using layout routes resolves correctly', async () => {
+  it('should resolve "/u/tanner" in "/u/_layout/$username" to "/u/tkdodo"', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/u/tanner'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.location.pathname).toBe('/u/tanner')
+
+    await router.navigate({
+      to: '/u/$username',
+      params: { username: 'tkdodo' },
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/u/tkdodo')
+  })
+
+  it('should resolve "/u/tanner" in "/u/_layout/$username" to "/u/tkdodo" w/o "to" path being provided', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/u/tanner'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.location.pathname).toBe('/u/tanner')
+
+    await router.navigate({
+      params: { username: 'tkdodo' },
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/u/tkdodo')
+  })
+})
diff --git a/packages/react-router/tests/utils.test.ts b/packages/react-router/tests/utils.test.ts
index d26b7a596..5504c5cbd 100644
--- a/packages/react-router/tests/utils.test.ts
+++ b/packages/react-router/tests/utils.test.ts
@@ -4,6 +4,7 @@ import {
   isPlainArray,
   removeTrailingSlash,
   replaceEqualDeep,
+  removeLayoutSegments,
 } from '../src/utils'
 
 describe('replaceEqualDeep', () => {
@@ -360,3 +361,53 @@ describe('exactPathTest', () => {
     expect(result).toBe(true)
   })
 })
+
+describe('removeLayoutSegments', () => {
+  it('should remove the "_layout" segment from "/_layout/" and return "/"', () => {
+    const path = '/_layout/'
+    const result = removeLayoutSegments(path)
+    expect(result).toBe('/')
+  })
+
+  it('should remove the "_layout" segment from "/_layout/blog" and return "/blog"', () => {
+    const path = '/_layout/blog'
+    const result = removeLayoutSegments(path)
+    expect(result).toBe('/blog')
+  })
+
+  it('should remove the "_layout1" and "_layout2" segments from "/_layout1/" and return "/"', () => {
+    const path = '/_layout1/_layout2/'
+    const result = removeLayoutSegments(path)
+    expect(result).toBe('/')
+  })
+
+  it('should remove the "_layout1" and "_layout2" segments from "/_layout1/blog" and return "/blog"', () => {
+    const path = '/_layout1/_layout2/blog'
+    const result = removeLayoutSegments(path)
+    expect(result).toBe('/blog')
+  })
+
+  it('should remove the "_layout" segment from "/posts/_layout/" and return "/posts/"', () => {
+    const path = '/posts/_layout/1'
+    const result = removeLayoutSegments(path)
+    expect(result).toBe('/posts/1')
+  })
+
+  it('should remove the "_layout" segment from "/posts/_layout/1" and return "/posts/1"', () => {
+    const path = '/posts/_layout/1'
+    const result = removeLayoutSegments(path)
+    expect(result).toBe('/posts/1')
+  })
+
+  it('should remove the "_layout" segment from "/posts/_layout/" and return "/posts"', () => {
+    const path = '/posts/_layout/'
+    const result = removeLayoutSegments(path)
+    expect(result).toBe('/posts/')
+  })
+
+  it('should remove the "_layout" segment from "/posts/_layout/blog" and return "/posts/blog"', () => {
+    const path = '/posts/_layout/blog'
+    const result = removeLayoutSegments(path)
+    expect(result).toBe('/posts/blog')
+  })
+})