Programmatic Navigation
Links cover navigation that a user clicks, but plenty of navigation happens in response to code: after a form saves, once a login succeeds, or when a guard decides a visitor cannot stay on a page. For those cases React Router gives you the useNavigate hook and the Navigate component. Both change the URL the same way a Link does — updating history without a full page reload — but they fire from your logic instead of from a click. Knowing when to reach for each one keeps your flows predictable and keeps the back button working the way users expect.
Navigating with useNavigate
useNavigate returns a navigate function you can call from event handlers, effects, or callbacks. Pass it a path string to go somewhere, or a number to move through history.
import { useNavigate } from 'react-router-dom';
function CreatePostButton() {
const navigate = useNavigate();
return (
<button onClick={() => navigate('/posts/new')}>
Write a post
</button>
);
}
The function accepts a second options argument that controls how the navigation is recorded and what travels with it. The most common options are below.
| Option | Type | What it does |
|---|---|---|
replace | boolean | Replaces the current history entry instead of pushing a new one |
state | any | Attaches data to the new location, readable via useLocation |
relative | 'route' | 'path' | Resolves relative paths against the route or the raw URL |
preventScrollReset | boolean | Keeps the current scroll position after navigating |
Push versus replace
By default navigate('/path') pushes a new entry onto the history stack, so the browser back button returns to where the user was. Passing replace: true swaps the current entry instead, which is exactly what you want after actions that should not be repeatable with back — like a completed login or a successful checkout.
function LoginForm() {
const navigate = useNavigate();
async function handleSubmit(event) {
event.preventDefault();
const form = new FormData(event.currentTarget);
await login(form.get('email'), form.get('password'));
// Replace so "back" doesn't return to the login screen.
navigate('/dashboard', { replace: true });
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Sign in</button>
</form>
);
}
Passing state and reading it back
The state option lets you carry data through a navigation without putting it in the URL. It survives back/forward but is lost on a hard refresh, so treat it as a hint rather than a source of truth.
import { useNavigate, useLocation } from 'react-router-dom';
function ProductCard({ product }) {
const navigate = useNavigate();
return (
<button onClick={() => navigate(`/products/${product.id}`, { state: { from: 'list' } })}>
{product.name}
</button>
);
}
function ProductPage() {
const location = useLocation();
const cameFromList = location.state?.from === 'list';
return <p>{cameFromList ? 'Back to list' : 'Welcome'}</p>;
}
Going back and forward
Pass a number to move relative to the current position in history: -1 is back, -2 is two back, and 1 is forward.
function BackButton() {
const navigate = useNavigate();
return <button onClick={() => navigate(-1)}>Go back</button>;
}
Calling
navigateduring render throws an error. Only call it inside an event handler or an effect — never directly in the component body.
Redirecting with the Navigate component
When the decision to leave a page is made during render rather than in a handler, render the Navigate component instead of calling the hook. This is the idiomatic pattern for guards and for empty states. As soon as Navigate renders, the router changes the URL.
import { Navigate, useLocation } from 'react-router-dom';
function RequireAuth({ user, children }) {
const location = useLocation();
if (!user) {
// Remember where they were headed so we can return after login.
return <Navigate to="/login" replace state={{ from: location }} />;
}
return children;
}
Because RequireAuth returns Navigate while rendering, there is no flash of the protected content. The replace prop keeps the login redirect out of history, and the saved state.from lets the login handler send the user back to their original destination after authenticating.
Navigating after an async action
A frequent shape is: submit, await the server, then move on. Disable the control while the request is in flight and only navigate on success so failures keep the user on the page with their input intact.
function NewArticle() {
const navigate = useNavigate();
const [saving, setSaving] = useState(false);
async function handleSubmit(event) {
event.preventDefault();
setSaving(true);
try {
const article = await createArticle(new FormData(event.currentTarget));
navigate(`/articles/${article.id}`);
} catch (err) {
setSaving(false);
}
}
return (
<form onSubmit={handleSubmit}>
<input name="title" required />
<button type="submit" disabled={saving}>
{saving ? 'Saving…' : 'Publish'}
</button>
</form>
);
}
Output:
POST /api/articles -> 201 Created
URL changes to /articles/42 without a page reload
Best practices
- Use
replace: trueafter logins, logouts, and one-shot actions so the back button never returns to a stale or non-repeatable screen. - Reach for
<Navigate>when the redirect is decided during render (guards, redirects); useuseNavigatefor navigation triggered by events. - Only navigate after an async operation resolves successfully, so errors leave the user in place with their data.
- Keep
statefor transient hints; anything that must survive a refresh or be shareable belongs in the URL as a path or query param. - Save the attempted location in
state.fromon protected routes so you can bounce the user back after they sign in. - Avoid calling
navigatein the render path — wrap it in an effect or a handler to prevent runtime errors.