Routing in React page

Learn how to set up and use React Router to update the application's user interface in response to URL changes.

Overview

Routing is a mechanism used by single-page applications to determine what their user interface looks like based on the browser's current URL. This follows the standard pattern for browsers where changing the URL fetches a new resource; however in client-rendered applications like React, there is no communication between the browser and server; instead, a specific component is displayed.

Routing is crucial for:

  • User Experience (UX): Provides seamless navigation without page reloads.
  • Bookmarking: Enables users to bookmark or share URLs that lead directly to a specific state of the application.
  • Organization: Helps in structuring the application into logical views or components.

We will use the React Router package to provide routing for the Place My Order application. This router is popular in the React community and a great de facto choice when building a React application.

Objective 1: Defining routes and Outlets

Our goal is to split the homepage of our Place My Order application so that we only see the Home content on the homepage.

In this section, we will:

  • Install React Router.
  • Set up a RouterProvider with static routes.
  • Create an Outlet to display the route's component.

createBrowserRouter

React Router works by matching the segments of the browser's URL path to components, i.e. each segment of the URL's path corresponds to a particular React component based on the segment's value. The mapping of a segment's value to a component is done through a collection of RouteObject items that are passed to the router when it is initialized.

The createBrowserRouter function is a key part of React Router. This function is used to create a router object that defines the navigation structure of our app. The structure is defined through a configuration object that specifies the paths and corresponding components. Let’s break down its usage with the example below:

import React from 'react'
import ReactDOM from 'react-dom/client'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import AboutPage from './pages/AboutPage'
import HomePage from './pages/HomePage'
import App from './App.tsx'

const router = createBrowserRouter(
  [
    {
      path: '/',
      element: <App />,
      children: [
        {
          index: true,
          element: <HomePage />,
        },
        {
          path: 'about',
          element: <AboutPage />,
        },
      ],
    },
  ],
  {
    basename: import.meta.env.BASE_URL,
  },
)

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
)

The first argument to createBrowserRouter is an array of route objects. Each route object represents a navigation route in our application, and it typically contains the following properties:

  • path: A string that defines the URL path for the route.
  • element: A React component that will be rendered when the route's path matches the current URL.
  • children: An array of nested route objects, allowing for the creation of nested URL structures.
  • index: A boolean for indicating that the route should act as a default or fallback route within a group of nested routes.

The second argument to createBrowserRouter is an optional configuration object. In the code above, it sets the basename, which is the base URL for all locations. Here, it's dynamically set based on the environment variable import.meta.env.BASE_URL. This is useful for scenarios where your application is served from a subdirectory on your server.

RouterProvider

Next, we can use RouterProvider to render our <App> component. Remember that our main.tsx file currently looks like this:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

Instead of only rendering our <App> component, we can use <RouterProvider>:

import React from 'react'
import ReactDOM from 'react-dom/client'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import Home from './pages/Home'
import RestaurantList from './pages/RestaurantList'
import App from './App.tsx'
import './index.css'

const router = createBrowserRouter(
  [
    {
      path: '/',
      element: <App />,
      children: [
        {
          index: true,
          element: <Home />,
        },
        {
          path: 'restaurants',
          element: <RestaurantList />,
        },
      ],
    },
  ],
  {
    basename: import.meta.env.BASE_URL,
  },
)

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
)

The RouterProvider requires the router object created by createBrowserRouter. This object contains all the route configurations and settings for our application.

Outlet

The final piece of React Router API that we need to use is the <Outlet> component.

The Outlet component is used within a parent route component to render its child route components. Think of it as a marker that tells React Router, “Insert the child route component here.”

When you define nested routes in your route configuration above, you don't immediately specify where in the parent component's JSX the child components should appear. Instead, you use Outlet in the parent component's JSX as a placeholder for where the matched child route component should be rendered.

In the example below, the child component (<AboutPage> or <HomePage> from our createBrowserRouter example) would be rendered inside the <main> element:

const Layout = () => {
  return (
    <>
      <header>My awesome site</header>
      <main>
        <Outlet /> {/* Child routes will render here */}
      </main>
      <footer>©</footer>
    </>
  );
};

Setup

To get starting with React Router in our application, let’s install react-router-dom with npm:

✏️ Run:

npm install react-router-dom@6

✏️ Create src/pages/Home/ (folder)

✏️ Create src/pages/Home/index.ts and update it to be:

export { default } from "./Home"

✏️ Create src/pages/Home/Home.tsx and update it to be:

import HeroImage from "place-my-order-assets/images/homepage-hero.jpg"

const Home: React.FC = () => {
    return (
        <div className="homepage" style={{ margin: "auto" }}>
            <img
                alt="Restaurant table with glasses."
                height="380"
                src={HeroImage}
                width="250"
            />

            <h1>Ordering food has never been easier</h1>

            <p>
                We make it easier than ever to order gourmet food from your favorite
                local restaurants.
            </p>
        </div>
    )
}

export default Home

✏️ Update src/App.tsx to be:

import { Outlet } from 'react-router-dom'
import './App.css'

function App() {
  return (
    <>
      <header>
        <nav>
          <h1>place-my-order.com</h1>
          <ul>
            <li>
              <a href="/">Home</a>
            </li>
            <li>
              <a href="/restaurants">Restaurants</a>
            </li>
          </ul>
        </nav>
      </header>

      <RestaurantList />
    </>
  )
}

export default App

✏️ Update src/main.tsx to be:

import React from 'react'
import ReactDOM from 'react-dom/client'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import Home from './pages/Home'
import RestaurantList from './pages/RestaurantList'
import App from './App.tsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

Verify

✏️ Update src/App.test.tsx to be:

import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { describe, expect, it } from 'vitest';

import App from './App';

// Wrap App with MemoryRouter to mock routing
const renderWithRouter = (ui, { route = '/' } = {}) => {
  window.history.pushState({}, 'Test page', route)
  return render(ui, { wrapper: MemoryRouter });
};

describe('App component', () => {
  it('renders without crashing', () => {
    renderWithRouter(<App />);
    expect(screen.getByText(/place-my-order.com/i)).toBeInTheDocument();
  });

  it('contains the navigation bar with correct links', () => {
    renderWithRouter(<App />);
    expect(screen.getByText('Home')).toBeInTheDocument();
    expect(screen.getByText('Restaurants')).toBeInTheDocument();

    const homeLink = screen.getByText('Home').closest('a');
    expect(homeLink).toHaveAttribute('href', '/');

    const restaurantsLink = screen.getByText('Restaurants').closest('a');
    expect(restaurantsLink).toHaveAttribute('href', '/restaurants');
  });
});

Exercise 1

Create routes for the <Home> component and <RestaurantList> component. When the route is "", the <Home> component should display, and when the route is /restaurants then the <RestaurantList> component should display. These changes should be made in src/App.tsx and src/main.tsx.

Having issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.

Solution

Click to see the solution

✏️ Update src/App.tsx to be:

import { Outlet } from 'react-router-dom'
import './App.css'

function App() {
  return (
    <>
      <header>
        <nav>
          <h1>place-my-order.com</h1>
          <ul>
            <li>
              <a href="/">Home</a>
            </li>
            <li>
              <a href="/restaurants">Restaurants</a>
            </li>
          </ul>
        </nav>
      </header>

      <Outlet />
    </>
  )
}

export default App

✏️ Update src/main.tsx to be:

import React from 'react'
import ReactDOM from 'react-dom/client'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import Home from './pages/Home'
import RestaurantList from './pages/RestaurantList'
import App from './App.tsx'
import './index.css'

const router = createBrowserRouter(
  [
    {
      path: '/',
      element: <App />,
      children: [
        {
          index: true,
          element: <Home />,
        },
        {
          path: 'restaurants',
          element: <RestaurantList />,
        },
      ],
    },
  ],
  {
    basename: import.meta.env.BASE_URL,
  },
)

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
)

Having issues with your local setup? See the solution in StackBlitz or CodeSandbox.

Objective 2: Creating links between pages

Next, we want to update Place My Order to include links in our <header> so we can navigate between pages. We also want to update our <Home> component to use React Router for its link to the /restaurants page.

Link

The Link component is a basic building block in React Router. It allows you to create links in our application that navigate to different routes (or paths) defined in our React application.

Use the Link component similarly to how you use an <a> element in HTML. Instead of using href, you use to to specify the path.

<Link to="/about">
    About
</Link>

When users click on this link, they are directed to the /about route in your application, without causing a full page reload.

NavLink

NavLink is a special version of the Link component that can add styling attributes to the rendered element when it matches the current URL. This is particularly useful for highlighting the active link in the navigation.

Use NavLink in place of Link where you need to style the active link.

<NavLink to="/home" activeClassName="active">
    Home
</NavLink>

The activeClassName prop is used to specify the class name that will be added when the link is active (i.e., when the path in the to prop matches the current URL). You can also use the activeStyle prop for inline styling.

Match based on the current route

The activeClassName API above is useful if you only need styling on the <a> itself. However, if you want to style another element based on the current route, you’ll need to use the useMatches function:

const aboutMatch = useMatch('/about');
if (aboutMatch) {
  // The current path is /about
}

In the example above, aboutMatch will be an object with details about the route if the current route is /about; otherwise, useMatch will return undefined if the current route is something else.

Setup

✏️ Update src/App.tsx to be:

import { Link, Outlet, useMatch } from 'react-router-dom'
import './App.css'

function App() {
  return (
    <>
      <header>
        <nav>
          <h1>place-my-order.com</h1>
          <ul>
            <li>
              <a href="/">Home</a>
            </li>
            <li>
              <a href="/restaurants">Restaurants</a>
            </li>
          </ul>
        </nav>
      </header>

      <Outlet />
    </>
  )
}

export default App

Verify

✏️ Update src/App.test.tsx to be:

import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { describe, expect, it } from 'vitest';

import App from './App';

// Wrap App with MemoryRouter to mock routing
const renderWithRouter = (ui, { route = '/' } = {}) => {
  window.history.pushState({}, 'Test page', route)
  return render(ui, { wrapper: MemoryRouter });
};

describe('App component', () => {
  it('renders without crashing', () => {
    renderWithRouter(<App />);
    expect(screen.getByText(/place-my-order.com/i)).toBeInTheDocument();
  });

  it('contains the navigation bar with correct links', () => {
    renderWithRouter(<App />);
    expect(screen.getByText('Home')).toBeInTheDocument();
    expect(screen.getByText('Restaurants')).toBeInTheDocument();

    const homeLink = screen.getByText('Home').closest('a');
    expect(homeLink).toHaveAttribute('href', '/');

    const restaurantsLink = screen.getByText('Restaurants').closest('a');
    expect(restaurantsLink).toHaveAttribute('href', '/restaurants');
  });

  it('highlights "Home" link as active when on the home page', () => {
    renderWithRouter(<App />, { route: '/' });
    expect(screen.getByText('Home').closest('li')).toHaveClass('active');
    expect(screen.getByText('Restaurants').closest('li')).not.toHaveClass('active');
  });
});

Exercise 2

Add a Link and update class names based on route matching

In the <Home> component, we want to use React Router for the link to /restaurants.

In the <App> component, we want to use React Router to add navigation links inside the <nav> component. Here’s an example of what the DOM should look like when the home page is active:

<ul>
    <li class="active">
        <a href='/'>Home</a>
    </li>
    <li>
        <a href='/restaurants'>Restaurants</a>
    </li>
</ul>

Having issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.

Solution

Click to see the solution

✏️ Update src/App.tsx to be:

import { Link, Outlet, useMatch } from 'react-router-dom';
import './App.css';

function App() {
  const homeMatch = useMatch('/');
  const restaurantsMatch = useMatch('/restaurants');

  return (
    <>
      <header>
        <nav>
          <h1>place-my-order.com</h1>
          <ul>
            <li className={homeMatch ? 'active' : ''}>
              <Link to='/'>Home</Link>
            </li>
            <li className={restaurantsMatch ? 'active' : ''}>
              <Link to='/restaurants'>Restaurants</Link>
            </li>
          </ul>
        </nav>
      </header>

      <Outlet />
    </>
  );
}

export default App;

Having issues with your local setup? See the solution in StackBlitz or CodeSandbox.