Programmatic Navigation

So far, navigation has probably meant clicking a Link. But plenty of navigation isn’t triggered by a click on a link at all — it happens because a form was submitted, a login succeeded, a timer expired, or a user isn’t authorized to view a page. That’s programmatic navigation: changing the URL from inside your JavaScript code rather than from markup. React Router v6+ gives you two tools for this: the useNavigate hook for imperative navigation, and the Navigate component for declarative redirects during render.

Overview / How it works

Every route-related React Router feature relies on a router (usually BrowserRouter) that keeps the browser’s history stack in sync with React state. When the URL changes — whether from a click, a back-button press, or your own code — the router re-evaluates which route matches and re-renders the corresponding element. Link triggers this by rendering an anchor tag whose click handler calls into that history object instead of doing a full page load. useNavigate gives you a function that does the exact same thing, but you call it yourself, whenever you decide navigation should happen.

useNavigate() returns a stable function, commonly named navigate, that you call with either a path string or a number. Passing a path (navigate("/dashboard")) pushes a new entry onto the history stack, just like clicking a link to that path — the current page is added to history, and the browser’s Back button will return to it. Passing a number (navigate(-1), navigate(1)) moves relative to the current position in history, exactly like clicking the browser’s Back or Forward buttons.

Because useNavigate is a hook, it follows the Rules of Hooks: it must be called at the top level of a function component (or a custom hook), unconditionally, on every render, in the same order. You then invoke the function it returns conditionally or inside event handlers — that part is normal JavaScript and is completely fine. What you can never do is call useNavigate() itself inside an if, a loop, or after an early return.

There’s a second, purely declarative way to redirect: the Navigate component. Instead of calling a function, you render <Navigate to="/login" /> as JSX. React Router treats rendering this element as an instruction to redirect, and it performs the navigation as a side effect during commit, not during render itself. This is the preferred approach when a redirect is a direct consequence of what’s being rendered — for example, blocking an unauthenticated user from a protected page — rather than the result of an event like a click.

Syntax

import { useNavigate, useLocation, Navigate } from "react-router-dom";

const navigate = useNavigate();
navigate(to, { replace: false, state: undefined });
navigate(delta); // e.g. navigate(-1)

<Navigate to="/path" replace state={{ from: location.pathname }} />
API What it does
useNavigate() Returns a function you call to navigate imperatively from event handlers or effects.
navigate(path) Pushes a new history entry and navigates to path.
navigate(path, { replace: true }) Navigates to path but replaces the current history entry instead of adding a new one.
navigate(path, { state }) Attaches arbitrary data to the new history entry, readable on the next page via useLocation().state.
navigate(-1) / navigate(1) Moves backward or forward in the history stack, like the browser’s Back/Forward buttons.
useLocation() Returns the current location object, including pathname and any state passed by navigate.
<Navigate to="/path" /> A component that redirects as soon as it’s rendered — used for conditional, render-time redirects.

Examples

Example 1: Navigating on a button click

import { useNavigate } from "react-router-dom";

function LoginButton() {
  const navigate = useNavigate();

  function handleLogin() {
    // pretend this runs after a successful login API call
    navigate("/dashboard");
  }

  return ;
}

export default LoginButton;

Output:

Renders a button reading "Log In". Clicking it calls navigate("/dashboard"),
which updates the URL to /dashboard and renders the route element registered
for that path, with no full page reload.

This is the most common case: navigation triggered by an event handler. useNavigate is called once, at the top of the component, and the returned navigate function is invoked only when the button is actually clicked.

Example 2: Redirecting after a form submission, with replace and state

import { useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";

function LoginPage() {
  const [username, setUsername] = useState("");
  const navigate = useNavigate();
  const location = useLocation();

  const from = location.state?.from ?? "/dashboard";

  function handleSubmit(e) {
    e.preventDefault();
    // pretend this succeeds
    navigate(from, { replace: true });
  }

  return (
    
setUsername(e.target.value)} />
); } export default LoginPage;

Output:

Renders a labeled text input and a "Log In" button. Submitting the form
prevents the default full-page POST, then navigates to location.state.from
(e.g. "/settings" if the user was sent here from a protected route), or
"/dashboard" if no state was provided. Because replace: true is used, the
login page is not left in history, so the browser Back button afterward
does not return to the login form.

Two details matter here. First, e.preventDefault() stops the browser’s native form submission, which would otherwise reload the page. Second, { replace: true } swaps the current history entry instead of pushing a new one — exactly what you want after a login, so pressing Back doesn’t take the user right back to a form they already submitted.

Example 3: A declarative redirect and a back button

import { Navigate, useLocation, useNavigate } from "react-router-dom";

function ProtectedRoute({ isAuthenticated, children }) {
  const location = useLocation();

  if (!isAuthenticated) {
    return ;
  }

  return children;
}

function BackButton() {
  const navigate = useNavigate();
  return ;
}

export { ProtectedRoute, BackButton };

Output:

ProtectedRoute renders its children only when isAuthenticated is true;
otherwise it renders a Navigate element, which immediately redirects to
/login and carries the original path in route state. BackButton renders a
button reading "Back" that, when clicked, moves one entry back in session
history, the same as the browser's own Back button.

ProtectedRoute is typically used to wrap a route’s element in your router config: <Route path="/settings" element={<ProtectedRoute isAuthenticated={isAuthenticated}><Settings /></ProtectedRoute>} />. Because the redirect happens by rendering Navigate rather than by calling navigate() directly during render, React Router can perform it safely as a commit-phase effect.

How it works step by step

  • On call: when you invoke navigate(path), React Router updates its internal history object, which is backed by the browser’s history API (pushState or replaceState). This changes window.location without a network request.
  • On history change: the router’s top-level state updates to reflect the new location, which triggers a re-render of the router tree.
  • On re-render: React Router matches the new path against your route configuration and renders whichever element corresponds to the best match, swapping out the previous route’s element.
  • With Navigate: rendering <Navigate> doesn’t navigate immediately during render — React first finishes rendering, commits it, and then, as an effect, calls the equivalent of navigate(), which restarts the cycle above.
  • On unmount: if the previous route’s component had effects with cleanup (subscriptions, timers, event listeners), React runs those cleanup functions before the new route’s component mounts, exactly as with any other unmount.

Common Mistakes

1. Calling useNavigate conditionally. Hooks must run unconditionally, every render, in the same order.

function LogoutButton({ show }) {
  if (show) {
    const navigate = useNavigate(); // hook called conditionally - breaks Rules of Hooks
  }
  return ;
}

Fix: call the hook unconditionally at the top, and apply the condition to how you use the returned function, not to the hook call itself.

function LogoutButton({ show }) {
  const navigate = useNavigate();
  if (!show) return null;
  return ;
}

2. Calling navigate() directly in the component body instead of an effect or handler. This runs on every single render, which can trigger an infinite render loop or a “Cannot update a component while rendering a different component” warning.

function Redirector() {
  const navigate = useNavigate();
  navigate("/home"); // called during render - runs every render
  return 

Redirecting...

; }

Fix: for a render-time redirect, render <Navigate> instead; it’s designed for exactly this. If you truly need imperative logic, wrap the call in useEffect so it only runs after render, in response to specific dependency changes.

function Redirector() {
  return ;
}

3. Forgetting that navigate only understands in-app routes. Passing a full external URL like navigate("https://example.com") won’t leave your app the way a real link would — React Router treats it as an internal path. For external destinations, use a plain <a href> or set window.location.href directly.

Best Practices

  • Use Link for anything the user can click to navigate; reserve useNavigate for navigation that results from logic — form submissions, async success/error handling, timeouts.
  • Use { replace: true } whenever the current page shouldn’t remain in history — login forms, wizards, and any redirect that stands in for a page the user never really meant to visit.
  • Prefer the declarative <Navigate> component over calling navigate() in the render body; it keeps redirects as a render output React can commit safely, not a side effect during render.
  • Pass only small, serializable-ish data through state (like a redirect-from path or a success message) — it’s kept in memory tied to the history entry, not persisted like URL params or query strings.
  • Always read location.state defensively with optional chaining (location.state?.from), since a user can land on a page directly, with no state attached at all.
  • Keep the call to useNavigate() itself unconditional, at the top of the component, even if you only use the returned function inside a condition.

Practice Exercises

  • Build a SignupForm that, on successful submit, calls navigate("/welcome", { replace: true }) and also verify that pressing Back afterward does not return to the signup form.
  • Create a ProductPage with a “Go back to results” button that uses navigate(-1), and a fallback “Back to home” Link for when there’s no previous page in history.
  • Extend the ProtectedRoute example so that after a successful login on /login, the user is sent back to location.state.from rather than always to /dashboard.

Summary

  • useNavigate() returns a function for navigating imperatively from event handlers and effects.
  • navigate(path) pushes a new history entry; navigate(path, { replace: true }) replaces the current one.
  • navigate(-1) / navigate(1) move backward or forward through history, like the browser’s own buttons.
  • navigate(path, { state }) attaches data readable via useLocation().state on the destination page.
  • The Navigate component performs a redirect declaratively as a result of rendering — use it for conditional, render-driven redirects like auth checks.
  • useNavigate is a hook and must be called unconditionally at the top level; only the function it returns can be used conditionally.