React Basics #15: Routing overview (React Router)

9 min read

Last time we covered performance optimization tools. This is the final article of the series. Up until now we’ve dealt with things happening on a single screen, but real apps typically have multiple screens. The screen changes when a menu is clicked, the URL changes, and the back button must work. The tool that handles this kind of screen transition is routing.

Traditional web vs SPA #

In traditional web pages, every time the user clicks a link, the browser requests a new page from the server and redraws the whole screen with the HTML the server returns. This is what causes the white flash on every page transition.

A SPA (Single Page Application) loads HTML once at the start, and after that, screen transitions are handled by JavaScript redrawing the screen. Instead of asking the server for new HTML, the client swaps the screen on its own — fast and smooth.

A React app is, by default, a SPA. But because everything happens inside the HTML loaded once, we have to define which screen to show when the URL changes. This is called client-side routing, and the de facto standard library in the React ecosystem is React Router.

Note
Frameworks like Next.js provide their own file-system-based routing and don’t need React Router separately. We’ll cover that in a future series. This article focuses on how to handle screen transitions in pure React (a Vite-built app).

Installing React Router #

Add React Router to the Vite project we’ve been using.

install React Router
npm install react-router-dom

(It’s react-router-dom, not react-router — the React Router package for the web)

The simplest example #

Let me show the basic structure of routing first.

src/App.jsx
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

function Home() {
  return <h1>Home page</h1>;
}

function About() {
  return <h1>About page</h1>;
}

function App() {
  return (
    <BrowserRouter>
      <nav style={{ padding: '8px', borderBottom: '1px solid #ccc' }}>
        <Link to="/">Home</Link>
        {' | '}
        <Link to="/about">About</Link>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

The key pieces:

  • <BrowserRouter> — the top-level wrapper that enables routing. Wrap the entire app
  • <Routes> — a container that picks the one <Route> matching the current URL and renders it
  • <Route path="..." element={<...>} /> — defines which component to show for a given path
  • <Link to="..."> — a link that switches routes without a screen flash

If you use a regular anchor tag like <a href="/about">, the browser reloads the page and you lose the SPA’s benefits. You must use <Link> for client-side transitions to happen.

URL parameters — dynamic routes #

Some routes have parts that change dynamically — like a product detail page or a user profile. Use a colon (:) to mark the dynamic part.

src/App.jsx
<Route path="/users/:userId" element={<UserProfile />} />

URLs like /users/123 and /users/alice all match this route. Inside the component, use the useParams hook to extract the dynamic value.

src/UserProfile.jsx
import { useParams } from 'react-router-dom';

function UserProfile() {
  const { userId } = useParams();

  return <h1>User ID: {userId}</h1>;
}

export default UserProfile;

useParams returns all parameters declared in path as an object. For path="/users/:userId/posts/:postId", you can pull out { userId, postId }.

Programmatic navigation — useNavigate #

Sometimes you need to navigate from code, not from a link. Things like moving to a result page after submitting a form, or a logout button sending you home. Use the useNavigate hook.

src/LoginForm.jsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

function LoginForm() {
  const [email, setEmail] = useState('');
  const navigate = useNavigate();

  function handleSubmit(e) {
    e.preventDefault();
    // ... login handling ...
    navigate('/dashboard');
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <button type="submit">Log in</button>
    </form>
  );
}

Use navigate('/path') to navigate to a path, navigate(-1) to go back, and navigate(1) to go forward.

Query parameters — useSearchParams #

For the ?key=value&key2=value2 part of a URL (the query string), use useSearchParams.

src/SearchPage.jsx
import { useSearchParams } from 'react-router-dom';

function SearchPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const query = searchParams.get('q') ?? '';

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setSearchParams({ q: e.target.value })}
        placeholder="search"
      />
      <p>Current search: {query}</p>
    </div>
  );
}

useSearchParams works with an interface similar to useState. Typing updates the URL itself to something like /search?q=react, and refreshing or sharing the URL restores the same state. It’s useful when “the state should be reflected in the URL,” like a search results page.

Nested routes and Outlet #

When several pages share the same layout (header, sidebar, etc.), nested routes are clean. The parent route draws the common layout, and the child route fills in the content slot.

src/App.jsx
<Routes>
  <Route path="/" element={<Layout />}>
    <Route index element={<Home />} />
    <Route path="about" element={<About />} />
    <Route path="users/:userId" element={<UserProfile />} />
  </Route>
</Routes>
src/Layout.jsx
import { Link, Outlet } from 'react-router-dom';

function Layout() {
  return (
    <div>
      <header style={{ padding: '8px', background: '#f4f4f4' }}>
        <Link to="/">Home</Link>
        {' | '}
        <Link to="/about">About</Link>
      </header>
      <main style={{ padding: '16px' }}>
        <Outlet />
      </main>
    </div>
  );
}

The child route’s component is rendered in place of <Outlet />. <Route index> denotes the child to show when the URL matches the parent path (/) exactly.

This pattern lets you keep the header/footer code in one place while swapping only the middle content based on the URL.

404 page #

For a “no matching route” page, put a wildcard route with path="*" last.

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  <Route path="*" element={<NotFound />} />
</Routes>

Routes are tried top to bottom, and * catches whatever doesn’t match.

Highlighting active links — NavLink #

To emphasize the current page’s link in a navigation bar, use <NavLink> instead of <Link>.

src/Nav.jsx
import { NavLink } from 'react-router-dom';

function Nav() {
  return (
    <nav>
      <NavLink
        to="/"
        end
        style={({ isActive }) => ({
          fontWeight: isActive ? 'bold' : 'normal',
          color: isActive ? 'tomato' : 'inherit',
        })}
      >
        Home
      </NavLink>
      {' | '}
      <NavLink
        to="/about"
        style={({ isActive }) => ({
          fontWeight: isActive ? 'bold' : 'normal',
          color: isActive ? 'tomato' : 'inherit',
        })}
      >
        About
      </NavLink>
    </nav>
  );
}

If you pass a function to style (or className), you can branch based on isActive. The end prop means “active only on this exact path” and is often used for a root path like / (without it, every sub-path counts as active).

Try it yourself #

Let’s combine what we’ve covered into a small mini site. It has four pages: home, about, user list, and user detail.

src/Layout.jsx:

src/Layout.jsx
import { NavLink, Outlet } from 'react-router-dom';

function Layout() {
  const linkStyle = ({ isActive }) => ({
    fontWeight: isActive ? 'bold' : 'normal',
    color: isActive ? 'tomato' : 'inherit',
    marginRight: '12px',
  });

  return (
    <div>
      <header style={{ padding: '12px', background: '#f4f4f4', borderBottom: '1px solid #ccc' }}>
        <NavLink to="/" end style={linkStyle}>Home</NavLink>
        <NavLink to="/about" style={linkStyle}>About</NavLink>
        <NavLink to="/users" style={linkStyle}>Users</NavLink>
      </header>
      <main style={{ padding: '16px' }}>
        <Outlet />
      </main>
    </div>
  );
}

export default Layout;

src/pages/Home.jsx:

src/pages/Home.jsx
function Home() {
  return (
    <div>
      <h1>Home</h1>
      <p>A React Router mini site.</p>
    </div>
  );
}

export default Home;

src/pages/About.jsx:

src/pages/About.jsx
function About() {
  return (
    <div>
      <h1>About</h1>
      <p>The example built in the final article of this series.</p>
    </div>
  );
}

export default About;

src/pages/UserList.jsx:

src/pages/UserList.jsx
import { Link } from 'react-router-dom';

const USERS = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 3, name: 'Charlie' },
];

function UserList() {
  return (
    <div>
      <h1>User list</h1>
      <ul>
        {USERS.map(user => (
          <li key={user.id}>
            <Link to={`/users/${user.id}`}>{user.name}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserList;

src/pages/UserDetail.jsx:

src/pages/UserDetail.jsx
import { useParams, useNavigate } from 'react-router-dom';

const USERS = {
  1: { name: 'Alice', email: 'alice@example.com' },
  2: { name: 'Bob', email: 'bob@example.com' },
  3: { name: 'Charlie', email: 'charlie@example.com' },
};

function UserDetail() {
  const { userId } = useParams();
  const navigate = useNavigate();
  const user = USERS[userId];

  if (!user) {
    return (
      <div>
        <h1>User not found.</h1>
        <button onClick={() => navigate('/users')}>Back to list</button>
      </div>
    );
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
      <button onClick={() => navigate(-1)}>Back</button>
    </div>
  );
}

export default UserDetail;

src/pages/NotFound.jsx:

src/pages/NotFound.jsx
import { Link } from 'react-router-dom';

function NotFound() {
  return (
    <div>
      <h1>404  Page not found</h1>
      <Link to="/">Go home</Link>
    </div>
  );
}

export default NotFound;

src/App.jsx:

src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Layout from './Layout';
import Home from './pages/Home';
import About from './pages/About';
import UserList from './pages/UserList';
import UserDetail from './pages/UserDetail';
import NotFound from './pages/NotFound';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="about" element={<About />} />
          <Route path="users" element={<UserList />} />
          <Route path="users/:userId" element={<UserDetail />} />
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Save and try it in the browser.

  • Clicking the header menu changes the screen without a refresh
  • The current page’s link is bold and tomato-colored
  • Clicking a name in the user list moves to the dynamic URL (/users/1) → detail page
  • Clicking “Back” goes back in browser history
  • Typing /some-nonexistent-path directly in the address bar → 404 page

Almost everything you’ve learned across the series is present in this single mini site — splitting components, props, useState, event handling, conditional rendering, and list rendering.

Wrapping up — and finishing the series #

In this article we covered routing. To summarize:

  • A SPA handles screen transitions on the client → needs a routing library
  • React Router essentials: BrowserRouter, Routes, Route, Link/NavLink
  • Dynamic routes (:param) and useParams
  • Programmatic navigation: useNavigate
  • Query parameters: useSearchParams
  • Common layout: nested routes + <Outlet />
  • 404 with path="*"

That’s the end of the React Basics series (#1–#15). We started with “What is React?” and made it to where you can build a small SPA with routing on your own. Looking back at what the series covered:

  • #1–3 Solid foundations — what React is, environment setup, JSX
  • #4–8 Core building blocks — components/props, state, events, conditional/list rendering
  • #9–12 Real-world patterns — forms, useEffect, lifting state up, Context
  • #13–15 Wrap-up — custom hooks, performance optimization, routing

What you’ve learned here is fundamentals that apply equally to any React-based framework (Next.js, Remix, etc.). Frameworks just add routing, data fetching, SSR, and other extras on top.

Recommended paths for those moving on:

  • Modern React 19 + Next.js series (planned) — the latest models like Server Components, use(), Actions, and Suspense
  • Real-world build series (planned) — comprehensive practice with small projects like a todo app, a blog, or a shopping site
  • Start a small project of your own (a personal note-taker, a workout tracker, etc.) and read the official docs whenever you get stuck

Thank you for reading all the way through. Just as small components add up to a big app, anyone who’s followed along article by article is already capable of building their first React app. Have a great React journey!

X