Repository: TanStack/router (ts)

Issue:
Navigation without to param does not work since 1.30.1
### Describe the bug

I updated to 1.30.1, because fix for bug with path params was introduced and it works fine. But, it broke other thing. Before 1.30.1 I was able to make link, or use navigate(), or throw redirect() without specifing to. I did it, because I only need to change path param(for example language code in path param), but I want to leave user on the same page and it worked fine. Now it ignores new path params and generates href with old params in this case.

### Your Example Website or App

everywhere

### Steps to Reproduce the Bug or Issue

1. Create <Link> component and specify only from and params
2. Press/hover on it. Nothing happens

### Expected behavior

I expected to generate href with new params, but previous path

### Screenshots or Videos

_No response_

### Platform

- OS: all
- Browser: all
- Version: 1.30.1


### Additional context

_No response_

Pull request description:
fix(react-router): changing path params without specifying the `to` destination
Add fixes and tests for `router.navigate` when using the `params: (prevParams) => newParams` option (with no extra `to` or `from` option).

Thanks to @SeanCassiere for the suggestion to patch `router.ts`

Fixes #1525
Should also fix https://github.com/TanStack/tanstack.com/issues/218 

Patch:
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index 7a756b8d9..58d8113f4 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -937,7 +937,11 @@ export class Router<
 
       let pathname = dest.to
         ? this.resolvePathWithBase(fromPath, `${dest.to}`)
-        : this.resolvePathWithBase(fromPath, fromPath)
+        : this.resolvePathWithBase(
+            fromPath,
+            stayingMatches?.find((d) => d.pathname === fromPath)?.routeId ||
+              fromPath,
+          )
 
       const prevParams = { ...last(fromMatches)?.params }
 


Test patch:
diff --git a/packages/react-router/tests/redirects.test.ts b/packages/react-router/tests/redirects.test.ts
new file mode 100644
index 000000000..ccc5c8d79
--- /dev/null
+++ b/packages/react-router/tests/redirects.test.ts
@@ -0,0 +1,356 @@
+import { describe, expect, it } from 'vitest'
+
+import {
+  type RouterHistory,
+  createMemoryHistory,
+  createRootRoute,
+  createRoute,
+  createRouter,
+  redirect,
+} from '../src'
+
+function createTestRouter(initialHistory?: RouterHistory) {
+  const history =
+    initialHistory ?? createMemoryHistory({ initialEntries: ['/'] })
+
+  const rootRoute = createRootRoute({})
+  const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: '/' })
+  const postsRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/posts',
+  })
+  const postIdRoute = createRoute({
+    getParentRoute: () => postsRoute,
+    path: '/$slug',
+  })
+  const projectRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/p',
+  })
+  const projectIdRoute = createRoute({
+    getParentRoute: () => projectRoute,
+    path: '/$projectId',
+  })
+  const projectVersionRoute = createRoute({
+    getParentRoute: () => projectIdRoute,
+    path: '/$version',
+  })
+  const projectFrameRoute = createRoute({
+    getParentRoute: () => projectVersionRoute,
+    path: '/$framework',
+  })
+
+  const projectTree = projectRoute.addChildren([
+    projectIdRoute.addChildren([
+      projectVersionRoute.addChildren([projectFrameRoute]),
+    ]),
+  ])
+
+  const routeTree = rootRoute.addChildren([
+    indexRoute,
+    postsRoute.addChildren([postIdRoute]),
+    projectTree,
+  ])
+  const router = createRouter({ routeTree, history })
+
+  return {
+    router,
+    routes: {
+      indexRoute,
+      postsRoute,
+      postIdRoute,
+      projectRoute,
+      projectIdRoute,
+      projectVersionRoute,
+      projectFrameRoute,
+    },
+  }
+}
+
+describe('router.navigate navigation using a single path param - object syntax for updates', () => {
+  it('should change $slug in "/posts/$slug" from "tanner" to "tkdodo"', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/posts/tanner'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.resolvedLocation.pathname).toBe('/posts/tanner')
+
+    await router.navigate({
+      to: '/posts/$slug',
+      params: { slug: 'tkdodo' },
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/posts/tkdodo')
+  })
+
+  it('should change $slug in "/posts/$slug" from "tanner" to "tkdodo" w/o "to" path being provided', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/posts/tanner'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.resolvedLocation.pathname).toBe('/posts/tanner')
+
+    await router.navigate({
+      params: { slug: 'tkdodo' },
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/posts/tkdodo')
+  })
+})
+
+describe('router.navigate navigation using a single path param - function syntax for updates', () => {
+  it('should change $slug in "/posts/$slug" from "tanner" to "tkdodo"', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/posts/tanner'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.resolvedLocation.pathname).toBe('/posts/tanner')
+
+    await router.navigate({
+      to: '/posts/$slug',
+      params: (p: any) => ({ ...p, slug: 'tkdodo' }),
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/posts/tkdodo')
+  })
+
+  it('should change $slug in "/posts/$slug" from "tanner" to "tkdodo" w/o "to" path being provided', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/posts/tanner'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.resolvedLocation.pathname).toBe('/posts/tanner')
+
+    await router.navigate({
+      params: (p: any) => ({ ...p, slug: 'tkdodo' }),
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/posts/tkdodo')
+  })
+})
+
+describe('router.navigate navigation using multiple path params - object syntax for updates', () => {
+  it('should change $projectId in "/p/$projectId/$version/$framework" from "router" to "query"', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/p/router/v1/react'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.location.pathname).toBe('/p/router/v1/react')
+
+    await router.navigate({
+      to: '/p/$projectId/$version/$framework',
+      params: { projectId: 'query' },
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/p/query/v1/react')
+  })
+
+  it('should change $projectId in "/p/$projectId/$version/$framework" from "router" to "query" w/o "to" path being provided', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/p/router/v1/react'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.location.pathname).toBe('/p/router/v1/react')
+
+    await router.navigate({
+      params: { projectId: 'query' },
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/p/query/v1/react')
+  })
+
+  it('should change $version in "/p/$projectId/$version/$framework" from "v1" to "v3"', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/p/router/v1/react'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.location.pathname).toBe('/p/router/v1/react')
+
+    await router.navigate({
+      to: '/p/$projectId/$version/$framework',
+      params: { version: 'v3' },
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/p/router/v3/react')
+  })
+
+  it('should change $version in "/p/$projectId/$version/$framework" from "v1" to "v3" w/o "to" path being provided', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/p/router/v1/react'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.location.pathname).toBe('/p/router/v1/react')
+
+    await router.navigate({
+      params: { version: 'v3' },
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/p/router/v3/react')
+  })
+
+  it('should change $framework in "/p/$projectId/$version/$framework" from "react" to "vue"', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/p/router/v1/react'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.location.pathname).toBe('/p/router/v1/react')
+
+    await router.navigate({
+      to: '/p/$projectId/$version/$framework',
+      params: { framework: 'vue' },
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/p/router/v1/vue')
+  })
+
+  it('should change $framework in "/p/$projectId/$version/$framework" from "react" to "vue" w/o "to" path being provided', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/p/router/v1/react'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.location.pathname).toBe('/p/router/v1/react')
+
+    await router.navigate({
+      params: { framework: 'vue' },
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/p/router/v1/vue')
+  })
+})
+
+describe('router.navigate navigation using multiple path params - function syntax for updates', () => {
+  it('should change $projectId in "/p/$projectId/$version/$framework" from "router" to "query"', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/p/router/v1/react'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.location.pathname).toBe('/p/router/v1/react')
+
+    await router.navigate({
+      to: '/p/$projectId/$version/$framework',
+      params: (p: any) => ({ ...p, projectId: 'query' }),
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/p/query/v1/react')
+  })
+
+  it('should change $projectId in "/p/$projectId/$version/$framework" from "router" to "query" w/o "to" path being provided', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/p/router/v1/react'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.location.pathname).toBe('/p/router/v1/react')
+
+    await router.navigate({
+      params: (p: any) => ({ ...p, projectId: 'query' }),
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/p/query/v1/react')
+  })
+
+  it('should change $version in "/p/$projectId/$version/$framework" from "v1" to "v3"', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/p/router/v1/react'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.location.pathname).toBe('/p/router/v1/react')
+
+    await router.navigate({
+      to: '/p/$projectId/$version/$framework',
+      params: (p: any) => ({ ...p, version: 'v3' }),
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/p/router/v3/react')
+  })
+
+  it('should change $version in "/p/$projectId/$version/$framework" from "v1" to "v3" w/o "to" path being provided', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/p/router/v1/react'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.location.pathname).toBe('/p/router/v1/react')
+
+    await router.navigate({
+      params: (p: any) => ({ ...p, version: 'v3' }),
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/p/router/v3/react')
+  })
+
+  it('should change $framework in "/p/$projectId/$version/$framework" from "react" to "vue"', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/p/router/v1/react'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.location.pathname).toBe('/p/router/v1/react')
+
+    await router.navigate({
+      to: '/p/$projectId/$version/$framework',
+      params: (p: any) => ({ ...p, framework: 'vue' }),
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/p/router/v1/vue')
+  })
+
+  it('should change $framework in "/p/$projectId/$version/$framework" from "react" to "vue" w/o "to" path being provided', async () => {
+    const { router } = createTestRouter(
+      createMemoryHistory({ initialEntries: ['/p/router/v1/react'] }),
+    )
+
+    await router.load()
+
+    expect(router.state.location.pathname).toBe('/p/router/v1/react')
+
+    await router.navigate({
+      params: (p: any) => ({ ...p, framework: 'vue' }),
+    })
+    await router.invalidate()
+
+    expect(router.state.location.pathname).toBe('/p/router/v1/vue')
+  })
+})