Skip to content

The React Router Journey

Posted on:June 23, 2022

Welcome, Developer 👋 How have you been? If you are a first-timer, be very welcome to our blog! For those who have been here before, hope you have learned something cool so far 😀

Every time more, I do my best to invest time and effort to the Welcome, Developer ecosystem. On the last week, I published the first video to our YouTube channel 🎉 It presents a quick and practical approach for getting started with React Query. If you are interested in learning more about managing cache on your React apps, check it out as well as the post about the subject here in the blog.

For today, I’ve prepared a fun journey for us to go through together. It’s one of the most important knowledges (IMO) to have when developing React applications (after React itself, of course 😅). It’s about the React Router library.

Done with all the talking, time to get our hands dirty, developer! Grab your favourite beverage (no judgement) and let’s get started, shall we?

Table of contents

Open Table of contents

Introduction

What React Router is? It’s a client and server-side routing library for React (not my words!). Got it, but hold on… what is routing, please? Well, routing is the mechanism by which page requests are redirected to the component or code that handles them. Simply put, it’s how React know what UI component to present based on a specific route.

In the following chapters, we’ll learn how to become fluent in the React Router library through hands-on exercises and step-by-step explanations 🤓

I hope you enjoy the ride, developer 🤗

Before We Start

Before we get started, shall we create our React app? I’m going to use Vite with npm, feel free to use your favourite one though (CRA, yarn):

npm init vite@latest my-react-router-app -- --template react-ts

Next, let’s install the dependencies and make sure the app is running successfully:

cd my-react-router-app
 
npm i # or npm install
 
npm run dev
 

Awesome, that’s all we need before starting, developer 👍

Installing React Router

Now with our React app created, next let’s install React Router library:

npm i react-router-dom@6

Tidying Up Our App

To start on a green field, let’s get ride of everything that comes in the Vite boilerplate that we do not actually need.

First, on src/App.tsx, replace the existing code by the one below:

import "./App.css";
 
function App() {
  return (
    <div className="App">
      <header className="App-header">Welcome, Developer 👋</header>
      <main className="App-content">This is the Home page 🪐</main>
    </div>
  );
}
 
export default App;

Next, a little tweak to the src/App.css file, as highlighted in the following code:

.App-header {
  background-color: #282c34;
  min-height: 30vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}
 
.App-content {
  background-color: #1e1e1e;
  min-height: 70vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: #458cf1;
}

That’s it. If you wanna remove unused files, such as logo.svg, it’s up to you.

Browser Router

First thing we need to be ready to start using the routing library is adding the Browser Router to our entry point component. It basically consists of an interface to enable React Router running in a web browser.

It stores the current location in the browser’s address bar and is able to navigate using the browser’s built-in history.

Let’s add the Browser Router to the entry point of our app. If you also created the app using Vite, then your file is the src/main.tsx:

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
 
import App from "./App";
import "./index.css";
 
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

Now, we’re ready to start using the routing library, my friend👌

Routes

Well, you and I know that the main reason for having a routing library is to be able to navigate between different pages or content, don’t we? Good, we’re on the same page, developer!

So, we must do our part of the deal and create the routes while React Router does its part and provide us with the navigation. Sound fair?

To put it simply, a route is nothing more than a path that React Router will use to determine what component to render. Could I create any path I want? Yes, you can. Should I? Well, as long as you follow an appropriate naming convention, you can 🙃

A few examples of reasonable route paths I have in mind is ‘src/pages’, ‘src/routes’, ‘src/you-name-it’. My favourite, and the one I chose for this tutorial, is src/routes. Yep, it’s the same used on the React Router tutorial and also the one used by Remix, but we must agree that is the one that makes the most sense.

I thought about creating a couple of routes to illustrate our learning journey:

  1. Project
  2. About
  3. Contact

Create a new folder called src/routes below the src/ folder.

cd src
mkdir routes

Let’s create a route for the first one above together, then you do the same for the other ones:

  1. Inside the src/routes folder, create a new component called project:
cd routes
echo > project.tsx
  1. Then, add the following code to the project.tsx component file:
const Project = () => {
  return <h2>This is the Project page 🧑‍💻</h2>;
};
 
export default Project;

Piece of cake, right? I will wait for you to do the same for the other routes, take your time.

Done? Great, can we start using the routes then? Not so fast, developer 😉 Our app has not a single idea of the routes we just created, and even less what to present in every each of them!

Let’s solve this problem, shall we?

Registering Routes

On src/main.tsx, add the following code so that React Router can find the routes:

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router-dom";
 
import App from "./App";
import "./index.css";
 
import Project from "./routes/project";
import About from "./routes/about";
import Contact from "./routes/contact";
 
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App />} />
        <Route path="project" element={<Project />} />
        <Route path="about" element={<About />} />
        <Route path="contact" element={<Contact />} />
      </Routes>
    </BrowserRouter>
  </React.StrictMode>
);

Nice one, developer. So far, our App knows what to render when it receives a determined route.

If you are keen to test it, when running the app, you can navigate to any route by typing the route in the address bar of your browser👍

Last piece of the puzzle, let’s learn how to navigate from one route to another. We will need links that redirect an user to different routes, so let’s create it!

In the React Router’s land, there is a Link component that can be used to create a link to a route. This component renders an accessible <a> element, with the same behavior in the browser that you would expect from that HTML element (right-click options, for example).

Let’s build a quick navigation component to use in our app. Then, we’ll a couple of links to it.

First things first, we must create a new folder called components inside the src folder:

cd src
mkdir components

Now, let’s add a new folder in src/components to store our navigation component, called Navigation:

cd components
mkdir Navigation

Personal preference now, but I like to organize my React components following the approach very well described by Josh W. Comeau here 👍 If you are like me, we’ll need two files, an index.tsx and a Navigation.tsx file:

cd Navigation
echo > index.tsx
echo > Navigation.tsx

The index.tsx file is only responsible for exporting the component to the world (I mean, to other components in the app). The Navigation.tsx file is the one that actually contains the UI component code. Have you found yourself with many index files opened through your VS Code tabs that made you feel lost? Well, you can now close them and open just the component files, with proper tab names that won’t be confusing!

Apologies for the distraction, back to our Navigation component 🙃 Let’s code it:

import { Link } from "react-router-dom";
 
export const Navigation = () => (
  <div
    style={{
      margin: "20px",
    }}
  >
    <nav>
      <Link to="/">Home</Link>
      {" | "}
      <Link to="/project">Projects</Link>
      {" | "}
      <Link to="/about">About</Link>
      {" | "}
      <Link to="/contact">Contact</Link>
    </nav>
  </div>
);

See the Link component? Simple to use, isn’t it?

Then, we export it through the index file:

export * from "./Navigation";
export { Navigation } from "./Navigation";

Shall we test it now, develop? Let’s do it!

npm run dev

Are you able to navigate to the routes? Yes, you are! (hopefully) 😅

✨ The Doors of Durin 🧙‍♂️

Developer, let me tell you something you already know. At this point, you know enough of React Router to create a simple navigation logic that can take you to different routes in a React application. So you might ask, is it all that the library provides? The answer is no, definitely not!

React Router is a fantastic library and provides a lot more than what I just showed you. If you are interested in just the very basics of the library, you are good to go, developer!

In case you have more time available, and most importantly, if you are enjoying this journey, I invite you to bear with me and enter the library’s world. What do you say? If you are keen and ready, the key is simple answer:

“Ennyn Durin Aran Moria. Pedo Mellon a Minno. Im Narvi hain echant. Celebrimbor o Eregion teithant i thiw hin.” - Gandalf the Grey

If you are a fan of LOTR, I’m pretty sure you’ve got the reference 😉 Let’s continue our journey, my friend.

Nested Routes

If you navigated to other routes, you will see that each of them has a unique look and feel. That’s because they do not share anything in common, apart from being called by the same navigation component.

In the real world, often we want to have a layout in common across most of the routes. This common layout is usually composed of a combination of UI components, such as navigation menu, header, footer, company logo, etc. Do we want to replicate this layout in every route? Of course not, Ctrl+C and Ctrl+V isn’t so popular sometimes, am I right?

React Router 6 introduced a new way to share layouts in the application (which is awesome by the way). This concept is called Nested Routes, and consists of routes inside other routes. There are many cases when it’s used, such as when sharing a common layout, when decoupling parts of the page into routes containing smaller UI components, and so on.

You will have to pay close attention to the code to see the subtle change we will make. Back to our main component:

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import App from "./App";
import "./index.css";
import Project from "./routes/project";
import About from "./routes/about";
import Contact from "./routes/contact";
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App />}>
          <Route path="project" element={<Project />} />
          <Route path="about" element={<About />} />
          <Route path="contact" element={<Contact />} />
        </Route>
      </Routes>
    </BrowserRouter>
  </React.StrictMode>
);

Be honest with me, developer…If I hadn’t highlighted the code, would you note the difference? Probably not, right? Pretty cool, isn’t?

Really, was that all? Well, almost 👌 Let me give you one last tip. On the App component, we will introduce a new component called Outlet. Allow me to quickly explain what it is, please. An Outlet component is used in a parent route to render child route elements. Does it make sense? Do you remember those layout components in React? Same thing pretty much.

“What if I have an Outlet on my route component but I’m not sure if I always have some child route element to render there?” No problems, nothing will be rendered in there and will have no impact to your parent route component.

Back to the App component, mate! Shall we add an Outlet component then?

import { Outlet } from "react-router-dom";
import "./App.css";
 
import { Navigation } from "./components/Navigation";
 
function App() {
  return (
    <div className="App">
      <header className="App-header">
        <b>Welcome, Developer 👋</b>
        <Navigation />
      </header>
      <main className="App-content">
        <Outlet />
      </main>
    </div>
  );
}
 
export default App;

Nice, please note that the App component isn’t our Homepage anymore. Let’s create a route component to be a homepage instead? The process is the same as we did previously for the other routes (ie, Projects, etc.).

cd src
cd routes
echo > home.tsx

Then, add a simple content to it:

const Home = () => {
  return <h2>This is the Home page 🪐</h2>;
};
export default Home;

Lastly, a wee adjust to the main component:

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router-dom";
 
import App from "./App";
import "./index.css";
 
import Project from "./routes/project";
import About from "./routes/about";
import Contact from "./routes/contact";
import Home from "./routes/home";
 
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <BrowserRouter>
      <Routes>
        <Route element={<App />}>
          <Route path="/" element={<Home />} />
          <Route path="project" element={<Project />} />
          <Route path="about" element={<About />} />
          <Route path="contact" element={<Contact />} />
        </Route>
      </Routes>
    </BrowserRouter>
  </React.StrictMode>
);

Ready for the moment of truth, developer? Give it a go then!

npm run dev

Use the navigation menu to switch between routes! How cool is that, huh? 😏

Nested routes? Not an unknown territory for us anymore!

Next, let me tell you how to add a “not found” route.

Not Found Route

Well, we never know what routes (aka URLs) the user will enter. So, we need to handle the case when the user enters a wrong URL, resulting in a 404 error.

Remember on the main.tsx file that we added all routes we planned? Let’s get back there, and add a small adjustment to the Routes component:

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import App from "./App";
import "./index.css";
 
import Project from "./routes/project";
import About from "./routes/about";
import Contact from "./routes/contact";
import Home from "./routes/home";
import NotFound from "./routes/notFound";
 
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <BrowserRouter>
      <Routes>
        <Route element={<App />}>
          <Route path="/" element={<Home />} />
          <Route path="project" element={<Project />} />
          <Route path="about" element={<About />} />
          <Route path="contact" element={<Contact />} />
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>
  </React.StrictMode>
);

Got the logic, developer? If the requested route isn’t anything specifically defined (project, about, contact), it will be captured by the NotFound route.

As you imagined, we must create a new route component called NotFound on the routes folder:

cd src
cd routes
echo > notFound.tsx

Then, add a simple content to it:

const NotFound = () => {
  return <h2>No signal of intelligent life detected 🤖</h2>;
};
export default NotFound;

Take the time to test id, my friend. I wait for you.

npm run dev

Cool, what’s next… Have you heard about Index routes? Allow me to tell you what it is and how to use it, please 😊

Index Routes

Follow along with me, developer. Imagine you have a list of nested routes, as we do, and the parent route component has a Outlet component (same as our App component, right). Nothing new here, I know. What if we want to render something in that Outlet space when the parent route is requested? Am I being clear?

For example, looking at our app so far, the main.tsx component has the following logic (no need to do anything):

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import App from "./App";
import "./index.css";
 
import Project from "./routes/project";
import About from "./routes/about";
import Contact from "./routes/contact";
import Home from "./routes/home";
import NotFound from "./routes/notFound";
 
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <BrowserRouter>
      <Routes>
        <Route element={<App />}>
          <Route path="/" element={<Home />} />
          <Route path="project" element={<Project />} />
          <Route path="about" element={<About />} />
          <Route path="contact" element={<Contact />} />
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>
  </React.StrictMode>
);

What’s the purpose of a Home route component? Should not it be render through the App component instead? That’t when an Index route comes to the rescue! So, tell me exactly when to use an Index route, please:

Let me admit something. The scenarios above aren’t from my head. I copied them over from the official documentation. Sorry, I couldn’t come up with more straightforward explanations!

So, how do we adjust our app to start using an Index route? Easy, let me guide you. On the main.tsx:

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import App from "./App";
import "./index.css";
 
import Project from "./routes/project";
import About from "./routes/about";
import Contact from "./routes/contact";
import Home from "./routes/home";
import NotFound from "./routes/notFound";
 
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App />}>
          <Route index element={<Home />} />
          <Route path="project" element={<Project />} />
          <Route path="about" element={<About />} />
          <Route path="contact" element={<Contact />} />
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>
  </React.StrictMode>
);

Piece of cake, isn’t it?

So many cool things to show you, developer! Hopefully, you aren’t getting bored, are you? 🫠

How to make a link active? How to show the user what route is active without this having to look at the browser URL?

Well, spoiler alert, it’s easy with React Router!

If we want to change the style of a link, we must go to our Navigation component:

import { NavLink } from "react-router-dom";
 
export const Navigation = () => (
  <div
    style={{
      margin: "20px",
    }}
  >
    <nav>
      <NavLink
        style={({ isActive }) => {
          return {
            color: isActive ? "pink" : "",
          };
        }}
        to="/"
      >
        Home
      </NavLink>
      {" | "}
      <NavLink
        style={({ isActive }) => {
          return {
            color: isActive ? "pink" : "",
          };
        }}
        to="/project"
      >
        Projects
      </NavLink>
      {" | "}
      <NavLink
        style={({ isActive }) => {
          return {
            color: isActive ? "pink" : "",
          };
        }}
        to="/about"
      >
        About
      </NavLink>
      {" | "}
      <NavLink
        style={({ isActive }) => {
          return {
            color: isActive ? "pink" : "",
          };
        }}
        to="/contact"
      >
        Contact
      </NavLink>
    </nav>
  </div>
);

Right, see that we are using a NavLink instead of a Link? That’s because we want to use the isActive prop to change the style of the link. You might be wondering: “Just because of the style thing?“. Well, you are right 😊 A NavLink is a special kind of Link that knows whether or not it is active, so we can use the isActive property to set different CSS styles.

I know, I know…those bunch of NavLinks could be a reusable component instead. But, let’s stick to the purpose of this tutorial.

What next? I saved the best for last, developer! If you are tired, take a break, get another coffee. I’ll do the same, and meet you here ☕

Reading URL Params

Question: What if we want to get the params from the URL? What for? Humm, imagine we built a bunch of routes through an array looping, then ended up with a list of dynamic routes. For example, for each user, we created a route called /user/:id.

All of the “user” routes share the same component, but each one presents a different content based on their IDs. Sound pretty familiar with any real world app, doesn’t it?

Shall we create a dataset of users? Let’s create a new folder under src called data, then a new file called users.ts:

cd src
mkdir data
cd data
echo > users.ts

Then, we can add some users to the dataset:

export interface User {
  id: number;
  name: string;
  emoji: string;
}
 
const users: User[] = [
  {
    id: 1,
    name: "Martin McFly",
    emoji: "🧑",
  },
  {
    id: 2,
    name: "Lorraine Baines",
    emoji: "👩",
  },
  {
    id: 3,
    name: "Doc Brown",
    emoji: "👴🏻",
  },
  {
    id: 4,
    name: "Einstein",
    emoji: "🐶",
  },
];
 
export function getUsers(): User[] {
  return users;
}

Just some TypeScript, developer. First, we defined an interface to ensure that our users are well-formed. Then, we added a function to get the users.

We want to create a link for each user above on our homepage. So, whoever has landed on the app, can tell us who they are.

Let’s create a new link for each user’s route on home.tsx, setting its path according to their IDs:

import { Link } from "react-router-dom";
 
import { getUsers, User } from "../data/users";
 
const Home = () => {
  const users = getUsers();
 
  return (
    <>
      <h2>Who are you, traveler? 🛸</h2>
      <p>My name is:</p>
      {users.map((user: User) => (
        <Link to={`/user/${user.id}`} key={user.id}>
          {user.name}
        </Link>
      ))}
      <p>and I come in peace 🖖</p>
    </>
  );
};
export default Home;

Perfect, we’re getting there. Now, we must decide where to render the user information. What about creating a User page? By “page”, I mean a route and a component 😎

cd src
cd routes
echo > user.tsx

Then, add the basic content to the User route component:

const User = () => {
  return <h2>Hi, I'm ?. I came from the future!</h2>;
};
export default User;

Cool, as you noticed, we want to replace ? by the user’s name. How to get it? Well, we must find a way to read the user ID from the URL, then search on our dataset what record matches the ID. Let’s do it?

Jump into the User route component, and apply the changes below:

import { useParams } from "react-router-dom";
 
const User = () => {
  const params = useParams();
  return <h2>{`Hi, I'm ${params.userID}. I came from the future!`}</h2>;
};
export default User;

We’re good to test it, developer. Let me know if you can navigate to the user page with the right ID, please?

No, right? No worries, that’s because we haven’t registered yet the route for each user. Back to the main.tsx component:

...
 
import User from "./routes/user";
 
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App />}>
          <Route index element={<Home />} />
          <Route path="project" element={<Project />} />
          <Route path="about" element={<About />} />
          <Route path="contact" element={<Contact />} />
          <Route path="user" element={<User />}>
            <Route path=":userID" element={<User />} />
          </Route>
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>
  </React.StrictMode>
);

By the way, order does matter. The User route must be registered before the NotFound route, otherwise, it will never match.

Shall we test it again?

npm run dev

Can you navigate to the user page, showing the right data? Oh okay, is it only showing the ID instead of the name? That’s expected 👍 Don’t you worry, this is the last step I promise!

We need to do some magic to the User route component:

import { useParams } from "react-router-dom";
 
import { getUsers } from "../data/users";
 
const User = () => {
  const params = useParams();
  const users = getUsers();
 
  const selectedUser = users.find(
    (user) => params.userID === user.id.toString()
  );
 
  const name = selectedUser ? selectedUser.name : "(forgot my name)";
  const picture = selectedUser ? selectedUser.emoji : "🤔";
 
  return <h2>{`Hi, I'm ${name} ${picture} I came from the future!`}</h2>;
};
 
export default User;

You should be good to go now! Cool, isn’t it? 👽

Next Steps

Question: Did we covered everything about the React Router library? Not at all.

Question: Did we covered enough for you to hit the ground running? Definitely 🚀

What next, then? Take the time to read the React Router documentation. It’s worth it, believe me (and it isn’t one of those boring documentations).

Resources

Conclusion

Whatever you stopped before, or went through the Doors of Durin, I admire your efforts 👊

In this busy world we live in, finding the time to study and learn new skills isn’t always easy, is it? I know, we’re all on the same boat, my friend. Always remember this famous quote by Martin Luther King Jr.:

“If you can’t fly, run. If you can’t run, walk. If you can’t walk, crawl, but by all means, keep moving.”

I hope you enjoyed this journey, and have learned a lot about React Router 💪

Take care, stay safe, and see you in the next one, developer!