React 61 ⚛️ Programmatic Navigation
A link is a declaration: the user clicks, and the router navigates. But not every navigation is a click. A form submits, a login succeeds, an OAuth callback lands, a timer fires, and the application must move the user to a different route without a link in sight. Programmatic navigation is the imperative side of the router, and React Router exposes it through the useNavigate hook. The hook returns a function that changes the location, and the function accepts a path, a delta, or a location object, with options that control the history stack and the state that travels with the navigation.
Key point: useNavigate returns a function that must be called from an event handler, an effect, or another callback. Calling it during render is a bug. The function is stable across renders, so it can be stored in a variable or passed to a child without causing re-renders.
Why programmatic navigation exists
The event problem. A navigation is often the result of an event that is not a link click. A form submits successfully, and the application must redirect to the dashboard. A button triggers a logout, and the user must be sent to the login page. The useNavigate hook provides the imperative call.
The condition problem. The destination depends on the outcome. A login form submits, the server returns the user’s role, and the application navigates to the admin panel or the user dashboard. The branch is in the code, not in the markup.
The redirect problem. A protected route renders, the authentication check fails, and the component must redirect to the login page. The <Navigate> component is the declarative form, and the useNavigate hook is the imperative form, and both are used in different contexts.
The history problem. The back button, the forward button, and the browser history are part of the navigation model. The navigate(-1) call moves the user back, and the replace: true option replaces the current entry instead of adding a new one.
The state problem. A navigation can carry state that is not in the URL. The login form passes the user’s intended destination, and the login page reads it after the authentication. The state option carries the data, and the useLocation hook reads it.
The callback problem. An OAuth provider redirects the user to a callback URL, and the callback component must parse the token and navigate to the application. The navigation is the result of the callback, not a user click.
The timer problem. A session expires after a period of inactivity, and the application must navigate to the login page. The timer fires, and the navigation happens without user interaction.
a. The useNavigate hook
The useNavigate hook returns the navigate function. The function is called with a destination and an optional options object.
The basic usage
import { useNavigate } from 'react-router-dom';
function LoginButton() {
const navigate = useNavigate();
function handleClick() {
navigate('/dashboard');
}
return <button onClick={handleClick}>Log in</button>;
}
The navigate function changes the route to /dashboard. The current history entry is replaced by the new one.
The path
The first argument is the destination. It can be a string path or a location object.
navigate('/dashboard');
navigate('/users/42');
navigate('/users/42?tab=profile');
The string is parsed and matched against the route configuration.
The location object
The destination can be a location object with a pathname, a search, and a hash.
navigate({
pathname: '/dashboard',
search: '?tab=profile',
hash: '#section',
});
The object form is more explicit, and it is the form the useLocation hook returns.
The delta
The argument can be a number, which moves the user backward or forward in the history.
navigate(-1); // go back
navigate(1); // go forward
navigate(-2); // go back two entries
The number is the delta. The -1 is the same as the back button.
The replace option
The replace option replaces the current entry instead of adding a new one.
navigate('/login', { replace: true });
After the navigation, the back button returns to the page before the current one, not to the page that issued the navigation. The option is used after a login, a logout, or a redirect, where returning to the previous page is not wanted.
The state option
The state option carries data that is not in the URL.
navigate('/login', { state: { from: '/settings' } });
The state is stored in the history entry, and the useLocation hook reads it in the destination.
function LoginPage() {
const location = useLocation();
const from = location.state?.from ?? '/';
function handleLogin() {
// Authenticate, then navigate to the original destination.
navigate(from, { replace: true });
}
return <button onClick={handleLogin}>Log in</button>;
}
The state is the mechanism for the “return to the original page after login” pattern.
The relative navigation
The destination can be relative to the current route.
navigate('settings'); // relative to the current path
navigate('../settings'); // up one level, then settings
navigate('./settings'); // explicit current level
The relative form is useful inside a nested route.
The -1 delta and the state
The navigate(-1) call does not carry state. It returns to the previous entry exactly as it was, including its state.
b. The common patterns
The useNavigate hook is used in a small set of patterns. The patterns are the reason the hook exists.
The login redirect
A login form navigates to the dashboard after a successful authentication.
function LoginForm() {
const navigate = useNavigate();
const [email, setEmail] = useState('');
async function handleSubmit(e) {
e.preventDefault();
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email }),
});
if (response.ok) {
navigate('/dashboard', { replace: true });
}
}
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<button type="submit">Log in</button>
</form>
);
}
The replace: true prevents the user from returning to the login form with the back button.
The logout redirect
A logout button navigates to the login page after clearing the session.
function LogoutButton() {
const navigate = useNavigate();
function handleLogout() {
localStorage.removeItem('token');
navigate('/login', { replace: true });
}
return <button onClick={handleLogout}>Log out</button>;
}
The protected route
A component checks the authentication and redirects if the check fails.
function ProtectedRoute({ children }) {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
The <Navigate> component is the declarative form. The useNavigate hook is the imperative form. The <Navigate> is used inside a render, and the useNavigate is used inside a handler.
The form submit
A form submits to a server, and the result determines the destination.
function CreatePostForm() {
const navigate = useNavigate();
async function handleSubmit(e) {
e.preventDefault();
const data = new FormData(e.target);
const post = await createPost(data);
navigate(`/posts/${post.id}`);
}
return <form onSubmit={handleSubmit}>...</form>;
}
The destination is the new resource’s URL.
The post-login return
The login page navigates to the original destination after a successful authentication.
function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from?.pathname ?? '/dashboard';
async function handleLogin(credentials) {
await login(credentials);
navigate(from, { replace: true });
}
return <LoginForm onSubmit={handleLogin} />;
}
The location.state.from is the original location, and the replace: true prevents the user from returning to the login page.
The OAuth callback
The OAuth callback component parses the token and navigates.
function OAuthCallback() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
useEffect(() => {
const token = searchParams.get('token');
if (token) {
localStorage.setItem('token', token);
navigate('/dashboard', { replace: true });
} else {
navigate('/login', { replace: true });
}
}, [navigate, searchParams]);
return <p>Authenticating...</p>;
}
The effect runs once, and the navigation happens after the token is stored.
The conditional redirect
A component redirects based on a condition.
function Dashboard() {
const { user } = useAuth();
const navigate = useNavigate();
useEffect(() => {
if (!user) {
navigate('/login', { replace: true });
}
}, [user, navigate]);
if (!user) return null;
return <div>Welcome, {user.name}</div>;
}
The effect runs when the user changes, and the navigation happens if the user is not authenticated.
The wizard
A wizard navigates to the next step after the current step is complete.
function WizardStep({ step }) {
const navigate = useNavigate();
function handleNext() {
navigate(`/wizard/${step + 1}`);
}
function handleBack() {
navigate(`/wizard/${step - 1}`);
}
return (
<div>
<button onClick={handleBack} disabled={step === 1}>Back</button>
<button onClick={handleNext}>Next</button>
</div>
);
}
The search form
A search form navigates to the results page with the query string.
function SearchForm() {
const navigate = useNavigate();
const [query, setQuery] = useState('');
function handleSubmit(e) {
e.preventDefault();
navigate(`/search?q=${encodeURIComponent(query)}`);
}
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button type="submit">Search</button>
</form>
);
}
The query is encoded and passed in the search string.
The session timeout
A timer navigates to the login page after a period of inactivity.
function SessionTimeout({ timeout = 30 * 60 * 1000 }) {
const navigate = useNavigate();
useEffect(() => {
const timer = setTimeout(() => {
navigate('/login', { replace: true });
}, timeout);
return () => clearTimeout(timer);
}, [navigate, timeout]);
return null;
}
c. The caveats and the common mistakes
The useNavigate hook has specific rules. The rules prevent the subtle bugs.
The render rule
The navigate function must not be called during render. The call is a side effect, and the render must be pure.
// ❌ Wrong: navigate during render
function Component() {
const navigate = useNavigate();
navigate('/dashboard');
return <div>...</div>;
}
// ✅ Correct: navigate in an effect
function Component() {
const navigate = useNavigate();
useEffect(() => {
navigate('/dashboard');
}, [navigate]);
return <div>...</div>;
}
The effect runs after the render, and the navigation is the side effect.
The <Navigate> alternative
The <Navigate> component is the declarative form of the same operation. It is used inside a render, and the navigation happens when the component renders.
function ProtectedRoute({ children }) {
const { user } = useAuth();
if (!user) return <Navigate to="/login" replace />;
return children;
}
The two forms are equivalent. The <Navigate> is for the render-time redirect, and the useNavigate is for the event-handler redirect.
The stale closure
The navigate function is stable, but the values it captures are not. The useEffect dependency array must include the values the effect uses.
useEffect(() => {
if (!user) navigate('/login', { replace: true });
}, [user, navigate]);
The user is in the dependency array. The effect re-runs when the user changes.
The double navigation
A component that navigates in both the render and the effect navigates twice. The render-time navigation is the bug.
// ❌ Wrong: navigates twice
function Component() {
const navigate = useNavigate();
if (!user) navigate('/login'); // Render
useEffect(() => {
if (!user) navigate('/login'); // Effect
}, [user, navigate]);
}
The relative navigation
The relative navigation depends on the current route. The navigate('settings') inside /users/42 goes to /users/settings, not /settings.
// From /users/42
navigate('settings'); // /users/settings
navigate('../settings'); // /settings
navigate('/settings'); // /settings (absolute)
The leading slash makes the path absolute.
The state is not in the URL
The state option is not visible in the URL. The state is lost when the page is refreshed, and the state is not shareable.
navigate('/login', { state: { from: '/settings' } });
// The URL is /login, not /login?from=/settings
The state is the mechanism for the transient data, and the query string is the mechanism for the persistent data.
The replace vs the push
The replace: true replaces the current entry, and the default is the push. The replace is the right choice for a redirect, a login, a logout, and a 404. The push is the right choice for a link.
// Replace: the back button does not return here.
navigate('/login', { replace: true });
// Push: the back button returns here.
navigate('/dashboard');
The useNavigate in a class component
A class component cannot use the hook. The withRouter HOC or the <Navigate> component is the alternative, and the functional component is the recommended approach.
The caveat table
| Caveat | Rule |
|---|---|
| Render | Do not call navigate during render |
| Effect | Use an effect for the render-time redirect |
<Navigate> | The declarative alternative |
| Dependencies | Include the values in the dependency array |
| Relative | The leading slash makes the path absolute |
| State | Not in the URL, not shareable |
| Replace | Use for redirects, not for links |
Complete Example Session
// ============================================
// PART 1: THE BASIC NAVIGATE
// ============================================
import { useNavigate } from 'react-router-dom';
function LoginButton() {
const navigate = useNavigate();
return <button onClick={() => navigate('/dashboard')}>Log in</button>;
}
// ============================================
// PART 2: THE REPLACE OPTION
// ============================================
function LogoutButton() {
const navigate = useNavigate();
function handleLogout() {
localStorage.removeItem('token');
navigate('/login', { replace: true });
}
return <button onClick={handleLogout}>Log out</button>;
}
// ============================================
// PART 3: THE STATE OPTION
// ============================================
function SettingsPage() {
const navigate = useNavigate();
function handleLogin() {
navigate('/login', { state: { from: '/settings' } });
}
return <button onClick={handleLogin}>Log in to edit</button>;
}
function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from ?? '/';
function handleSuccess() {
navigate(from, { replace: true });
}
return <button onClick={handleSuccess}>Log in</button>;
}
// ============================================
// PART 4: THE DELTA
// ============================================
function BackButton() {
const navigate = useNavigate();
return <button onClick={() => navigate(-1)}>Back</button>;
}
// ============================================
// PART 5: THE PROTECTED ROUTE
// ============================================
function ProtectedRoute({ children }) {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
// ============================================
// PART 6: THE FORM SUBMIT
// ============================================
function CreatePostForm() {
const navigate = useNavigate();
async function handleSubmit(e) {
e.preventDefault();
const data = new FormData(e.target);
const post = await createPost(data);
navigate(`/posts/${post.id}`);
}
return <form onSubmit={handleSubmit}>...</form>;
}
// ============================================
// PART 7: THE OAUTH CALLBACK
// ============================================
function OAuthCallback() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
useEffect(() => {
const token = searchParams.get('token');
if (token) {
localStorage.setItem('token', token);
navigate('/dashboard', { replace: true });
} else {
navigate('/login', { replace: true });
}
}, [navigate, searchParams]);
return <p>Authenticating...</p>;
}
// ============================================
// PART 8: THE CONDITIONAL REDIRECT
// ============================================
function Dashboard() {
const { user } = useAuth();
const navigate = useNavigate();
useEffect(() => {
if (!user) navigate('/login', { replace: true });
}, [user, navigate]);
if (!user) return null;
return <div>Welcome, {user.name}</div>;
}
// ============================================
// PART 9: THE SESSION TIMEOUT
// ============================================
function SessionTimeout({ timeout = 30 * 60 * 1000 }) {
const navigate = useNavigate();
useEffect(() => {
const timer = setTimeout(() => {
navigate('/login', { replace: true });
}, timeout);
return () => clearTimeout(timer);
}, [navigate, timeout]);
return null;
}
// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================
// 1. Call navigate in an event handler or an effect.
// 2. Use the replace option for redirects.
// 3. Use the state option for the transient data.
// 4. Use the delta for the back and forward.
// 5. Use <Navigate> for the render-time redirect.
// 6. Include the dependencies in the effect array.
// 7. Use the leading slash for the absolute paths.
// 8. Do not call navigate during render.
The ten parts showed the basic navigate, the replace option, the state option, the delta, the protected route, the form submit, the OAuth callback, the conditional redirect, the session timeout, and the complete pattern.
Quick Reference
The useNavigate API
| Argument | Type | Purpose |
|---|---|---|
to | String or object | The destination |
options.replace | Boolean | Replace the history entry |
options.state | Any | The transient data |
options.relative | 'route' | 'path' | The relative resolution |
The Destination Forms
| Form | Example |
|---|---|
| String path | navigate('/dashboard') |
| Object | navigate({ pathname: '/dashboard' }) |
| Delta | navigate(-1) |
| Relative | navigate('settings') |
| Absolute | navigate('/settings') |
The Common Patterns
| Pattern | Options |
|---|---|
| Login redirect | replace: true |
| Logout redirect | replace: true |
| Post-login return | state: { from }, replace: true |
| Back button | navigate(-1) |
| Search form | navigate('/search?q=...') |
The useNavigate vs <Navigate>
| Aspect | useNavigate | <Navigate> |
|---|---|---|
| Used in | Event handler, effect | Render |
| Form | Imperative | Declarative |
| Use case | After an event | During a render |
| Redirect | Yes | Yes |
The Caveats
| Caveat | Rule |
|---|---|
| Render | Do not call during render |
| Dependencies | Include the values |
| State | Not in the URL |
| Relative | The leading slash is absolute |
| Replace | Use for redirects |
Best Practices
✅ Do This:
// Call navigate in an event handler.
function handleClick() { navigate('/dashboard'); } // ✅
// Call navigate in an effect.
useEffect(() => { if (!user) navigate('/login'); }, [user, navigate]); // ✅
// Use the replace option for the login redirect.
navigate('/dashboard', { replace: true }); // ✅
// Use the state option for the return destination.
navigate('/login', { state: { from: location } }); // ✅
// Use the delta for the back button.
navigate(-1); // ✅
// Use <Navigate> for the render-time redirect.
if (!user) return <Navigate to="/login" replace />; // ✅
// Use the leading slash for the absolute path.
navigate('/settings'); // ✅
❌ Don’t Do This:
// Don't call navigate during render.
function Component() {
const navigate = useNavigate();
navigate('/dashboard'); // ❌
return <div>...</div>;
}
// Don't forget the dependencies.
useEffect(() => {
if (!user) navigate('/login');
}, []); // ❌ The user is stale
// Don't use the state for the persistent data.
navigate('/search', { state: { q: 'react' } }); // ❌ Use the query string
// Don't use the push for the redirect.
navigate('/login'); // ❌ The back button returns to the form
// Don't forget the leading slash for the absolute path.
navigate('settings'); // ⚠️ Relative to the current path
// Don't navigate in both the render and the effect.
if (!user) navigate('/login'); // ❌ Double navigation
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Navigation on render | The call is a side effect | Use an effect |
| Double navigation | The render and the effect | Remove the render call |
| Stale closure | The dependency array is empty | Add the values |
| Back button returns to the form | No replace: true | Add the option |
| State lost on refresh | The state is not in the URL | Use the query string |
| Relative path surprise | The leading slash is missing | Add the slash |
useNavigate in a class | The hook is for functions | Use <Navigate> |
Real-World Examples
1. Basic Navigate
navigate('/dashboard');
2. Replace
navigate('/login', { replace: true });
3. State
navigate('/login', { state: { from: location } });
4. Delta
navigate(-1);
5. Login Redirect
navigate('/dashboard', { replace: true });
6. Post-Login Return
navigate(from, { replace: true });
7. Protected Route
if (!user) return <Navigate to="/login" replace />;
8. OAuth Callback
useEffect(() => { navigate('/dashboard', { replace: true }); }, []);
9. Session Timeout
setTimeout(() => navigate('/login', { replace: true }), timeout);
10. Search Form
navigate(`/search?q=${encodeURIComponent(query)}`);
Visual
The useNavigate Flow
┌────────────────────────────────────────────────────────────┐
│ THE useNavigate FLOW │
│ │
│ EVENT │
│ ┌──────────────────────────────────────────────────┐ │
│ │ <button onClick={handleClick}> │ │
│ │ → handleClick() │ │
│ │ → navigate('/dashboard') │ │
│ │ → the router changes the location │ │
│ │ → the matched component renders │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ EFFECT │
│ ┌──────────────────────────────────────────────────┐ │
│ │ useEffect(() => { │ │
│ │ if (!user) navigate('/login'); │ │
│ │ }, [user, navigate]); │ │
│ │ │ │
│ │ The effect runs after the render. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Replace vs Push
┌────────────────────────────────────────────────────────────┐
│ REPLACE vs PUSH │
│ │
│ PUSH (default) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ History: [/, /login, /dashboard] │ │
│ │ │ │
│ │ The back button returns to /login. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ REPLACE │
│ ┌──────────────────────────────────────────────────┐ │
│ │ History: [/, /dashboard] │ │
│ │ │ │
│ │ The /login entry is replaced. │ │
│ │ The back button returns to /. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The replace is the right choice for a redirect. │
└────────────────────────────────────────────────────────────┘
The State
┌────────────────────────────────────────────────────────────┐
│ THE STATE │
│ │
│ SOURCE: /settings │
│ ┌──────────────────────────────────────────────────┐ │
│ │ navigate('/login', { state: { from: '/settings' } });│ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ DESTINATION: /login │
│ ┌──────────────────────────────────────────────────┐ │
│ │ const location = useLocation(); │ │
│ │ const from = location.state?.from; │ │
│ │ │ │
│ │ from === '/settings' │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ AFTER LOGIN: /settings │
│ ┌──────────────────────────────────────────────────┐ │
│ │ navigate(from, { replace: true }); │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The useNavigate vs Navigate
┌────────────────────────────────────────────────────────────┐
│ useNavigate vs <Navigate> │
│ │
│ useNavigate │
│ ┌──────────────────────────────────────────────────┐ │
│ │ function handleClick() { │ │
│ │ navigate('/dashboard'); │ │
│ │ } │ │
│ │ │ │
│ │ Imperative. Used in an event handler. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ <Navigate> │
│ ┌──────────────────────────────────────────────────┐ │
│ │ if (!user) return <Navigate to="/login" replace />;│ │
│ │ │ │
│ │ Declarative. Used during a render. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The two are equivalent. Use the right one for the context.│
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
useNavigate | The hook that returns the navigate function |
navigate(to) | The basic navigation |
navigate(-1) | The back navigation |
replace: true | Replace the history entry |
state: {...} | The transient data |
<Navigate> | The declarative alternative |
| Relative path | No leading slash |
| Absolute path | Leading slash |
useLocation | Reads the state in the destination |
| Effect | The right place for the render-time redirect |
Key takeaways:
useNavigatereturns thenavigatefunction. The function is called with a destination and an options object. The destination can be a string, a location object, or a delta. The function is stable across renders.- The
navigatefunction must not be called during render. The call is a side effect. The event handler and the effect are the right places. The render-time redirect uses the<Navigate>component. - The
replaceoption replaces the history entry. The option is used after a login, a logout, a redirect, and a 404. The default is the push, which adds a new entry. - The
stateoption carries the transient data. The data is not in the URL, and the destination reads it with theuseLocationhook. The “return to the original page after login” pattern uses the state. - The relative path is relative to the current route. The
navigate('settings')inside/users/42goes to/users/settings. The leading slash makes the path absolute. - The
<Navigate>component is the declarative form. It is used inside a render, and the navigation happens when the component renders. TheuseNavigatehook is used inside an event handler or an effect. - The dependency array must include the values. The
useEffectthat navigates based on the user must include theuserand thenavigatein the array. The missing dependency produces the stale closure. - The double navigation is the common bug. The component that navigates in both the render and the effect navigates twice. The render call is the bug.
Remember: The useNavigate hook is the imperative side of React Router. It returns the navigate function, which is called from an event handler or an effect. The function accepts a path, a delta, or a location object, and the options control the history stack and the state. The <Navigate> component is the declarative alternative for the render-time redirect. The result is an application that moves the user through the routes in response to the events, the conditions, and the timeouts, and not only to the clicks.
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!