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
useNavigatereturns a function: Callconst navigate = useNavigate(), thennavigate('/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) ornavigate('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./profilefrom 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:
- User fills form and clicks "Login".
handleSubmitvalidates input and sends API request.- On success, token is saved and
navigate('/dashboard')is called. - React Router renders the Dashboard component.
- On error,
setErrordisplays a message; no navigation occurs.
Navigating with State: Passing Data Between Routes
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:
ProtectedRoutewraps components that require authentication. If not authenticated, it redirects via theNavigatecomponent (compile-time redirect).LoginPageusesuseNavigate()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
What's the difference between navigate('/path') and <Link to="/path">?
<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()}`).