Repository: TanStack/router (ts)

Issue:
useNavigate does not work when `from` is passed on hook declaration
### Which project does this relate to?

Router

### Describe the bug

Hi there! Love the tool, but the `useNavigate` hook doesn't seem to respect the `from` field. If I create the hook with `useNavigate({ from: Route.fullRoute })`, it seems to be relative to the current URL. If instead, I pass `from` directly while calling it (i.e., `const navigate = useNavigate(); navigate({ from: Route.fullRoute, ... })`), it works correctly and navigates to the URL relative to the full path of the provided route.

I'm using the `useNavigate` hook within a `route.tsx`, I'm not sure it would possible to replicate this behavior outside of a route file (since, then, the `fullRoute` would just be the current URL).

### Your Example Website or App

https://github.com/Andrew-Talley/tsr-navigate-bug

### Steps to Reproduce the Bug or Issue

This was using create tanstack-router — to set it up, just run `yarn` and `yarn start`.

From there, click on "Navigate to subfolder" and then "Go to File 2." From there, if you click on "Go Home - Inherited from" (which uses `useNavigate({ from: Route.fullPath })`), the URL doesn't change and it stays on /folder-1/folder-2/file. If you click "Go Home - Explicit from" (which uses `navigate({ from: Route.fullPath, to: '.' })`), then it does work.

### Expected behavior

Passing `from` when declaring the `useNavigate` hook should behave the same as passing it when calling the function.

### Screenshots or Videos

_No response_

### Platform

- OS: Mac
- Browser: Chrome
- Version: 1.112.13


### Additional context

_No response_

Pull request:
fix: use default from in `useNavigate`
fixes #3668

Patch:
diff --git a/docs/start/framework/react/build-from-scratch.md b/docs/start/framework/react/build-from-scratch.md
index 9532ee083..66eecc1e9 100644
--- a/docs/start/framework/react/build-from-scratch.md
+++ b/docs/start/framework/react/build-from-scratch.md
@@ -43,6 +43,8 @@ We highly recommend using TypeScript with TanStack Start. Create a `tsconfig.jso
 }
 ```
 
+> [!NOTE] > Enabling `verbatimModuleSyntax` can result in server bundles leaking into client bundles. It is recommended to keep this option disabled.
+
 ## Install Dependencies
 
 TanStack Start is (currently\*) powered by [Vinxi](https://vinxi.vercel.app/) and [TanStack Router](https://tanstack.com/router) and requires them as dependencies.
diff --git a/packages/react-router/src/useNavigate.tsx b/packages/react-router/src/useNavigate.tsx
index 8319a969e..64fdeb470 100644
--- a/packages/react-router/src/useNavigate.tsx
+++ b/packages/react-router/src/useNavigate.tsx
@@ -19,28 +19,14 @@ export function useNavigate<
   return React.useCallback(
     (options: NavigateOptions) => {
       return navigate({
+        from: _defaultOpts?.from,
         ...options,
       })
     },
-    [navigate],
+    [_defaultOpts?.from, navigate],
   ) as UseNavigateResult<TDefaultFrom>
 }
 
-// NOTE: I don't know of anyone using this. It's undocumented, so let's wait until someone needs it
-// export function typedNavigate<
-//   TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-//   TDefaultFrom extends RoutePaths<TRouteTree> = '/',
-// >(navigate: (opts: NavigateOptions<any>) => Promise<void>) {
-//   return navigate as <
-//     TFrom extends RoutePaths<TRouteTree> = TDefaultFrom,
-//     TTo extends string = '',
-//     TMaskFrom extends RoutePaths<TRouteTree> = '/',
-//     TMaskTo extends string = '',
-//   >(
-//     opts?: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
-//   ) => Promise<void>
-// } //
-
 export function Navigate<
   TRouter extends AnyRouter = RegisteredRouter,
   const TFrom extends string = string,
diff --git a/packages/solid-router/src/useNavigate.tsx b/packages/solid-router/src/useNavigate.tsx
index 2c474fd03..d63e7b9bd 100644
--- a/packages/solid-router/src/useNavigate.tsx
+++ b/packages/solid-router/src/useNavigate.tsx
@@ -17,25 +17,10 @@ export function useNavigate<
   const { navigate } = useRouter()
 
   return ((options: NavigateOptions) => {
-    return navigate({ ...options })
+    return navigate({ from: _defaultOpts?.from, ...options })
   }) as UseNavigateResult<TDefaultFrom>
 }
 
-// NOTE: I don't know of anyone using this. It's undocumented, so let's wait until someone needs it
-// export function typedNavigate<
-//   TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-//   TDefaultFrom extends RoutePaths<TRouteTree> = '/',
-// >(navigate: (opts: NavigateOptions<any>) => Promise<void>) {
-//   return navigate as <
-//     TFrom extends RoutePaths<TRouteTree> = TDefaultFrom,
-//     TTo extends string = '',
-//     TMaskFrom extends RoutePaths<TRouteTree> = '/',
-//     TMaskTo extends string = '',
-//   >(
-//     opts?: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
-//   ) => Promise<void>
-// } //
-
 export function Navigate<
   TRouter extends AnyRouter = RegisteredRouter,
   const TFrom extends string = string,


Test patch:
diff --git a/packages/react-router/tests/useNavigate.test.tsx b/packages/react-router/tests/useNavigate.test.tsx
index 0293de467..621a82ad3 100644
--- a/packages/react-router/tests/useNavigate.test.tsx
+++ b/packages/react-router/tests/useNavigate.test.tsx
@@ -1335,3 +1335,141 @@ test('<Navigate> navigates only once in <StrictMode>', async () => {
   expect(await screen.findByTestId('posts-title')).toBeInTheDocument()
   expect(navigateSpy.mock.calls.length).toBe(1)
 })
+
+test('when on /posts/$postId and navigating to ../ with default `from` /posts', async () => {
+  const rootRoute = createRootRoute()
+
+  const IndexComponent = () => {
+    const navigate = useNavigate()
+    return (
+      <React.Fragment>
+        <h1 data-testid="index-heading">Index</h1>
+        <button onClick={() => navigate({ to: '/posts' })}>Posts</button>
+        <button
+          data-testid="index-to-first-post-btn"
+          onClick={() =>
+            navigate({
+              to: '/posts/$postId/details',
+              params: { postId: 'id1' },
+            })
+          }
+        >
+          To first post
+        </button>
+      </React.Fragment>
+    )
+  }
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    component: IndexComponent,
+  })
+
+  const layoutRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    id: '_layout',
+    component: () => {
+      return (
+        <>
+          <h1>Layout</h1>
+          <Outlet />
+        </>
+      )
+    },
+  })
+
+  const PostsComponent = () => {
+    const navigate = useNavigate({ from: '/posts' })
+    return (
+      <React.Fragment>
+        <h1>Posts</h1>
+        <button
+          data-testid="btn-to-home"
+          onClick={() => navigate({ to: '../' })}
+        >
+          To Home
+        </button>
+        <Outlet />
+      </React.Fragment>
+    )
+  }
+
+  const postsRoute = createRoute({
+    getParentRoute: () => layoutRoute,
+    path: 'posts',
+    component: PostsComponent,
+  })
+
+  const PostComponent = () => {
+    const params = useParams({ strict: false })
+    return (
+      <React.Fragment>
+        <span>Params: {params.postId}</span>
+        <Outlet />
+      </React.Fragment>
+    )
+  }
+
+  const postRoute = createRoute({
+    getParentRoute: () => postsRoute,
+    path: '$postId',
+    component: PostComponent,
+  })
+
+  const PostIndexComponent = () => {
+    return (
+      <>
+        <h1>Post Index</h1>
+      </>
+    )
+  }
+
+  const postIndexRoute = createRoute({
+    getParentRoute: () => postRoute,
+    path: '/',
+    component: PostIndexComponent,
+  })
+
+  const DetailsComponent = () => {
+    return (
+      <>
+        <h1 data-testid="details-heading">Details!</h1>
+      </>
+    )
+  }
+
+  const detailsRoute = createRoute({
+    getParentRoute: () => postRoute,
+    path: 'details',
+    component: DetailsComponent,
+  })
+
+  const router = createRouter({
+    routeTree: rootRoute.addChildren([
+      indexRoute,
+      layoutRoute.addChildren([
+        postsRoute.addChildren([
+          postRoute.addChildren([postIndexRoute, detailsRoute]),
+        ]),
+      ]),
+    ]),
+  })
+
+  render(<RouterProvider router={router} />)
+
+  const postsButton = await screen.findByTestId('index-to-first-post-btn')
+
+  fireEvent.click(postsButton)
+
+  expect(await screen.findByTestId('details-heading')).toBeInTheDocument()
+
+  expect(window.location.pathname).toEqual('/posts/id1/details')
+
+  const homeButton = await screen.findByTestId('btn-to-home')
+
+  fireEvent.click(homeButton)
+
+  expect(await screen.findByTestId('index-heading')).toBeInTheDocument()
+  expect(window.location.pathname).toEqual('/')
+})
diff --git a/packages/solid-router/tests/useNavigate.test.tsx b/packages/solid-router/tests/useNavigate.test.tsx
index 373f4bf1e..3f026754d 100644
--- a/packages/solid-router/tests/useNavigate.test.tsx
+++ b/packages/solid-router/tests/useNavigate.test.tsx
@@ -1302,3 +1302,141 @@ test('when setting search params with 2 parallel navigate calls', async () => {
   expect(search.get('param1')).toEqual('foo')
   expect(search.get('param2')).toEqual('bar')
 })
+
+test('when on /posts/$postId and navigating to ../ with default `from` /posts', async () => {
+  const rootRoute = createRootRoute()
+
+  const IndexComponent = () => {
+    const navigate = useNavigate()
+    return (
+      <>
+        <h1 data-testid="index-heading">Index</h1>
+        <button onClick={() => navigate({ to: '/posts' })}>Posts</button>
+        <button
+          data-testid="index-to-first-post-btn"
+          onClick={() =>
+            navigate({
+              to: '/posts/$postId/details',
+              params: { postId: 'id1' },
+            })
+          }
+        >
+          To first post
+        </button>
+      </>
+    )
+  }
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    component: IndexComponent,
+  })
+
+  const layoutRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    id: '_layout',
+    component: () => {
+      return (
+        <>
+          <h1>Layout</h1>
+          <Outlet />
+        </>
+      )
+    },
+  })
+
+  const PostsComponent = () => {
+    const navigate = useNavigate({ from: '/posts' })
+    return (
+      <>
+        <h1>Posts</h1>
+        <button
+          data-testid="btn-to-home"
+          onClick={() => navigate({ to: '../' })}
+        >
+          To Home
+        </button>
+        <Outlet />
+      </>
+    )
+  }
+
+  const postsRoute = createRoute({
+    getParentRoute: () => layoutRoute,
+    path: 'posts',
+    component: PostsComponent,
+  })
+
+  const PostComponent = () => {
+    const params = useParams({ strict: false })
+    return (
+      <>
+        <span>Params: {params().postId}</span>
+        <Outlet />
+      </>
+    )
+  }
+
+  const postRoute = createRoute({
+    getParentRoute: () => postsRoute,
+    path: '$postId',
+    component: PostComponent,
+  })
+
+  const PostIndexComponent = () => {
+    return (
+      <>
+        <h1>Post Index</h1>
+      </>
+    )
+  }
+
+  const postIndexRoute = createRoute({
+    getParentRoute: () => postRoute,
+    path: '/',
+    component: PostIndexComponent,
+  })
+
+  const DetailsComponent = () => {
+    return (
+      <>
+        <h1 data-testid="details-heading">Details!</h1>
+      </>
+    )
+  }
+
+  const detailsRoute = createRoute({
+    getParentRoute: () => postRoute,
+    path: 'details',
+    component: DetailsComponent,
+  })
+
+  const router = createRouter({
+    routeTree: rootRoute.addChildren([
+      indexRoute,
+      layoutRoute.addChildren([
+        postsRoute.addChildren([
+          postRoute.addChildren([postIndexRoute, detailsRoute]),
+        ]),
+      ]),
+    ]),
+  })
+
+  render(() => <RouterProvider router={router} />)
+
+  const postsButton = await screen.findByTestId('index-to-first-post-btn')
+
+  fireEvent.click(postsButton)
+
+  expect(await screen.findByTestId('details-heading')).toBeInTheDocument()
+
+  expect(window.location.pathname).toEqual('/posts/id1/details')
+
+  const homeButton = await screen.findByTestId('btn-to-home')
+
+  fireEvent.click(homeButton)
+
+  expect(await screen.findByTestId('index-heading')).toBeInTheDocument()
+  expect(window.location.pathname).toEqual('/')
+})