Skip to main content

    Lesson 7 • Intermediate

    React Router ⚛️

    By the end of this lesson you'll be able to turn a single React component into a multi-page app — defining routes, linking between pages without reloads, reading data out of the URL, redirecting in code, sharing layouts with nested routes, and showing a proper 404 page.

    What You'll Learn

    • Switch on routing by wrapping your app in BrowserRouter
    • Map URLs to components with Routes and Route (plus a 404 with path="*")
    • Navigate without full reloads using Link, and highlight the current page with NavLink
    • Read dynamic URL values like /users/42 with useParams
    • Redirect from code (e.g. after login) with useNavigate
    • Share a layout across pages using nested routes and Outlet

    1. Setup & Basic Routes

    A router's one job is to look at the current URL and show the matching component. You switch routing on by wrapping your whole app in <BrowserRouter> once (usually in main.jsx). Then you list your pages inside <Routes>, one <Route> per page, each with a path and the element to render. Read this worked example carefully — every line is commented.

    // main.jsx — wrap your WHOLE app in <BrowserRouter> exactly once.
    import { BrowserRouter } from 'react-router-dom';
    import ReactDOM from 'react-dom/client';
    import App from './App';
    
    ReactDOM.createRoot(document.getElementById('root')).render(
      <BrowserRouter>      {/* turns on client-side routing for everything inside */}
        <App />
      </BrowserRouter>
    );
    
    // App.jsx — map URL paths to components with <Routes> and <Route>.
    import { Routes, Route } from 'react-router-dom';
    
    function App() {
      return (
        <Routes>
          {/* path="/"  -> the homepage; element is the component to show */}
          <Route path="/"        element={<Home />} />
          <Route path="/about"   element={<About />} />
          <Route path="/contact" element={<Contact />} />
          {/* path="*" is the catch-all 404 — it MUST come last */}
          <Route path="*"        element={<NotFound />} />
        </Routes>
      );
    }
    // Visiting /about renders <About />. Visiting /banana renders <NotFound />.

    The special path="*" route matches any URL no other route caught, so it's your 404 page — and it goes last. Now run the logic a router uses to pick a route. This is plain JS you can execute:

    Worked example: how route matching works

    Run it and watch /banana fall through to the wildcard 404.

    Try it Yourself »
    JavaScript
    // === How a router decides which page to show ===
    // React Router compares the current URL against each route's pattern,
    // top to bottom, and renders the FIRST one that matches.
    
    const routes = [
      { path: "/",        page: "Home" },
      { path: "/about",   page: "About" },
      { path: "/contact", page: "Contact" },
      { path: "*",        page: "NotFound" },   // catch-all, kept last
    ];
    
    // A tiny matcher: "*" matches anything, otherwise it's an exact match.
    function matchRoute(url) {
      for (const 
    ...

    Your turn. The matcher below is almost done — fill in the one blank so unknown URLs fall through to the 404, then run it and check the output.

    🎯 Your turn: finish the route matcher

    Replace ___ with the catch-all string, then run.

    Try it Yourself »
    JavaScript
    // 🎯 YOUR TURN — replace each ___ then press "Run".
    // Goal: finish the matcher so unknown URLs fall through to the 404 page.
    
    const routes = [
      { path: "/",      page: "Home" },
      { path: "/shop",  page: "Shop" },
      { path: "*",      page: "NotFound" },
    ];
    
    function matchRoute(url) {
      for (const route of routes) {
        // 1) The wildcard "*" should match ANY url. Compare route.path to it.
        if (route.path === ___ || route.path === url) {  // 👉 the catch-all string
          return route.page;
    
    ...

    2. Linking Between Pages

    Inside a routed app you must never use a plain <a href="..."> to move between your own pages — that makes the browser throw your app away and reload the whole thing from the server, losing all React state. Use <Link to="..."> instead: it swaps the page in JavaScript, instantly. <NavLink> is the same thing but it knows when it points at the current page, so you can style the active link.

    // Navbar.jsx — navigate WITHOUT a full page reload.
    import { Link, NavLink } from 'react-router-dom';
    
    function Navbar() {
      return (
        <nav>
          {/* <Link> swaps the page in JavaScript — React state survives. */}
          <Link to="/">Home</Link>
          <Link to="/about">About</Link>
    
          {/* <NavLink> is a <Link> that KNOWS if it's the current page.
              The className function gives you { isActive } to style with. */}
          <NavLink
            to="/contact"
            className={({ isActive }) =>
              isActive ? 'font-bold text-blue-600' : 'text-gray-600'
            }
          >
            Contact
          </NavLink>
        </nav>
      );
    }
    // On /contact the Contact link is bold+blue; the others stay grey.
    // Never write <a href="/about"> here — that reloads the whole app.

    🔎 Deep Dive: why <a> is so bad here

    A normal anchor tag tells the browser "go fetch this URL from the server". The browser tears down the current page, downloads everything again, and re-runs your whole React app from scratch — so any state (a half-filled form, a scroll position, a logged-in flag held in memory) vanishes, and there's a visible flash.

    <Link> intercepts the click, updates the URL bar with the History API, and just renders the new component. No server round-trip, no flash, no lost state. That speed is the entire point of a single-page app.

    3. Dynamic Routes & URL Params

    You don't write one route per user. Instead you put a placeholder in the path with a colon — /users/:userId — and that :userId matches whatever's in that slot. Inside the component you read it with the useParams() hook. The key you destructure must match the name after the colon, and the value always comes back as a string.

    // 1) Declare a dynamic segment with a colon: :userId is a placeholder.
    <Route path="/users/:userId" element={<UserProfile />} />
    
    // 2) Read it inside the component with useParams().
    import { useParams } from 'react-router-dom';
    
    function UserProfile() {
      const { userId } = useParams();   // key MUST match the route: ":userId"
      // URL /users/42    -> userId is "42"   (always a string!)
      // URL /users/alice -> userId is "alice"
      return <h1>Profile for user {userId}</h1>;
    }
    
    // Multiple params work the same way:
    <Route path="/products/:category/:id" element={<Product />} />
    // URL /products/shoes/7 -> { category: "shoes", id: "7" }

    Now run the actual logic useParams performs: it splits the route pattern and the real URL into pieces and, wherever the pattern has a :name, it grabs the value from the same slot. Fill in the three blanks:

    🎯 Your turn: build useParams from scratch

    Parse a URL against its pattern to extract { userId, postId }.

    Try it Yourself »
    JavaScript
    // 🎯 YOUR TURN — this is the logic behind useParams().
    // A route pattern and a real URL are split into pieces. Where the
    // pattern piece starts with ":", the matching URL piece is a PARAM value.
    
    const pattern = "/users/:userId/posts/:postId";
    const url     = "/users/42/posts/7";
    
    const patternParts = pattern.split("/");   // ["", "users", ":userId", "posts", ":postId"]
    const urlParts      = url.split("/");      // ["", "users", "42", "posts", "7"]
    
    const params = {};
    for (let i = 0; i < patt
    ...

    4. Programmatic Navigation

    Sometimes you need to change pages from code rather than from a click — for example, redirecting to /dashboard after a successful login. The useNavigate() hook gives you a navigate function: call navigate('/somewhere') and you're there.

    // useNavigate() lets you change the URL from CODE (not a click).
    import { useNavigate } from 'react-router-dom';
    
    function LoginForm() {
      const navigate = useNavigate();
    
      async function handleSubmit(e) {
        e.preventDefault();
        await logIn();                            // do the work first...
        navigate('/dashboard');                   // ...then send them there
      }
    
      return <form onSubmit={handleSubmit}>{/* ...inputs... */}</form>;
    }
    
    // Handy variations:
    navigate('/dashboard', { replace: true });   // don't add to history (no "Back" to login)
    navigate(-1);                                 // go back one page, like the browser's Back button

    Use { replace: true } after login so the login page isn't left in the history (otherwise pressing Back drops the user right back on it). And navigate(-1) is the same as clicking the browser's Back button.

    5. Nested Routes & Outlet

    Most apps share a frame across several pages — a dashboard with a fixed sidebar, say. Rather than repeat that sidebar in every component, you nest child routes inside a parent route. The parent renders a shared layout containing an <Outlet />, and React Router drops the matched child page into that slot. The special index route is what shows at the parent's exact path.

    // Nested routes share a layout. The parent renders an <Outlet />
    // and the matched CHILD route appears in that slot.
    import { Outlet, Link } from 'react-router-dom';
    
    // App.jsx — children are nested INSIDE the parent <Route>:
    <Routes>
      <Route path="/dashboard" element={<DashboardLayout />}>
        <Route index            element={<DashboardHome />} />   {/* /dashboard */}
        <Route path="settings"  element={<Settings />} />        {/* /dashboard/settings */}
        <Route path="profile"   element={<Profile />} />         {/* /dashboard/profile */}
      </Route>
    </Routes>
    
    // DashboardLayout.jsx — the shared frame:
    function DashboardLayout() {
      return (
        <div className="dashboard">
          <aside>
            <Link to="settings">Settings</Link>   {/* relative link */}
            <Link to="profile">Profile</Link>
          </aside>
          <main>
            <Outlet />   {/* the child route (Settings / Profile / …) renders HERE */}
          </main>
        </div>
      );
    }
    // The sidebar stays put; only the <Outlet /> content changes between pages.

    Now /dashboard/settings renders DashboardLayout with Settings inside its <Outlet />. The sidebar never re-mounts; only the outlet's contents change as you click around.

    Mini-Challenge: a tiny history stack

    No blanks this time — just a brief and an outline. Routers keep a history array so Back works; you'll build a miniature version with push, replace, and pop. Run it and check your output against the expected lines in the comments.

    🎯 Mini-Challenge: navigate / replace / goBack

    Implement the three functions, then run the sequence.

    Try it Yourself »
    JavaScript
    // 🎯 MINI-CHALLENGE: build a mini "navigate()" history stack
    //
    // React Router keeps a history of URLs so the Back button works.
    // Simulate it with a plain array.
    //
    // 1. Start with: const history = ["/"];
    // 2. Write navigate(url)        -> push url onto history, print "Now at: <url>"
    // 3. Write navigateReplace(url) -> REPLACE the last entry (no new history),
    //                                  then print "Replaced -> <url>"
    // 4. Write goBack()             -> remove the last entry and pri
    ...

    Common Errors (and the fix)

    • Page flashes / state resets on every click: you used <a href="/about"> instead of <Link to="/about">. The anchor forces a full server reload; switch to <Link> (or <NavLink>) for internal navigation.
    • useParams gives undefined: the destructured key doesn't match the route. Route /users/:userId must be read as const { userId } = useParams() — not id or user. The names have to be identical.
    • "useNavigate() may be used only in the context of a Router" / blank screen: you forgot to wrap the app in <BrowserRouter>. Every Link, Route, and router hook must live inside that wrapper.
    • Nested page is blank: the parent layout has no <Outlet />. Without it, child routes have nowhere to render.
    • Comparing a param like a number fails: params are always strings, so userId === 42 is false. Convert first: Number(userId) === 42.

    Pro Tips

    • 💡 Put path="*" last. Routes are read top-to-bottom; a wildcard placed early would swallow everything below it.
    • 💡 Redirect after login with { replace: true } so the user can't press Back into the login form.
    • 💡 Relative links in nested routes (<Link to="settings">, no leading slash) resolve against the parent — easier to refactor than absolute paths.
    • 💡 Params are strings. Wrap with Number(...) before doing maths or strict comparisons.

    📋 Quick Reference

    GoalCode
    Turn on routing<BrowserRouter><App /></BrowserRouter>
    Define a page<Route path="/about" element={<About />} />
    404 page<Route path="*" element={<NotFound />} />
    Link (no reload)<Link to="/about">About</Link>
    Active-aware link<NavLink to="/about">…</NavLink>
    Dynamic segmentpath="/users/:userId"
    Read a paramconst { userId } = useParams()
    Redirect in codeconst navigate = useNavigate(); navigate("/x")
    Go backnavigate(-1)
    Nested child slot<Outlet /> (in the parent layout)

    Frequently Asked Questions

    Q: Is React Router part of React?

    No. React itself has no concept of URLs or pages. React Router is a separate library you add with npm install react-router-dom. It's the de-facto standard for routing in React apps.

    Q: When should I use Link vs useNavigate?

    Use <Link> for things the user clicks (nav bars, buttons that are really links). Use useNavigate when code decides to move — after a form submits, a login succeeds, or a timer fires.

    Q: Why is my useParams value a string when the URL had a number?

    A URL is just text, so every param arrives as a string. /users/42 gives you "42". Convert it with Number(userId) if you need to do maths or a strict === comparison.

    Q: What's the difference between index and path="" in nested routes?

    An index route is what renders at the parent's exact URL (e.g. /dashboard with nothing after it). It's the nested-route way of saying "the default child page."

    🎉 Lesson Complete

    • BrowserRouter wraps the app once and turns routing on
    • Routes + Route map URLs to components; path="*" (last) is the 404
    • Link navigates without a reload; NavLink styles the active page
    • :param segments + useParams() read data out of the URL (always strings)
    • useNavigate() redirects from code; { replace: true } and navigate(-1) control history
    • ✅ Nested routes + Outlet share a layout across pages
    • Next lesson: Building a Complete React App — wire all of this into a real project

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service