| | |

TanStack Router 06 ⚛️ Imperative Nav — The useNavigate hook

⏱️ Estimated read: 9 min | |

The previous chapter covered the <Link> component, which is the declarative navigation mechanism. This chapter covers the imperative navigation: the useNavigate hook, which returns a function that navigates programmatically. The LFCS exam does not test TanStack Router, but the front-end engineering certification paths do. The critical distinction is between the typed useNavigate hook and the legacy window.location or the untyped router APIs. The TanStack Router’s useNavigate is fully typed against the route tree. The to prop is a union of the valid paths. The params prop is required for the dynamic segments. The search prop is typed against the validateSearch schema. The function accepts an options object, not a string, which is the difference from the React Router’s navigate function. The navigate({ to: "/posts/$postId", params: { postId: "1" } }) is the typed form. The navigate("/posts/1") is a type error.

Key point: The useNavigate hook returns a function that accepts a route options object: { to, params, search, hash, state, replace, resetScroll }. The to is a typed route path from the route tree. The params is required for the dynamic segments. The search is typed against the route’s validateSearch. The replace option replaces the history entry instead of pushing. The resetScroll option scrolls to the top after navigation. The hook must be called inside a component rendered within the router context.


Why imperative navigation requires a specific mental model

The typed path problem. The to prop in the React Router’s navigate function is a string. In the TanStack Router’s navigate function, the to is a typed route path. The TypeScript compiler verifies that the path exists in the route tree. The /posts/$postId path is valid. The /invalid path is a compile-time error. The params prop is required for the dynamic segments. The /posts/$postId path without the params is a compile error.

The options object problem. The navigate function does not accept a plain string. It accepts an options object with the to property. The React Router’s navigate("/path") is the TanStack Router’s navigate({ to: "/path" }). The object form is more verbose, but it allows the typed params, search, and state to be passed in the same call.

The from problem. The navigate function is sometimes used with the from option to resolve the relative navigation. The from is the current route. The to is the target. The relative path ".." navigates to the parent route. The from option is inferred from the render context in most cases, so it is not required for the absolute paths.

The replace problem. The replace: true option replaces the current history entry. The user cannot press the back button to return to the previous page. The option is the same as the React Router’s { replace: true }. The correct use cases are the login redirect, the auth guard, and the search filter that should not pollute the history.

The state problem. The state option passes the in-memory object that the useLocation hook reads. The navigate({ to: "/checkout", state: { cartId } }) passes the cart ID. The target route reads it with useLocation({ from: ... }).state. The state is preserved in the history entry, but it is lost on the page refresh.

The trade-off. The typed navigate is stricter than the React Router’s string-based navigate. The strictness moves the errors to the compile time. The cost is the verbosity of the options object and the route tree generation. The benefit is that a broken path or a missing param is caught before the browser runs the code.


a. The basic navigation and the typed path

The useNavigate hook returns the navigate function. The function accepts the route options object.

import { useNavigate } from '@tanstack/react-router'

function LogoutButton() {
  const navigate = useNavigate()

  const handleLogout = async () => {
    await logout()
    navigate({ to: '/login' })
  }

  return <button onClick={handleLogout}>Logout</button>
}

The navigate({ to: '/login' }) pushes a new history entry. The user is on /login, and the back button returns to the previous page.

The type safety is the difference. The /login path is valid because the route exists. The /invalid path is a compile error.

// TypeScript error: '/invalid' is not a valid route
navigate({ to: '/invalid' })

The typed path is the differentiator. The invalid path cannot reach the browser.


b. The params, the search, and the state

The navigate function requires the params for the dynamic segments. The search is typed against the validateSearch.

function PostLink({ postId }: { postId: string }) {
  const navigate = useNavigate()

  const handleClick = () => {
    navigate({
      to: '/posts/$postId',
      params: { postId },
      search: { page: 1 },
    })
  }

  return <button onClick={handleClick}>Open post</button>
}

The to: '/posts/$postId' path requires the params: { postId }. The postId is typed as a string by default. The search: { page: 1 } is typed against the /posts route’s validateSearch. The page is a number.

The state is the in-memory data. The target route reads it with the useLocation hook.

navigate({
  to: '/checkout',
  state: { cartId: 'abc123' },
})

The target route reads the state.

import { useLocation } from '@tanstack/react-router'

function Checkout() {
  const { state } = useLocation()
  const cartId = state?.cartId
  return <p>Cart: {cartId}</p>
}

The state is the object that the navigate passed. The useLocation hook returns the current location, including the state. The state is lost on the page refresh. The URL parameter is the alternative for the data that must survive.


c. The replace, the relative navigation, and the router instance

The replace: true option replaces the history entry. The resetScroll: false option prevents the scroll-to-top.

navigate({
  to: '/dashboard',
  replace: true,
  resetScroll: false,
})

The replace: true ensures that the user cannot press the back button to return to the login page after the login. The resetScroll: false is the correct choice for the pagination and the filter that should not jump to the top.

The relative navigation uses the from option. The to: ".." navigates to the parent route.

navigate({ from: '/posts/$postId', to: '..' })

The from is the current route. The to: ".." is the parent. The /posts/1 route’s parent is /posts. The navigation goes to /posts.

The useRouter hook returns the router instance. The router.navigate is the alternative to the hook. The router.invalidate invalidates the route data. The router.state returns the current router state.

import { useRouter } from '@tanstack/react-router'

function InvalidateButton() {
  const router = useRouter()
  return <button onClick={() => router.invalidate()}>Refresh</button>
}

The router.invalidate() re-runs the loaders for the current route. The data is refreshed. The router.navigate is the same function as the useNavigate hook, but it is available outside the component tree.


Complete Example Session

# ============================================
# PART 1: BASIC NAVIGATE
# ============================================

const navigate = useNavigate()
navigate({ to: '/dashboard' })
# ============================================
# PART 2: TYPED PATH
# ============================================

navigate({ to: '/invalid' })
// TypeScript error: invalid route
# ============================================
# PART 3: DYNAMIC PARAMS
# ============================================

navigate({ to: '/posts/$postId', params: { postId: '123' } })
# ============================================
# PART 4: SEARCH PARAMS
# ============================================

navigate({ to: '/posts', search: { page: 2 } })
# ============================================
# PART 5: STATE
# ============================================

navigate({ to: '/checkout', state: { cartId: 'abc' } })
# ============================================
# PART 6: READ STATE
# ============================================

const { state } = useLocation()
const cartId = state?.cartId
# ============================================
# PART 7: REPLACE
# ============================================

navigate({ to: '/dashboard', replace: true })
# ============================================
# PART 8: RELATIVE
# ============================================

navigate({ from: '/posts/$postId', to: '..' })
# ============================================
# PART 9: ROUTER INVALIDATE
# ============================================

const router = useRouter()
router.invalidate()

The nine parts trace the imperative navigation: basic, typed path, params, search, state, read state, replace, relative, and router invalidate.


Quick Reference

useNavigate Options

OptionPurpose
toTyped route path
paramsDynamic segment values
searchTyped search params
hashURL hash
stateHistory state
replaceReplace history entry
resetScrollScroll to top
fromSource route for relative

Hook Form

FormExample
Absolutenavigate({ to: '/posts' })
Dynamicnavigate({ to: '/posts/$postId', params: { postId } })
Searchnavigate({ to: '/posts', search: { page: 2 } })
Relativenavigate({ from: '/posts/$postId', to: '..' })

Other Hooks

HookReturns
useNavigateNavigate function
useRouterRouter instance
useLocationCurrent location
useParamsRoute params

Best Practices

✅ Do This:

navigate({ to: '/posts/$postId', params: { postId: '123' } })
<!-- Provide the params for the dynamic segment -->    # ✅
navigate({ to: '/dashboard', replace: true })
<!-- Replace the history entry after login -->        # ✅
const router = useRouter()
router.invalidate()
<!-- Invalidate the route data -->                   # ✅

❌ Don’t Do This:

navigate('/dashboard')
<!-- String form is not supported -->                # ❌
navigate({ to: '/posts/$postId' })
<!-- Missing params for the dynamic segment -->      # ❌
navigate({ to: '/login' })
# After login
<!-- The back button returns to login -->            # ❌

Common Pitfalls

PitfallWhy It HappensFix
String form failsnavigate requires objectUse { to: ... }
Type error on paramsMissing dynamic segmentAdd params
Search type errorWrong typeCheck validateSearch
Back to loginUsed push{ replace: true }
State lost on refreshState is in-memoryUse URL param

Real-World Examples

1. Basic Navigate

navigate({ to: '/dashboard' })

2. Dynamic Params

navigate({ to: '/posts/$postId', params: { postId: '1' } })

3. Search Params

navigate({ to: '/posts', search: { page: 2 } })

4. State

navigate({ to: '/checkout', state: { cartId } })

5. Replace

navigate({ to: '/dashboard', replace: true })

6. Relative

navigate({ from: '/posts/$postId', to: '..' })

7. Router Invalidate

router.invalidate()

8. Router Navigate

router.navigate({ to: '/dashboard' })

9. Logout

await logout(); navigate({ to: '/login', replace: true })

10. After Form

await save(); navigate({ to: '/list' })

Visual

Typed Navigate

┌──────────────────────────────────────────────┐
│  Route tree                                  │
│  ├── /posts                                  │
│  └── /posts/$postId                          │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  navigate({ to: '/posts/$postId', params })  │
│  → TypeScript checks the path and params     │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Router navigates                            │
└──────────────────────────────────────────────┘

Push vs Replace

┌──────────────────────────────────────────────┐
│  navigate({ to: '/dashboard' })              │
│  Stack: [home, login, dashboard]             │
│  Back → /login                               │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  navigate({ to: '/dashboard', replace: true })│
│  Stack: [home, dashboard]                    │
│  Back → /home                                │
└──────────────────────────────────────────────┘

Summary

ItemValue
HookuseNavigate()
Function formnavigate({ to: ... })
Dynamic params{ to, params: { ... } }
Search params{ to, search: { ... } }
Replace{ replace: true }
State{ state: { ... } }
Relative{ from, to }
Router invalidaterouter.invalidate()

Key takeaways:

  • The navigate function accepts the options object, not a string. The navigate({ to: '/dashboard' }) is the correct form. The navigate("/dashboard") is a type error.
  • The to prop is a typed route path. The TypeScript compiler verifies that the path exists in the route tree. The invalid path is a compile error.
  • The params prop is required for the dynamic segments. The /posts/$postId path without the params is a compile error. The params: { postId } is the correct form.
  • The replace: true replaces the history entry. The user cannot press the back button to return to the previous page. The login redirect is the common use case.
  • The state is the in-memory data. The useLocation hook reads it. The state is lost on the page refresh. The URL parameter is the alternative for the data that must survive.

Remember: The useNavigate is the imperative navigation. The options object is the form. The to is the typed path. The params is the dynamic. The search is the query. The replace is the history. The state is the in-memory. The exam tests the typed form and the replace option.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!