π§ Programmatic Navigation
A <Link> is great when the user decides to go somewhere. But some moves are decided by your code: redirect to the dashboard after a successful login, bounce a logged-out visitor to the sign-in page, jump to the confirmation screen once an order clears. For those, React Router hands you a steering wheel β the useNavigate hook.
Week 5 · Day 3 (Wednesday: Advanced Routing) · Lecture 3
π― Learning Objectives
By the end of this lesson, you will be able to:
- Navigate from event handlers and effects with the
useNavigatehook - Choose between a
<Link>and a programmaticnavigate()call - Redirect after async work β form submits, logins β and handle failures
- Use
replaceto keep one-way screens out of the history stack - Pass and read hidden location state across a navigation
- Move through history with
navigate(-1)and protect routes with a redirect pattern
Estimated Time: 70 minutes
Practice: Build a login form that redirects to the page the user originally wanted.
In This Lesson
Link vs Navigate
Both a <Link> and useNavigate change the URL without a full page reload. The difference is who pulls the trigger. A <Link> is a piece of UI the user clicks β it renders a real anchor, works with middle-click and "open in new tab," and should be your default. useNavigate is for navigation that happens as a consequence of logic: an await resolving, a condition failing, a timer firing.
(default β it's an anchor)"] B --> |Your code decides| D["useNavigate()"] D --> E[After a form submit] D --> F[After login / auth check] D --> G[Redirect on a condition] D --> H["Back / forward: navigate(-1)"]
π Rule of thumb
If a user could reasonably want to right-click and open in a new tab, it's a <Link>. If the navigation is a side effect of something that already happened, it's useNavigate. Don't wrap a <Link> around a button just to call navigate in its onClick β pick the right tool.
Redirecting After Async Work
The most common use of useNavigate: do some async work, then move the user based on the result. A registration form submits, waits for the server, and on success sends the user to a welcome screen β on failure, it stays put and shows the error.
import { useNavigate } from 'react-router-dom';
import { useState } from 'react';
function RegistrationForm() {
const navigate = useNavigate();
const [form, setForm] = useState({ username: '', email: '', password: '' });
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
setSubmitting(true);
setError(null);
try {
const { user } = await registerUser(form);
// Success β move on, passing a little context to the next page:
navigate('/welcome', { state: { user, isNewUser: true } });
} catch (err) {
// Failure β stay on the form and surface the message (don't navigate away)
setError(err.message);
} finally {
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
{/* fields bound to form state ... */}
{error && <p className="error">{error}</p>}
<button type="submit" disabled={submitting}>
{submitting ? 'Registeringβ¦' : 'Register'}
</button>
</form>
);
}
π‘ On failure, prefer local state to a redirect
The original instinct is often to navigate('/error') when something fails. Usually that's worse UX β it throws away what the user typed. Showing the error in place (as above) lets them fix a typo and retry. Reserve navigating-to-an-error-page for truly unrecoverable situations.
replace & Location State
Push vs replace
By default navigate() pushes a new entry onto the history stack, so Back returns to where you were. Passing { replace: true } swaps the current entry instead β the page you left is forgotten. That's exactly what you want after a login or a completed checkout, so the Back button doesn't drop the user onto a form they already submitted.
/login in history; replace overwrites it so Back never returns to the login screen.Passing location state
The second argument's state field ships hidden data along with a navigation β not visible in the URL, ideal for "where did this person come from" or a one-time flag. Read it on the destination with useLocation.
// Source:
navigate('/order-success', { state: { orderId: result.id }, replace: true });
// Destination:
import { useLocation } from 'react-router-dom';
function OrderSuccess() {
const location = useLocation();
const orderId = location.state?.orderId; // optional-chain: state may be null
if (!orderId) return <Navigate to="/" replace />; // opened directly, no state
return <h1>Order {orderId} confirmed π</h1>;
}
β οΈ Location state is fragile β and public-ish
State survives Back/Forward but is lost on a hard refresh or when someone pastes the URL fresh, so always guard for its absence (the ?. and fallback above). It's also stored in the browser's history β keep it small and never put secrets or full objects in it; pass an id and re-fetch on the other side.
Moving Through History
Pass navigate a number to walk the history stack, exactly like the browser's Back and Forward buttons β no path needed.
navigate(-1); // back one entry (like clicking Back)
navigate(1); // forward one entry
navigate(-2); // back two entries
// A "Cancel" button that returns wherever the user came from:
function CancelButton() {
const navigate = useNavigate();
return <button onClick={() => navigate(-1)}>Cancel</button>;
}
Why it matters: relative history moves keep components decoupled from the app's route map. A modal's Close button that calls navigate(-1) works no matter which page opened it β you don't have to hard-code a destination.
π‘ Careful with hard-coded numbers
navigate(-1) assumes there's somewhere to go back to. If a user landed on the page from an external link, Back may leave your app entirely. For a reliable "up one level," prefer navigating to an explicit parent path over a blind -1 when the destination actually matters.
Protected Routes
Programmatic navigation shines for auth guards. A protected wrapper checks for a logged-in user and, if there isn't one, redirects to the login page β while remembering where the user was headed so you can send them back after they sign in.
import { Navigate, useLocation } from 'react-router-dom';
// Declarative guard β redirect happens during render, safely:
function RequireAuth({ children }) {
const { user } = useAuth();
const location = useLocation();
if (!user) {
// Stash the attempted location so login can return here:
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
// Usage in the route tree:
<Route
path="/dashboard"
element={
<RequireAuth>
<Dashboard />
</RequireAuth>
}
/>
Now the login form reads that saved location and returns the user to it β the "resume where you left off" experience users expect.
function LoginForm() {
const navigate = useNavigate();
const location = useLocation();
const handleLogin = async (e) => {
e.preventDefault();
const { user, token } = await loginUser(credentials);
localStorage.setItem('token', token);
// Back to the intended page, or /dashboard as a fallback:
const from = location.state?.from?.pathname || '/dashboard';
navigate(from, { replace: true }); // replace so Back doesn't return to login
};
return <form onSubmit={handleLogin}>{/* fields */}</form>;
}
β
Why replace here matters
After login you replace the login entry so the user's Back button skips straight past it. Without replace, tapping Back would dump an already-authenticated user right back onto the sign-in screen β confusing and slightly broken-feeling.
Practice & Quiz
ποΈ Exercise 1: Redirect after submit
Goal: Write a createPost handler that awaits a save, then navigates to /posts/:id for the new post using replace.
async function handleCreate(data) {
// TODO: await savePost, then navigate to the new post's page
}
π‘ Hint
Get navigate from useNavigate(). Await the save, read the returned id, then call navigate(`/posts/${id}`, { replace: true }).
β Solution
function NewPost() {
const navigate = useNavigate();
const handleCreate = async (data) => {
const { id } = await savePost(data);
navigate(`/posts/${id}`, { replace: true });
};
return <PostEditor onSave={handleCreate} />;
}
ποΈ Exercise 2: Login returns you home
Goal: After a successful login, send the user to location.state?.from if present, else /dashboard β and keep login out of history.
β Solution
const from = location.state?.from?.pathname || '/dashboard';
navigate(from, { replace: true });
The replace keeps the login page off the stack; the fallback covers users who came to /login directly.
π― Quick Quiz
Question 1: When should you prefer useNavigate over a <Link>?
Question 2: What does navigate('/dashboard', { replace: true }) do to history?
Question 3: You need to redirect during render because a user isn't logged in. Best choice?
Best Practices & Pitfalls
β Do
- Default to
<Link>for user-clickable destinations; useuseNavigatefor code-driven moves - Redirect with
{ replace: true }after logins and completed one-way flows - Wrap navigation after
awaitintry/catchand handle failures gracefully - Pass a small id in location state and re-fetch, rather than a whole object
- Use
<Navigate>for redirects that happen during render
β Don't
- Call
navigate()in the render body β put it in a handler or effect - Redirect to an error page on every failure and lose the user's input
- Rely on location state surviving a refresh β always guard for its absence
- Blindly
navigate(-1)when a specific parent path is what you actually mean
β οΈ Pass minimal state
// β Ships an entire object through history β bloated and fragile
navigate('/details', { state: { item } });
// β
Pass an id; re-fetch on the destination
navigate('/details', { state: { id: item.id } });
Summary
π Key Takeaways
useNavigate()returns anavigatefunction for code-driven route changes- Use a
<Link>when the user clicks;useNavigatewhen your logic decides - Redirect after async work in a handler, and handle failures without losing input
{ replace: true }keeps login and one-way screens out of the Back stack- Location state ships hidden data but is lost on refresh β guard for it and keep it small
- Pass a number to walk history; use
<Navigate>for redirects during render
π Additional Resources
- React Router β useNavigate
- React Router β Navigate component
- React Router β useLocation
- MDN β History API
π What's Next?
You've finished the Advanced Routing track β your app can nest layouts, read the URL, and steer itself. Next we turn to making it fast: React.memo & useMemo, the tools for skipping unnecessary re-renders and expensive recalculations.
π Routing mastered!
Nested layouts, dynamic params, and programmatic navigation β you now have every routing tool a production React app needs.