Skip to main content

React Router: Programmatic Navigation with useNavigate

The useNavigate hook lets you navigate programmatically—navigate in response to events, not just user clicks on links. After a user submits a login form, validates input, or completes a transaction, you call navigate('/path') to redirect them. This is essential for building dynamic user flows like authenticated redirects, multi-step forms, and conditional navigation based on app state.

Key Takeaways

  • useNavigate returns a function: Call const navigate = useNavigate(), then navigate('/path') to change routes
  • Programmatic navigation follows events: Navigate after form submission, API calls, permission checks, or any logic—not just link clicks
  • Navigate with relative paths or absolute paths: navigate('/dashboard') (absolute) or navigate('profile') (relative) both work
  • Use navigate(-1) to go back: Equivalent to the browser's back button; navigate(-2) goes back two steps
  • Pass state through navigation: navigate('/next', { state: { message: 'Success!' } }) passes data to the next component
  • Conditional navigation based on auth: Redirect unauthenticated users to login; redirect authenticated users away from login form

Why Programmatic Navigation Matters

The <Link> and <NavLink> components handle user-initiated navigation (clicking). But many flows require navigation triggered by code logic:

  • After form submission: Redirect to success page only if validation passes
  • After authentication: Log in succeeds → redirect to dashboard
  • Permission checks: User lacks permission → redirect to error or login
  • Multi-step flows: Step 1 → validate → step 2 → validate → confirmation page
  • Redirects: Old URL pattern → redirect to new path

The useNavigate hook is the tool for all these scenarios.

How Do You Use useNavigate?

The useNavigate hook is simple: call it to get a navigate function, then call navigate() with a path.

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

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

const handleNavigate = () => {
navigate('/about'); // Go to /about
};

return <button onClick={handleNavigate}>Go to About</button>;
}

Key points:

  • useNavigate() returns a function (not the path itself).
  • navigate('/path') changes the current route and renders the matched component.
  • The component tree updates; no page reload.
  • Works with relative paths: navigate('profile') goes to ./profile from the current location.

Real Example: Login Form with Programmatic Redirect

Here's a practical example: a login form that validates input and navigates to a dashboard on success.

// LoginForm.jsx
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';

function LoginForm() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();

const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);

try {
// Simulate API call
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});

if (!response.ok) {
throw new Error('Invalid credentials');
}

const data = await response.json();
// Save token (in real app, use secure storage)
localStorage.setItem('authToken', data.token);

// Programmatically navigate to dashboard on success
navigate('/dashboard');
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

return (
<form onSubmit={handleSubmit}>
<h2>Login</h2>
{error && <p style={{ color: 'red' }}>{error}</p>}
<label>
Username:
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</label>
<label>
Password:
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</label>
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
);
}

export default LoginForm;

Flow:

  1. User fills form and clicks "Login".
  2. handleSubmit validates input and sends API request.
  3. On success, token is saved and navigate('/dashboard') is called.
  4. React Router renders the Dashboard component.
  5. On error, setError displays a message; no navigation occurs.

You can pass state through navigation using the second argument to navigate().

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

function SuccessPage() {
const navigate = useNavigate();
const location = useLocation();

// location.state contains data passed from navigate()
const message = location.state?.message || 'No message';

return (
<div>
<h1>Success!</h1>
<p>{message}</p>
<button onClick={() => navigate('/home')}>Go Home</button>
</div>
);
}

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

const handleSubmit = (e) => {
e.preventDefault();
// Perform action...
// Pass state to the next component
navigate('/success', { state: { message: 'Your data was saved!' } });
};

return (
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
);
}

Key points:

  • navigate(path, { state: { ... } }) sends data.
  • The destination component reads it with const location = useLocation(); location.state.
  • This is useful for confirmation messages, data references, or UI state.
  • State is NOT persisted in URL (unlike query params); refreshing the page loses it.

Going Back and Forward: Relative Navigation

navigate() accepts relative numbers for browser history.

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

return (
<div>
<h1>Details</h1>
<button onClick={() => navigate(-1)}>← Back</button>
<button onClick={() => navigate(-2)}>← Back 2 Steps</button>
<button onClick={() => navigate(1)}>Forward →</button>
</div>
);
}

navigate(-1) is equivalent to clicking the browser's back button. Use this for "Cancel" buttons in forms or after completing a task.

Advanced Example: Authentication Guard with Navigation

A common pattern: redirect unauthenticated users to login, authenticated users away from the login page.

// ProtectedRoute.jsx
import { Navigate } from 'react-router-dom';
import { useAuth } from './AuthContext';

function ProtectedRoute({ children }) {
const { isAuthenticated } = useAuth();

if (!isAuthenticated) {
// Programmatically render a redirect (via Navigate component)
return <Navigate to="/login" replace />;
}

return children;
}

// LoginPage.jsx
import { useNavigate } from 'react-router-dom';
import { useAuth } from './AuthContext';
import { useEffect } from 'react';

function LoginPage() {
const navigate = useNavigate();
const { isAuthenticated } = useAuth();

useEffect(() => {
// If already logged in, redirect to dashboard
if (isAuthenticated) {
navigate('/dashboard', { replace: true });
}
}, [isAuthenticated, navigate]);

return (
<form onSubmit={(e) => {
e.preventDefault();
// Login logic...
navigate('/dashboard');
}}>
{/* Form fields */}
</form>
);
}

// App.jsx
import { Routes, Route } from 'react-router-dom';

function App() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
</Routes>
);
}

Pattern:

  • ProtectedRoute wraps components that require authentication. If not authenticated, it redirects via the Navigate component (compile-time redirect).
  • LoginPage uses useNavigate() for runtime redirects after user interaction.
  • Both protect the routing logic; combining them covers most auth scenarios.

Complete Multi-Step Form Example

Here's a realistic multi-step form with navigation.

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

function MultiStepForm() {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({ name: '', email: '', phone: '' });
const navigate = useNavigate();

const handleNext = () => {
// Validate current step
if (step === 1 && !formData.name) {
alert('Name is required');
return;
}
if (step === 2 && !formData.email) {
alert('Email is required');
return;
}

if (step < 3) {
setStep(step + 1);
} else {
// All steps done, submit and navigate
submitForm();
}
};

const handleBack = () => {
if (step > 1) {
setStep(step - 1);
}
};

const submitForm = async () => {
// Send formData to API
const response = await fetch('/api/register', {
method: 'POST',
body: JSON.stringify(formData),
});

if (response.ok) {
navigate('/confirmation', { state: { formData } });
}
};

return (
<div>
<h2>Step {step} of 3</h2>
{step === 1 && (
<input
placeholder="Name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
)}
{step === 2 && (
<input
placeholder="Email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
)}
{step === 3 && (
<input
placeholder="Phone"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
/>
)}
<button onClick={handleBack} disabled={step === 1}>Back</button>
<button onClick={handleNext}>{step === 3 ? 'Submit' : 'Next'}</button>
</div>
);
}

This pattern combines local state (for multi-step data) with programmatic navigation (final submission redirects to confirmation).

Frequently Asked Questions

<Link> is declarative (rendered in JSX); navigate() is imperative (called in event handlers or effects). Use <Link> for user-initiated navigation (menu links); use navigate() for programmatic redirection (form submission, auth checks, validation).

Does navigate('/path') replace the history entry or add a new one?

By default, navigate('/path') adds a new entry to browser history. Use navigate('/path', { replace: true }) to replace the current entry (useful for login/logout to prevent back-button returning to login).

Can you navigate to a path that doesn't exist?

React Router will try to match it. If no route matches, either the <Route path="*" /> catch-all renders, or nothing renders (blank page). Define a 404 route to handle this gracefully.

What happens if you call navigate() during render?

React will warn: "state update during render." Always call navigate() in event handlers or useEffect, never directly in the component body. This prevents infinite re-render loops.

Can you navigate to a route with query params using useNavigate?

Yes: navigate('/search?query=react') or navigate({ pathname: '/search', search: '?query=react' }). For cleaner code, use URLSearchParams: navigate(/search?${new URLSearchParams({ query: 'react' }).toString()}`).

Further Reading