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.
Keen for a walk-through video of this post? Check it out on YouTube, developer π
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?
Introduction
What is React Router? 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 knows what UI components to present based on a 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, let's 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 # vite
# or
npx create-react-app my-react-router-app --template typescript # npx
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 # or `npm start` for CRA
Awesome, thatβs all we need before starting, developer π
Installing React Router
Now that we have just created our React app, letβs install the React Router library:
npm i react-router-dom@6
Note that we install the version 6 (six)
of the library, which is the latest one.
Tidying Up Boilerplate Code
To start with a green field, letβs get ride of everything that comes with the Vite boilerplate and we don't 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
CSS file, as highlighted below:
.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, before starting to work with the routing library, is to add the Browser Router to our entry point component. It basically consists of an interface that enables the React Router to run in the web browser. According to the documentation, "It stores the current location in the browser's address bar using clean URLs and navigates using the browser's built-in history stack.".
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
, otherwise, it should be index.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π
Routes
Well, you and I know that the main reason for having a routing library is to enable navigation amongst different pages or content, don't we? Good, we're on the same page. 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. Does it sound fair, developer?
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:
- Project
- About
- Contact
Create a new folder called src/routes
below the src/
folder:
cd src
mkdir routes
Let's create the first route together, then you do the same for the others:
- Inside the
src/routes
folder, create a new component calledproject
:
cd routes
echo > project.tsx
- 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'll wait for you, take your time!
Done? Great, can we start using the routes then? Not so fast, developer π Our app does not have a single idea yet of the routes that 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! The app knows what, and when, to render based on URL requests.
If you are keen to test it, just start the app and navigate to any of the routes we created by typing the path in the address bar of your browserπ
Links & Navigation
Last piece of the puzzle, let's learn how to navigate from one route to another (not manually, of course!). We're going to need links that redirect an user to different routes, so let's create it.
In case you are wondering if I'm talking about HTML <a>
hyperlink tags, you are almost right! 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 quickly build a navigation component to use in our app. Then, we'll add 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
Next, create 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 the 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? We just need to inform what is the route that the link redirects to, through the to
property.
Then, we export it through the index file:
export * from "./Navigation";
export { Navigation } from "./Navigation";
Perfect. Let's import the Navigation
component to our App
component:
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">
This is the Home page πͺ
</main>
</div>
);
}
export default App;
Shall we test it? Let's do it!
npm run dev
Are you able to navigate to the routes? Yes, you are! (hopefully) π do they all the share the same nice look? no, they do not. Stick with me, I can show you how!
β¨ 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.
In case you are interested in just the very basics of the library, you are good to go. However, if you have some time available and most importantly, if you are enjoying this journey, I invite you to bear with me so we can learn more advanced concepts! What do you say? The answer is a simple:
"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, like myself, I'm pretty sure you've got the reference π Let's continue our journey, my friend!
Layout Routes
If you have navigated to other routes, you saw that each of them has an unique look and feel. That's because they do not share anything in common, apart from being called by the same navigation component.
In a real world, often we want to have a layout shared across most of the routes. This common layout is usually composed of one or more UI components, such as one navigation menu, header, footer, company logo, etc. Do we want to re-write the layout on every route? Of course not, Ctrl+C
and Ctrl+V
isn't so popular sometimes, am I right?
The new version of React Router (6.0.3) introduced a new way of sharing layouts throughout the application (which is awesome, by the way!). The concept is called Layout Routes, and it consists of nested routes being rendered in a parent route. There are many cases when it is used, such as when sharing a common layout or when decoupling parts of the page to routes containing smaller UI components.
Now, you 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 be able to note the difference? Probably not, I guesss. 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 React Router 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 the parent route and I'm not sure if children routes will always be rendered?" No worries, it won't have any impacts to the parent route component.
Back to the App
component, mate. Let's 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. We must 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 very small 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 test!
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.
No Match Route
Well, we never know what routes (aka. URLs) the user will enter. So, we need to handle the scenarios when the user enters a wrong URL, resulting in a 404 HTTP error code.
Remember on the main.tsx
that we added all the routes that we have planned? Let's get back there, and add a new route to the Routes
collection:
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 (such as project, about, contact), it will be captured by the No Match
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 it π
npm run dev
Cool, one more lesson accomplished! 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 have, and the parent route component has a Outlet
component (same as our App
component). Nothing new here so far, I know.
What if we want to render something in that Outlet space when the parent route itself is requested? Some sort of default content to be presented through the Outlet. Does that make sense?
For example, looking at our app, the main.tsx
component currently 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 it not be rendered through the App
component instead? That't when an Index
route comes to the rescue! Let me tell you when to use an Index route, please:
- "Index routes render in the parent routes outlet at the parent route's path."
- "Index routes match when a parent route matches but none of the other children match."
- "Index routes are the default child route for a parent route."
- "Index routes render when the user hasn't clicked one of the items in a navigation list yet."
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:
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 yet to show you, developer! Hopefully, you aren't getting bored, are you? π«
Active Links
How to make a link to look active? How to show the user what route is active without this having to look at the browser's URL address? Well, spoiler alert, React Router has an easy way to do this!
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, do you see that we are using a NavLink
instead of a Link
now? That's because we want to use the isActive
prop to change the style of the link. You might be wondering: "Was the swap just because of the style thing?". Well, yes π 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 NavLink
s could be a reusable component instead. But, let's stick to the purpose of this tutorial.
What is 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 we would do that? Humm, imagine the following scenario.
We have a list of users. For each user, we want to create a route to be rendered in a parent route. This unique user-route should render its own content, based on its details. If the users list consists of thousands of users, should we create one route component for each user? Not viable, right?
What we want to do instead, is to create a single user-route component only and then render the content dynamically.
Shall we create a dataset of users to illustrate our goal? 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;
}
We just have defined an interface to ensure that our users are well-formed, and added a function to get the users.
Next, we want to create a link for each user in 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, 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={`/users/${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 Users page? By "page", I mean a route and a component π
cd src
cd routes
echo > users.tsx
Then, add the basic content to the Users
route component:
import { Outlet } from "react-router-dom";
const Users = () => {
return (
<>
<h2>This is the Users page!</h2>
<Outlet />
</>
);
};
export default Users;
I'm using an Outlet
component because I want to render user specific information in that space. Same as we did previously, correct?
Now, we need a route to handle and render only user information. Let's create a User
route component:
cd src
cd routes
echo > user.tsx
Then, add the basic content to the User
route component:
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've got the route components that we need. Next, back to the main.tsx
component, we register one route for the /users
path and another one for the dynamic user IDs:
...
import User from "./routes/user";
import Users from "./routes/users";
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="users" element={<Users />}>
<Route path=":userID" element={<User />} />
</Route>
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
</React.StrictMode>
);
By the way, order does matter. The Users
route must be registered before the No Match
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 cover everything about the React Router library? Not at all.
Question: Did we cover enough for you to hit the ground running? Definitely π
What is 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
- Post's App on GitHub
- React Router v6
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!