ReactProgrammatic Navigation

Programmatic Navigation

Link and NavLink handle navigation triggered by user clicks on anchors. Programmatic navigation handles the cases where you need to navigate in code — after a form submission, after a successful API call, after a logout, or when redirecting back to the intended destination after login.

useNavigate

useNavigate() returns a navigate function. Call it with a path string to push a new entry onto the browser's history stack:

TSX
import { useNavigate } from 'react-router-dom'

function SearchBar() {
  const navigate = useNavigate()
  const [query, setQuery] = React.useState('')

  const handleSearch = (e: React.FormEvent) => {
    e.preventDefault()
    if (query.trim()) {
      navigate(`/search?q=${encodeURIComponent(query)}`)
    }
  }

  return (
    <form onSubmit={handleSearch}>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search…"
      />
      <button type="submit">Search</button>
    </form>
  )
}
Navigate After Form Submit

The most common use of useNavigate is redirecting after a successful form submission — creating a new post, completing checkout, or saving settings:

TSX
import { useNavigate } from 'react-router-dom'

function NewPostForm() {
  const navigate = useNavigate()
  const [title, setTitle] = React.useState('')
  const [body, setBody] = React.useState('')
  const [isSubmitting, setIsSubmitting] = React.useState(false)

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    setIsSubmitting(true)

    try {
      const res = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ title, body }),
      })
      if (!res.ok) throw new Error('Failed to create post')
      const post = await res.json()

      // Navigate to the new post's detail page
      navigate(`/posts/${post.id}`)
    } catch (err) {
      console.error(err)
      setIsSubmitting(false)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" />
      <textarea value={body} onChange={(e) => setBody(e.target.value)} placeholder="Body" />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Publishing…' : 'Publish Post'}
      </button>
    </form>
  )
}
Going Back with navigate(-1)

Pass a number to navigate to move through browser history. -1 goes back, 1 goes forward, -2 goes back two entries:

TSX
function BackButton() {
  const navigate = useNavigate()
  return (
    <button onClick={() => navigate(-1)}>
      ← Back
    </button>
  )
}

// More specific: go back two steps
function CloseWizard() {
  const navigate = useNavigate()
  return <button onClick={() => navigate(-2)}>Cancel Wizard</button>
}
Note
navigate(-1) is equivalent to window.history.back() but keeps React Router in sync. Use it for "Back" buttons rather than a hardcoded path — it respects the actual navigation history.
replace: true — No History Entry

Pass { replace: true } as the second argument to replace the current history entry instead of pushing a new one. The user cannot navigate back to the replaced page:

TSX
// After login, replace the login page so the Back button doesn't return to it
navigate('/dashboard', { replace: true })

// After a successful password reset, replace the reset form
navigate('/login?success=true', { replace: true })

// Redirect from an old URL to a new one
function OldPostPage() {
  const { id } = useParams()
  return <Navigate to={`/blog/${id}`} replace />
}
Warning
Without replace: true, navigating after login leaves the login page in history. The user can press Back and end up on the login page while still authenticated — confusing and broken. Always use replace: true for auth redirects.
Passing State with navigate

The second argument also accepts a state property. State is passed in the location object and survives the navigation — use it to carry data that doesn't belong in the URL:

TSX
import { useNavigate, useLocation } from 'react-router-dom'

// Page A: navigate with state
function CheckoutPage() {
  const navigate = useNavigate()
  const location = useLocation()

  const handlePurchase = async () => {
    const order = await submitOrder()
    navigate('/order-confirmation', {
      state: {
        orderId: order.id,
        total: order.total,
        from: location.pathname,
      },
    })
  }

  return <button onClick={handlePurchase}>Complete Purchase</button>
}

// Page B: read navigation state
function OrderConfirmation() {
  const location = useLocation()
  const { orderId, total } = location.state ?? {}

  if (!orderId) {
    // State is gone if the user refreshes — handle gracefully
    return <p>No order data found. Check your email for confirmation.</p>
  }

  return (
    <div>
      <h1>Order Confirmed!</h1>
      <p>Order #{orderId}</p>
      <p>Total: ${total?.toFixed(2)}</p>
    </div>
  )
}
Tip
Navigation state is NOT persisted across page refreshes. If the confirmation page is refreshed, location.state will be null. Always provide a fallback UI when state is absent.
useLocation — Reading the Current Location

useLocation returns the current location object with pathname, search, hash, and state. It re-renders your component whenever the location changes:

TSX
import { useLocation } from 'react-router-dom'

function PageTracker() {
  const location = useLocation()

  React.useEffect(() => {
    // Track page views on every navigation
    analytics.track('page_view', {
      path: location.pathname,
      search: location.search,
    })
  }, [location])

  return null  // renders nothing, just tracks
}

// Reading path parts:
function Breadcrumb() {
  const location = useLocation()
  const segments = location.pathname.split('/').filter(Boolean)

  return (
    <nav>
      {segments.map((seg, i) => (
        <span key={i}>{seg} / </span>
      ))}
    </nav>
  )
}
Login Redirect — Navigate Back to Origin

The classic pattern: a user tries to visit a protected page, is redirected to login, and after a successful login is sent back to where they were trying to go. Store the intended destination in location state:

TSX
import { useNavigate, useLocation, Navigate } from 'react-router-dom'

// ProtectedRoute stores the attempted URL in state
function ProtectedRoute({ children }) {
  const { isLoggedIn } = useAuth()
  const location = useLocation()

  if (!isLoggedIn) {
    return (
      <Navigate
        to="/login"
        state={{ from: location.pathname + location.search }}
        replace
      />
    )
  }
  return children
}

// Login page reads state.from and redirects there on success
function LoginPage() {
  const navigate = useNavigate()
  const location = useLocation()
  const from = location.state?.from ?? '/dashboard'

  const handleLogin = async (credentials) => {
    await login(credentials)
    // Go back to the page they were trying to reach
    navigate(from, { replace: true })
  }

  return <LoginForm onSubmit={handleLogin} />
}
Navigate Options Summary
  • navigate("/path") — push new history entry, user can press Back.

  • navigate("/path", { replace: true }) — replace current entry, no Back entry.

  • navigate(-1) — go back one step in history.

  • navigate(1) — go forward one step in history.

  • navigate("/path", { state: { key: value } }) — pass data to the destination page.

  • useLocation().state — read state on the destination page.

  • useLocation().pathname — current path, useful for analytics and breadcrumbs.