Welcome to the tutorial! We’ll be building a small, but feature-rich app that lets you keep track of your contacts. We expect it to take between 30-60m if you’re following along.
👉 Every time you see this it means you need to do something in the app!
The rest is just there for your information and deeper understanding. Let’s get to it.
Setup
If you’re not going to follow along in your own app, you can skip this section
We’ll be using Vite for our bundler and dev server for this tutorial. You’ll need Node.js installed for the npm command line tool.
👉️ Open up your terminal and bootstrap a new React app with Vite:
npm create vite@latest name-of-your-project -- --template react
# follow prompts
cd <your new project directory>
npm install react-router-dom localforage match-sorter sort-by
npm run dev
You should be able to visit the URL printed in the terminal:
VITE v3.0.7 ready in 175 ms
➜ Local: http://127.0.0.1:5173/
➜ Network: use --host to expose
We’ve got some pre-written CSS for this tutorial so we can stay focused on React Router. Feel free to judge it harshly or write your own 😅 (We did things we normally wouldn’t in CSS so that the markup in this tutorial could stay as minimal as possible.)
👉 Copy/Paste the tutorial CSS found here into src/index.css
This tutorial will be creating, reading, searching, updating, and deleting data. A typical web app would probably be talking to an API on your web server, but we’re going to use browser storage and fake some network latency to keep this focused. None of this code is relevant to React Router, so just go ahead and copy/paste it all.
👉 Copy/Paste the tutorial data module found here into src/contacts.js
All you need in the src folder are contacts.js, main.jsx, and index.css. You can delete anything else (like App.js and assets, etc.).
👉 Delete unused files in src/ so all you have left are these:
src
├── contacts.js
├── index.css
└── main.jsx
If your app is running, it might blow up momentarily, just keep going 😋. And with that, we’re ready to get started!
Adding a Router
First thing to do is create a Browser Router and configure our first route. This will enable client side routing for our web app.
The main.jsx file is the entry point. Open it up and we’ll put React Router on the page.
👉 Create and render a browser router in main.jsx
import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import "./index.css";
const router = createBrowserRouter([
{
path: "/",
element: <div>Hello world!</div>,
},
]);
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
This first route is what we often call the «root route» since the rest of our routes will render inside of it. It will serve as the root layout of the UI, we’ll have nested layouts as we get farther along.
The Root Route
Let’s add the global layout for this app.
👉 Create src/routes and src/routes/root.jsx
mkdir src/routes
touch src/routes/root.jsx
(If you don’t want to be a command line nerd, use your editor instead of those commands 🤓)
👉 Create the root layout component
export default function Root() {
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
<form id="search-form" role="search">
<input
id="q"
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
/>
<div
id="search-spinner"
aria-hidden
hidden={true}
/>
<div
className="sr-only"
aria-live="polite"
></div>
</form>
<form method="post">
<button type="submit">New</button>
</form>
</div>
<nav>
<ul>
<li>
<a href={`/contacts/1`}>Your Name</a>
</li>
<li>
<a href={`/contacts/2`}>Your Friend</a>
</li>
</ul>
</nav>
</div>
<div id="detail"></div>
</>
);
}
Nothing React Router specific yet, so feel free to copy/paste all of that.
👉 Set <Root> as the root route’s element
/* existing imports */
import Root from "./routes/root";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
},
]);
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
The app should look something like this now. It sure is nice having a designer who can also write the CSS, isn’t it? (Thank you Jim 🙏).
Handling Not Found Errors
It’s always a good idea to know how your app responds to errors early in the project because we all write far more bugs than features when building a new app! Not only will your users get a good experience when this happens, but it helps you during development as well.
We added some links to this app, let’s see what happens when we click them?
👉 Click one of the sidebar names
Gross! This is the default error screen in React Router, made worse by our flex box styles on the root element in this app 😂.
Anytime your app throws an error while rendering, loading data, or performing data mutations, React Router will catch it and render an error screen. Let’s make our own error page.
👉 Create an error page component
touch src/error-page.jsx
import { useRouteError } from "react-router-dom";
export default function ErrorPage() {
const error = useRouteError();
console.error(error);
return (
<div id="error-page">
<h1>Oops!</h1>
<p>Sorry, an unexpected error has occurred.</p>
<p>
<i>{error.statusText || error.message}</i>
</p>
</div>
);
}
👉 Set the <ErrorPage> as the errorElement on the root route
/* previous imports */
import ErrorPage from "./error-page";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
},
]);
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
The error page should now look like this:
(Well, that’s not much better. Maybe somebody forgot to ask the designer to make an error page. Maybe everybody forgets to ask the designer to make an error page and then blames the designer for not thinking of it 😆)
Note that useRouteError provides the error that was thrown. When the user navigates to routes that don’t exist you’ll get an error response with a «Not Found» statusText. We’ll see some other errors later in the tutorial and discuss them more.
For now, it’s enough to know that pretty much all of your errors will now be handled by this page instead of infinite spinners, unresponsive pages, or blank screens 🙌
Instead of a 404 «Not Found» page, we want to actually render something at the URLs we’ve linked to. For that, we need to make a new route.
👉 Create the contact route module
touch src/routes/contact.jsx
👉 Add the contact component UI
It’s just a bunch of elements, feel free to copy/paste.
import { Form } from "react-router-dom";
export default function Contact() {
const contact = {
first: "Your",
last: "Name",
avatar: "https://placekitten.com/g/200/200",
twitter: "your_handle",
notes: "Some notes",
favorite: true,
};
return (
<div id="contact">
<div>
<img
key={contact.avatar}
src={contact.avatar || null}
/>
</div>
<div>
<h1>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}{" "}
<Favorite contact={contact} />
</h1>
{contact.twitter && (
<p>
<a
target="_blank"
href={`https://twitter.com/${contact.twitter}`}
>
{contact.twitter}
</a>
</p>
)}
{contact.notes && <p>{contact.notes}</p>}
<div>
<Form action="edit">
<button type="submit">Edit</button>
</Form>
<Form
method="post"
action="destroy"
onSubmit={(event) => {
if (
!confirm(
"Please confirm you want to delete this record."
)
) {
event.preventDefault();
}
}}
>
<button type="submit">Delete</button>
</Form>
</div>
</div>
</div>
);
}
function Favorite({ contact }) {
// yes, this is a `let` for later
let favorite = contact.favorite;
return (
<Form method="post">
<button
name="favorite"
value={favorite ? "false" : "true"}
aria-label={
favorite
? "Remove from favorites"
: "Add to favorites"
}
>
{favorite ? "★" : "☆"}
</button>
</Form>
);
}
Now that we’ve got a component, let’s hook it up to a new route.
👉 Import the contact component and create a new route
/* existing imports */
import Contact from "./routes/contact";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
},
{
path: "contacts/:contactId",
element: <Contact />,
},
]);
/* existing code */
Now if we click one of the links or visit /contacts/1 we get our new component!
However, it’s not inside of our root layout 😠
Nested Routes
We want the contact component to render inside of the <Root> layout like this.
We do it by making the contact route a child of the root route.
👉 Move the contacts route to be a child of the root route
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
},
],
},
]);
You’ll now see the root layout again but a blank page on the right. We need to tell the root route where we want it to render its child routes. We do that with <Outlet>.
Find the <div id="detail"> and put an outlet inside
👉 Render an <Outlet>
import { Outlet } from "react-router-dom";
export default function Root() {
return (
<>
{/* all the other elements */}
<div id="detail">
<Outlet />
</div>
</>
);
}
Client Side Routing
You may or may not have noticed, but when we click the links in the sidebar, the browser is doing a full document request for the next URL instead of using React Router.
Client side routing allows our app to update the URL without requesting another document from the server. Instead, the app can immediately render new UI. Let’s make it happen with <Link>.
👉 Change the sidebar <a href> to <Link to>
import { Outlet, Link } from "react-router-dom";
export default function Root() {
return (
<>
<div id="sidebar">
{/* other elements */}
<nav>
<ul>
<li>
<Link to={`contacts/1`}>Your Name</Link>
</li>
<li>
<Link to={`contacts/2`}>Your Friend</Link>
</li>
</ul>
</nav>
{/* other elements */}
</div>
</>
);
}
You can open the network tab in the browser devtools to see that it’s not requesting documents anymore.
Loading Data
URL segments, layouts, and data are more often than not coupled (tripled?) together. We can see it in this app already:
| URL Segment | Component | Data |
|---|---|---|
| / | <Root> |
list of contacts |
| contacts/:id | <Contact> |
individual contact |
Because of this natural coupling, React Router has data conventions to get data into your route components easily.
There are two APIs we’ll be using to load data, loader and useLoaderData. First we’ll create and export a loader function in the root module, then we’ll hook it up to the route. Finally, we’ll access and render the data.
👉 Export a loader from root.jsx
import { Outlet, Link } from "react-router-dom";
import { getContacts } from "../contacts";
export async function loader() {
const contacts = await getContacts();
return { contacts };
}
👉 Configure the loader on the route
/* other imports */
import Root, { loader as rootLoader } from "./routes/root";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
},
],
},
]);
👉 Access and render the data
import {
Outlet,
Link,
useLoaderData,
} from "react-router-dom";
import { getContacts } from "../contacts";
/* other code */
export default function Root() {
const { contacts } = useLoaderData();
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
{/* other code */}
<nav>
{contacts.length ? (
<ul>
{contacts.map((contact) => (
<li key={contact.id}>
<Link to={`contacts/${contact.id}`}>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}{" "}
{contact.favorite && <span>★</span>}
</Link>
</li>
))}
</ul>
) : (
<p>
<i>No contacts</i>
</p>
)}
</nav>
{/* other code */}
</div>
</>
);
}
That’s it! React Router will now automatically keep that data in sync with your UI. We don’t have any data yet, so you’re probably getting a blank list like this:
Data Writes + HTML Forms
We’ll create our first contact in a second, but first let’s talk about HTML.
React Router emulates HTML Form navigation as the data mutation primitive, according to web development before the JavaScript cambrian explosion. It gives you the UX capabilities of client rendered apps with the simplicity of the «old school» web model.
While unfamiliar to some web developers, HTML forms actually cause a navigation in the browser, just like clicking a link. The only difference is in the request: links can only change the URL while forms can also change the request method (GET vs POST) and the request body (POST form data).
Without client side routing, the browser will serialize the form’s data automatically and send it to the server as the request body for POST, and as URLSearchParams for GET. React Router does the same thing, except instead of sending the request to the server, it uses client side routing and sends it to a route action.
We can test this out by clicking the «New» button in our app. The app should blow up because the Vite server isn’t configured to handle a POST request (it sends a 404, though it should probably be a 405 🤷).
Instead of sending that POST to the Vite server to create a new contact, let’s use client side routing instead.
We’ll create new contacts by exporting an action in our root route, wiring it up to the route config, and changing our <form> to a React Router <Form>.
👉 Create the action and change <form> to <Form>
import {
Outlet,
Link,
useLoaderData,
Form,
} from "react-router-dom";
import { getContacts, createContact } from "../contacts";
export async function action() {
const contact = await createContact();
return { contact };
}
/* other code */
export default function Root() {
const { contacts } = useLoaderData();
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
{/* other code */}
<Form method="post">
<button type="submit">New</button>
</Form>
</div>
{/* other code */}
</div>
</>
);
}
👉 Import and set the action on the route
/* other imports */
import Root, {
loader as rootLoader,
action as rootAction,
} from "./routes/root";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
},
],
},
]);
That’s it! Go ahead and click the «New» button and you should see a new record pop into the list 🥳
The createContact method just creates an empty contact with no name or data or anything. But it does still create a record, promise!
🧐 Wait a sec … How did the sidebar update? Where did we call the
action? Where’s the code to refetch the data? Where areuseState,onSubmitanduseEffect?!
This is where the «old school web» programming model shows up. As we discussed earlier, <Form> prevents the browser from sending the request to the server and sends it to your route action instead. In web semantics, a POST usually means some data is changing. By convention, React Router uses this as a hint to automatically revalidate the data on the page after the action finishes. That means all of your useLoaderData hooks update and the UI stays in sync with your data automatically! Pretty cool.
URL Params in Loaders
👉 Click on the No Name record
We should be seeing our old static contact page again, with one difference: the URL now has a real ID for the record.
Reviewing the route config, the route looks like this:
[
{
path: "contacts/:contactId",
element: <Contact />,
},
];
Note the :contactId URL segment. The colon (:) has special meaning, turning it into a «dynamic segment». Dynamic segments will match dynamic (changing) values in that position of the URL, like the contact ID. We call these values in the URL «URL Params», or just «params» for short.
These params are passed to the loader with keys that match the dynamic segment. For example, our segment is named :contactId so the value will be passed as params.contactId.
These params are most often used to find a record by ID. Let’s try it out.
👉 Add a loader to the contact page and access data with useLoaderData
import { Form, useLoaderData } from "react-router-dom";
import { getContact } from "../contacts";
export async function loader({ params }) {
const contact = await getContact(params.contactId);
return { contact };
}
export default function Contact() {
const { contact } = useLoaderData();
// existing code
}
👉 Configure the loader on the route
/* existing code */
import Contact, {
loader as contactLoader,
} from "./routes/contact";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
loader: contactLoader,
},
],
},
]);
/* existing code */
Updating Data
Just like creating data, you update data with <Form>. Let’s make a new route at contacts/:contactId/edit. Again, we’ll start with the component and then wire it up to the route config.
👉 Create the edit component
touch src/routes/edit.jsx
👉 Add the edit page UI
Nothing we haven’t seen before, feel free to copy/paste:
import { Form, useLoaderData } from "react-router-dom";
export default function EditContact() {
const { contact } = useLoaderData();
return (
<Form method="post" id="contact-form">
<p>
<span>Name</span>
<input
placeholder="First"
aria-label="First name"
type="text"
name="first"
defaultValue={contact.first}
/>
<input
placeholder="Last"
aria-label="Last name"
type="text"
name="last"
defaultValue={contact.last}
/>
</p>
<label>
<span>Twitter</span>
<input
type="text"
name="twitter"
placeholder="@jack"
defaultValue={contact.twitter}
/>
</label>
<label>
<span>Avatar URL</span>
<input
placeholder="https://example.com/avatar.jpg"
aria-label="Avatar URL"
type="text"
name="avatar"
defaultValue={contact.avatar}
/>
</label>
<label>
<span>Notes</span>
<textarea
name="notes"
defaultValue={contact.notes}
rows={6}
/>
</label>
<p>
<button type="submit">Save</button>
<button type="button">Cancel</button>
</p>
</Form>
);
}
👉 Add the new edit route
/* existing code */
import EditContact from "./routes/edit";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
loader: contactLoader,
},
{
path: "contacts/:contactId/edit",
element: <EditContact />,
loader: contactLoader,
},
],
},
]);
/* existing code */
We want it to be rendered in the root route’s outlet, so we made it a sibling to the existing child route.
(You might note we reused the contactLoader for this route. This is only because we’re being lazy in the tutorial. There is no reason to attempt to share loaders among routes, they usually have their own.)
Alright, clicking the «Edit» button gives us this new UI:
The edit route we just created already renders a form. All we need to do to update the record is wire up an action to the route. The form will post to the action and the data will be automatically revalidated.
👉 Add an action to the edit module
import {
Form,
useLoaderData,
redirect,
} from "react-router-dom";
import { updateContact } from "../contacts";
export async function action({ request, params }) {
const formData = await request.formData();
const updates = Object.fromEntries(formData);
await updateContact(params.contactId, updates);
return redirect(`/contacts/${params.contactId}`);
}
/* existing code */
👉 Wire the action up to the route
/* existing code */
import EditContact, {
action as editAction,
} from "./routes/edit";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
loader: contactLoader,
},
{
path: "contacts/:contactId/edit",
element: <EditContact />,
loader: contactLoader,
action: editAction,
},
],
},
]);
/* existing code */
Fill out the form, hit save, and you should see something like this! (Except easier on the eyes and maybe less hairy.)
Mutation Discussion
😑 It worked, but I have no idea what is going on here…
Let’s dig in a bit…
Open up src/routes/edit.jsx and look at the form elements. Notice how they each have a name:
<input
placeholder="First"
aria-label="First name"
type="text"
name="first"
defaultValue={contact.first}
/>
Without JavaScript, when a form is submitted, the browser will create FormData and set it as the body of the request when it sends it to the server. As mentioned before, React Router prevents that and sends the request to your action instead, including the FormData.
Each field in the form is accessible with formData.get(name). For example, given the input field from above, you could access the first and last names like this:
export async function action({ request, params }) {
const formData = await request.formData();
const firstName = formData.get("first");
const lastName = formData.get("last");
// ...
}
Since we have a handful of form fields, we used Object.fromEntries to collect them all into an object, which is exactly what our updateContact function wants.
const updates = Object.fromEntries(formData);
updates.first; // "Some"
updates.last; // "Name"
Aside from action, none of these APIs we’re discussing are provided by React Router: request, request.formData, Object.fromEntries are all provided by the web platform.
After we finished the action, note the redirect at the end:
export async function action({ request, params }) {
const formData = await request.formData();
const updates = Object.fromEntries(formData);
await updateContact(params.contactId, updates);
return redirect(`/contacts/${params.contactId}`);
}
Loaders and actions can both return a Response (makes sense, since they received a Request!). The redirect helper just makes it easier to return a response that tells the app to change locations.
Without client side routing, if a server redirected after a POST request, the new page would fetch the latest data and render. As we learned before, React Router emulates this model and automatically revalidates the data on the page after the action. That’s why the sidebar automatically updates when we save the form. The extra revalidation code doesn’t exist without client side routing, so it doesn’t need to exist with client side routing either!
Redirecting new records to the edit page
Now that we know how to redirect, let’s update the action that creates new contacts to redirect to the edit page:
👉 Redirect to the new record’s edit page
import {
Outlet,
Link,
useLoaderData,
Form,
redirect,
} from "react-router-dom";
import { getContacts, createContact } from "../contacts";
export async function action() {
const contact = await createContact();
return redirect(`/contacts/${contact.id}/edit`);
}
Now when we click «New», we should end up on the edit page:
👉 Add a handful of records
I’m going to use the stellar lineup of speakers from the first Remix Conference 😁
Active Link Styling
Now that we have a bunch of records, it’s not clear which one we’re looking at in the sidebar. We can use NavLink to fix this.
👉 Use a NavLink in the sidebar
import {
Outlet,
NavLink,
useLoaderData,
Form,
redirect,
} from "react-router-dom";
export default function Root() {
return (
<>
<div id="sidebar">
{/* other code */}
<nav>
{contacts.length ? (
<ul>
{contacts.map((contact) => (
<li key={contact.id}>
<NavLink
to={`contacts/${contact.id}`}
className={({ isActive, isPending }) =>
isActive
? "active"
: isPending
? "pending"
: ""
}
>
{/* other code */}
</NavLink>
</li>
))}
</ul>
) : (
<p>{/* other code */}</p>
)}
</nav>
</div>
</>
);
}
Note that we are passing a function to className. When the user is at the URL in the NavLink, then isActive will be true. When it’s about to be active (the data is still loading) then isPending will be true. This allows us to easily indicate where the user is, as well as provide immediate feedback on links that have been clicked but we’re still waiting for data to load.
Global Pending UI
As the user navigates the app, React Router will leave the old page up as data is loading for the next page. You may have noticed the app feels a little unresponsive as you click between the list. Let’s provide the user with some feedback so the app doesn’t feel unresponsive.
React Router is managing all of the state behind the scenes and reveals the pieces of it you need to build dynamic web apps. In this case, we’ll use the useNavigation hook.
👉 useNavigation to add global pending UI
import {
// existing code
useNavigation,
} from "react-router-dom";
// existing code
export default function Root() {
const { contacts } = useLoaderData();
const navigation = useNavigation();
return (
<>
<div id="sidebar">{/* existing code */}</div>
<div
id="detail"
className={
navigation.state === "loading" ? "loading" : ""
}
>
<Outlet />
</div>
</>
);
}
useNavigation returns the current navigation state: it can be one of "idle" | "submitting" | "loading".
In our case, we add a "loading" class to the main part of the app if we’re not idle. The CSS then adds a nice fade after a short delay (to avoid flickering the UI for fast loads). You could do anything you want though, like show a spinner or loading bar across the top.
Note that our data model (src/contacts.js) has a clientside cache, so navigating to the same contact is fast the second time. This behavior is not React Router, it will re-load data for changing routes no matter if you’ve been there before or not. It does, however, avoid calling the loaders for unchanging routes (like the list) during a navigation.
Deleting Records
If we review code in the contact route, we can find the delete button looks like this:
<Form
method="post"
action="destroy"
onSubmit={(event) => {
if (
!confirm(
"Please confirm you want to delete this record."
)
) {
event.preventDefault();
}
}}
>
<button type="submit">Delete</button>
</Form>
Note the action points to "destroy". Like <Link to>, <Form action> can take a relative value. Since the form is rendered in contact/:contactId, then a relative action with destroy will submit the form to contact/:contactId/destroy when clicked.
At this point you should know everything you need to know to make the delete button work. Maybe give it a shot before moving on? You’ll need:
- A new route
- An
actionat that route deleteContactfromsrc/contacts.js
👉 Create the «destroy» route module
touch src/routes/destroy.jsx
👉 Add the destroy action
import { redirect } from "react-router-dom";
import { deleteContact } from "../contacts";
export async function action({ params }) {
await deleteContact(params.contactId);
return redirect("/");
}
👉 Add the destroy route to the route config
/* existing code */
import { action as destroyAction } from "./routes/destroy";
const router = createBrowserRouter([
{
path: "/",
/* existing root route props */
children: [
/* existing routes */
{
path: "contacts/:contactId/destroy",
action: destroyAction,
},
],
},
]);
/* existing code */
Alright, navigate to a record and click the «Delete» button. It works!
😅 I’m still confused why this all works
When the user clicks the submit button:
<Form>prevents the default browser behavior of sending a new POST request to the server, but instead emulates the browser by creating a POST request with client side routing- The
<Form action="destroy">matches the new route at"contacts/:contactId/destroy"and sends it the request - After the action redirects, React Router calls all of the loaders for the data on the page to get the latest values (this is «revalidation»).
useLoaderDatareturns new values and causes the components to update!
Add a form, add an action, React Router does the rest.
Contextual Errors
Just for kicks, throw an error in the destroy action:
export async function action({ params }) {
throw new Error("oh dang!");
await deleteContact(params.contactId);
return redirect("/");
}
Recognize that screen? It’s our errorElement from before. The user, however, can’t really do anything to recover from this screen except to hit refresh.
Let’s create a contextual error message for the destroy route:
[
/* other routes */
{
path: "contacts/:contactId/destroy",
action: destroyAction,
errorElement: <div>Oops! There was an error.</div>,
},
];
Now try it again:
Our user now has more options than slamming refresh, they can continue to interact with the parts of the page that aren’t having trouble 🙌
Because the destroy route has its own errorElement and is a child of the root route, the error will render there instead of the root. As you probably noticed, these errors bubble up to the nearest errorElement. Add as many or as few as you like, as long as you’ve got one at the root.
Index Routes
When we load up the app, you’ll notice a big blank page on the right side of our list.
When a route has children, and you’re at the parent route’s path, the <Outlet> has nothing to render because no children match. You can think of index routes as the default child route to fill in that space.
👉 Create the index route module
touch src/routes/index.jsx
👉 Fill in the index component’s elements
Feel free to copy paste, nothing special here.
export default function Index() {
return (
<p id="zero-state">
This is a demo for React Router.
<br />
Check out{" "}
<a href="https://reactrouter.com">
the docs at reactrouter.com
</a>
.
</p>
);
}
👉 Configure the index route
// existing code
import Index from "./routes/index";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
action: rootAction,
children: [
{ index: true, element: <Index /> },
/* existing routes */
],
},
]);
Note the { index:true } instead of { path: "" }. That tells the router to match and render this route when the user is at the parent route’s exact path, so there are no other child routes to render in the <Outlet>.
Voila! No more blank space. It’s common to put dashboards, stats, feeds, etc. at index routes. They can participate in data loading as well.
Cancel Button
On the edit page we’ve got a cancel button that doesn’t do anything yet. We’d like it to do the same thing as the browser’s back button.
We’ll need a click handler on the button as well as useNavigate from React Router.
👉 Add the cancel button click handler with useNavigate
import {
Form,
useLoaderData,
redirect,
useNavigate,
} from "react-router-dom";
export default function EditContact() {
const { contact } = useLoaderData();
const navigate = useNavigate();
return (
<Form method="post" id="contact-form">
{/* existing code */}
<p>
<button type="submit">Save</button>
<button
type="button"
onClick={() => {
navigate(-1);
}}
>
Cancel
</button>
</p>
</Form>
);
}
Now when the user clicks «Cancel», they’ll be sent back one entry in the browser’s history.
🧐 Why is there no
event.preventDefaulton the button?
A <button type="button">, while seemingly redundant, is the HTML way of preventing a button from submitting its form.
Two more features to go. We’re on the home stretch!
URL Search Params and GET Submissions
All of our interactive UI so far have been either links that change the URL or forms that post data to actions. The search field is interesting because it’s a mix of both: it’s a form but it only changes the URL, it doesn’t change data.
Right now it’s just a normal HTML <form>, not a React Router <Form>. Let’s see what the browser does with it by default:
👉 Type a name into the search field and hit the enter key
Note the browser’s URL now contains your query in the URL as URLSearchParams:
http://127.0.0.1:5173/?q=ryan
If we review the search form, it looks like this:
<form id="search-form" role="search">
<input
id="q"
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
/>
<div id="search-spinner" aria-hidden hidden={true} />
<div className="sr-only" aria-live="polite"></div>
</form>
As we’ve seen before, browsers can serialize forms by the name attribute of it’s input elements. The name of this input is q, that’s why the URL has ?q=. If we named it search the URL would be ?search=.
Note that this form is different from the others we’ve used, it does not have <form method="post">. The default method is "get". That means when the browser creates the request for the next document, it doesn’t put the form data into the request POST body, but into the URLSearchParams of a GET request.
GET Submissions with Client Side Routing
Let’s use client side routing to submit this form and filter the list in our existing loader.
👉 Change <form> to <Form>
<Form id="search-form" role="search">
<input
id="q"
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
/>
<div id="search-spinner" aria-hidden hidden={true} />
<div className="sr-only" aria-live="polite"></div>
</Form>
👉 Filter the list if there are URLSearchParams
export async function loader({ request }) {
const url = new URL(request.url);
const q = url.searchParams.get("q");
const contacts = await getContacts(q);
return { contacts };
}
Because this is a GET, not a POST, React Router does not call the action. Submitting a GET form is the same as clicking a link: only the URL changes. That’s why the code we added for filtering is in the loader, not the action of this route.
This also means it’s a normal page navigation. You can click the back button to get back to where you were.
Synchronizing URLs to Form State
There are a couple of UX issues here that we can take care of quickly.
- If you click back after a search, the form field still has the value you entered even though the list is no longer filtered.
- If you refresh the page after searching, the form field no longer has the value in it, even though the list is filtered
In other words, the URL and our form state are out of sync.
👉 Return q from your loader and set it as the search field default value
// existing code
export async function loader({ request }) {
const url = new URL(request.url);
const q = url.searchParams.get("q");
const contacts = await getContacts(q);
return { contacts, q };
}
export default function Root() {
const { contacts, q } = useLoaderData();
const navigation = useNavigation();
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
<Form id="search-form" role="search">
<input
id="q"
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
defaultValue={q}
/>
{/* existing code */}
</Form>
{/* existing code */}
</div>
{/* existing code */}
</div>
{/* existing code */}
</>
);
}
That solves problem (2). If you refresh the page now, the input field will show the query.
Now for problem (1), clicking the back button and updating the input. We can bring in useEffect from React to manipulate the form’s state in the DOM directly.
👉 Synchronize input value with the URL Search Params
import { useEffect } from "react";
// existing code
export default function Root() {
const { contacts, q } = useLoaderData();
const navigation = useNavigation();
useEffect(() => {
document.getElementById("q").value = q;
}, [q]);
// existing code
}
🤔 Shouldn’t you use a controlled component and React State for this?
You could certainly do this as a controlled component, but you’ll end up with more complexity for the same behavior. You don’t control the URL, the user does with the back/forward buttons. There would be more synchronization points with a controlled component.
If you’re still concerned, expand this to see what it would look like
Notice how controlling the input requires three points of synchronization now instead of just one. The behavior is identical but the code is more complex.
import { useEffect, useState } from "react";
// existing code
export async function loader({ request }) {
const url = new URL(request.url);
const q = url.searchParams.get("q") || "";
const contacts = await getContacts(q);
return { contacts, q };
}
// existing code
export default function Root() {
const { contacts, q } = useLoaderData();
const [query, setQuery] = useState(q);
const navigation = useNavigation();
useEffect(() => {
setQuery(q);
}, [q]);
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
<Form id="search-form" role="search">
<input
id="q"
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
value={query}
onChange={(e) => {
setQuery(e.target.value);
}}
/>
{/* existing code */}
</Form>
{/* existing code */}
</div>
{/* existing code */}
</div>
</>
);
}
Submitting Forms onChange
We’ve got a product decision to make here. For this UI, we’d probably rather have the filtering happen on every key stroke instead of when the form is explicitly submitted.
We’ve seen useNavigate already, we’ll use its cousin, useSubmit, for this.
// existing code
import {
// existing code
useSubmit,
} from "react-router-dom";
export default function Root() {
const { contacts, q } = useLoaderData();
const navigation = useNavigation();
const submit = useSubmit();
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
<Form id="search-form" role="search">
<input
id="q"
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
defaultValue={q}
onChange={(event) => {
submit(event.currentTarget.form);
}}
/>
{/* existing code */}
</Form>
{/* existing code */}
</div>
{/* existing code */}
</div>
{/* existing code */}
</>
);
}
Now as you type, the form is submitted automatically!
Note the argument to submit. We’re passing in event.currentTarget.form. The currentTarget is the DOM node the event is attached to, and the currentTarget.form is the input’s parent form node. The submit function will serialize and submit any form you pass to it.
Adding Search Spinner
In a production app, it’s likely this search will be looking for records in a database that is too large to send all at once and filter client side. That’s why this demo has some faked network latency.
Without any loading indicator, the search feels kinda sluggish. Even if we could make our database faster, we’ll always have the user’s network latency in the way and out of our control. For a better UX, let’s add some immediate UI feedback for the search. For this we’ll use useNavigation again.
👉 Add the search spinner
// existing code
export default function Root() {
const { contacts, q } = useLoaderData();
const navigation = useNavigation();
const submit = useSubmit();
const searching =
navigation.location &&
new URLSearchParams(navigation.location.search).has(
"q"
);
useEffect(() => {
document.getElementById("q").value = q;
}, [q]);
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
<Form id="search-form" role="search">
<input
id="q"
className={searching ? "loading" : ""}
// existing code
/>
<div
id="search-spinner"
aria-hidden
hidden={!searching}
/>
{/* existing code */}
</Form>
{/* existing code */}
</div>
{/* existing code */}
</div>
{/* existing code */}
</>
);
}
The navigation.location will show up when the app is navigating to a new URL and loading the data for it. It then goes away when there is no pending navigation anymore.
Managing the History Stack
Now that the form is submitted for every key stroke, if we type the characters «seba» and then delete them with backspace, we end up with 7 new entries in the stack 😂. We definitely don’t want this
We can avoid this by replacing the current entry in the history stack with the next page, instead of pushing into it.
👉 Use replace in submit
// existing code
export default function Root() {
// existing code
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
<Form id="search-form" role="search">
<input
id="q"
// existing code
onChange={(event) => {
const isFirstSearch = q == null;
submit(event.currentTarget.form, {
replace: !isFirstSearch,
});
}}
/>
{/* existing code */}
</Form>
{/* existing code */}
</div>
{/* existing code */}
</div>
{/* existing code */}
</>
);
}
We only want to replace search results, not the page before we started searching, so we do a quick check if this is the first search or not and then decide to replace.
Each key stroke no longer creates new entries, so the user can click back out of the search results without having to click it 7 times 😅.
Mutations Without Navigation
So far all of our mutations (the times we change data) have used forms that navigate, creating new entries in the history stack. While these user flows are common, it’s equally as common to want to change data without causing a navigation.
For these cases, we have the useFetcher hook. It allows us to communicate with loaders and actions without causing a navigation.
The ★ button on the contact page makes sense for this. We aren’t creating or deleting a new record, we don’t want to change pages, we simply want to change the data on the page we’re looking at.
👉 Change the <Favorite> form to a fetcher form
import {
useLoaderData,
Form,
useFetcher,
} from "react-router-dom";
// existing code
function Favorite({ contact }) {
const fetcher = useFetcher();
let favorite = contact.favorite;
return (
<fetcher.Form method="post">
<button
name="favorite"
value={favorite ? "false" : "true"}
aria-label={
favorite
? "Remove from favorites"
: "Add to favorites"
}
>
{favorite ? "★" : "☆"}
</button>
</fetcher.Form>
);
}
Might want to take a look at that form while we’re here. As always, our form has fields with a name prop. This form will send formData with a favorite key that’s either "true" | "false". Since it’s got method="post" it will call the action. Since there is no <fetcher.Form action="..."> prop, it will post to the route where the form is rendered.
👉 Create the action
// existing code
import { getContact, updateContact } from "../contacts";
export async function action({ request, params }) {
let formData = await request.formData();
return updateContact(params.contactId, {
favorite: formData.get("favorite") === "true",
});
}
export default function Contact() {
// existing code
}
Pretty simple. Pull the form data off the request and send it to the data model.
👉 Configure the route’s new action
// existing code
import Contact, {
loader as contactLoader,
action as contactAction,
} from "./routes/contact";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
action: rootAction,
children: [
{ index: true, element: <Index /> },
{
path: "contacts/:contactId",
element: <Contact />,
loader: contactLoader,
action: contactAction,
},
/* existing code */
],
},
]);
Alright, we’re ready to click the star next to the user’s name!
Check that out, both stars automatically update. Our new <fetcher.Form method="post"> works almost exactly like the <Form> we’ve been using: it calls the action and then all data is revalidated automatically—even your errors will be caught the same way.
There is one key difference though, it’s not a navigation—the URL doesn’t change, the history stack is unaffected.
Optimistic UI
You probably noticed the app felt kind of unresponsive when we clicked the favorite button from the last section. Once again, we added some network latency because you’re going to have it in the real world!
To give the user some feedback, we could put the star into a loading state with fetcher.state (a lot like navigation.state from before), but we can do something even better this time. We can use a strategy called «optimistic UI»
The fetcher knows the form data being submitted to the action, so it’s available to you on fetcher.formData. We’ll use that to immediately update the star’s state, even though the network hasn’t finished. If the update eventually fails, the UI will revert to the real data.
👉 Read the optimistic value from fetcher.formData
// existing code
function Favorite({ contact }) {
const fetcher = useFetcher();
let favorite = contact.favorite;
if (fetcher.formData) {
favorite = fetcher.formData.get("favorite") === "true";
}
return (
<fetcher.Form method="post">
<button
name="favorite"
value={favorite ? "false" : "true"}
aria-label={
favorite
? "Remove from favorites"
: "Add to favorites"
}
>
{favorite ? "★" : "☆"}
</button>
</fetcher.Form>
);
}
If you click the button now you should see the star immediately change to the new state. Instead of always rendering the actual data, we check if the fetcher has any formData being submitted, if so, we’ll use that instead. When the action is done, the fetcher.formData will no longer exist and we’re back to using the actual data. So even if you write bugs in your optimistic UI code, it’ll eventually go back to the correct state 🥹
Not Found Data
What happens if the contact we’re trying to load doesn’t exist?
Our root errorElement is catching this unexpected error as we try to render a null contact. Nice the error was properly handled, but we can do better!
Whenever you have an expected error case in a loader or action–like the data not existing–you can throw. The call stack will break, React Router will catch it, and the error path is rendered instead. We won’t even try to render a null contact.
👉 Throw a 404 response in the loader
export async function loader({ params }) {
const contact = await getContact(params.contactId);
if (!contact) {
throw new Response("", {
status: 404,
statusText: "Not Found",
});
}
return { contact };
}
Instead of hitting a render error with Cannot read properties of null, we avoid the component completely and render the error path instead, telling the user something more specific.
This keeps your happy paths, happy. Your route elements don’t need to concern themselves with error and loading states.
Pathless Routes
One last thing. The last error page we saw would be better if it rendered inside the root outlet, instead of the whole page. In fact, every error in all of our child routes would be better in the outlet, then the user has more options than hitting refresh.
We’d like it to look like this:
We could add the error element to every one of the child routes but, since it’s all the same error page, this isn’t recommended.
There’s a cleaner way. Routes can be used without a path, which lets them participate in the UI layout without requiring new path segments in the URL. Check it out:
👉 Wrap the child routes in a pathless route
createBrowserRouter([
{
path: "/",
element: <Root />,
loader: rootLoader,
action: rootAction,
errorElement: <ErrorPage />,
children: [
{
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Index /> },
{
path: "contacts/:contactId",
element: <Contact />,
loader: contactLoader,
action: contactAction,
},
/* the rest of the routes */
],
},
],
},
]);
When any errors are thrown in the child routes, our new pathless route will catch it and render, preserving the root route’s UI!
JSX Routes
And for our final trick, many folks prefer to configure their routes with JSX. You can do that with createRoutesFromElements. There is no functional difference between JSX or objects when configuring your routes, it’s simply a stylistic preference.
import {
createRoutesFromElements,
createBrowserRouter,
Route,
} from "react-router-dom";
const router = createBrowserRouter(
createRoutesFromElements(
<Route
path="/"
element={<Root />}
loader={rootLoader}
action={rootAction}
errorElement={<ErrorPage />}
>
<Route errorElement={<ErrorPage />}>
<Route index element={<Index />} />
<Route
path="contacts/:contactId"
element={<Contact />}
loader={contactLoader}
action={contactAction}
/>
<Route
path="contacts/:contactId/edit"
element={<EditContact />}
loader={contactLoader}
action={editAction}
/>
<Route
path="contacts/:contactId/destroy"
action={destroyAction}
/>
</Route>
</Route>
)
);
That’s it! Thanks for giving React Router a shot. We hope this tutorial gives you a solid start to build great user experiences. There’s a lot more you can do with React Router, so make sure to check out all the APIs 😀
Are you looking to learn how to set up React Router v6 and start routing your React applications? This guide will show you step-by-step how to setup React Router v6 and start building dynamic, single-page applications. With React Router v6, you can create powerful and dynamic routing solutions with ease.
React Router v6.4.0 is a library for routing in React applications and provides a routing solution that allows developers to declaratively map routes to components and manage the URL and navigation state of the application. It also provides features like route preloading, lazy loading, and query parameters.
Routing allows the user to navigate through different pages of your app without refreshing the whole page. With the help of routing, you can create a single-page application (SPA) that allows users to navigate through different pages of your web app without refreshing the whole page and improve the overall user experience by making your application faster, more responsive, and more dynamic.
Step-by-Step Guide to Setup React Router
Follow the setup instructions carefully to add an easy navigation feature to your React app with different pages.
Step 1: Installing React Router
To install React Router, you’ll need Node.js installed for the npm command line tool, after which you have to run the following command:
npm install react-router-dom
and wait for the installation to complete.
If you are using Yarn then run the following command:
yarn add react-router-dom
Step 2: Adding React Router
The first thing to do after installing react-router-dom is to make React Router available anywhere in your app. For this, we use a <BrowserRouter> that stores the current location in the browser’s address bar using clean URLs and navigates using the browser’s built-in history stack.
To do this, open the index.js file in the src folder and replace the <React.StrictMode> with <BrowserRouter> by importing it from react-router-dom and then wrapping the root component in it.
index.js file before changes:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
reportWebVitals();
index.js file after changes:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
reportWebVitals();
So, now you can access all the features of react-router-dom like routers, routes, etc.
Step 3: Routing and Rendering Components
1. Creating Multiple Components
To render different components for routing, we’ll create some components in our project. In your project src folder, create a new folder as Pages. In that, create some components such as Home.js, Courses.js, Live.js, and Contact.js.
See the code for all the components below:
Home.js
import React from 'react'
function Home() {
return (
<div>Home</div>
)
}
export default Home
Contact.js
import React from 'react'
function Contact() {
return (
<div>Contact</div>
)
}
export default Contact
Courses.js
import React from 'react'
function Courses() {
return (
<div>Courses</div>
)
}
export default Courses
Live.js
import React from 'react'
function Live() {
return (
<div>Live</div>
)
}
export default Live
After creating all the component files, the project directory should look like this:
2. Define routes
Our root component is the App.js component, where our React code gets rendered initially. We will be creating all our routes in it.
To define the routes, we are going to use two things: Routes and Route. Import both components from react-router-dom and use them to route the components. The syntax for defining the routes is as follows:
<Routes>
<Route path="/" element={<Home />} />
<Route path="/course" element={<Courses />} />
<Route path="/live" element={<Live />} />
<Route path="/contact" element={<Contact />} />
</Routes>
Let’s understand this syntax in detail:
- <Routes>- The <Routes> component is used to define the different routes that are available in your application. Each individual route is defined using a <Route> component.
- <Route>- The <Route> component takes in a path prop, which is a string that defines the URL path that the route should match.
- path- The path prop defines the route path.
- element- The component prop defines the React component to render when the route path matches the current URL.
3. Link to Navigate to Components
Now we will use the <Link> component of react-router-dom to create clickable links that change the URL of the browser and render a different component based on the new URL.
This component takes in a to prop, which is a string that defines the URL path that the link should navigate to, and when the user clicks on a <Link> component, the URL of the browser will change to the path specified in the to prop, and the appropriate component associated with that route will be rendered.
In our example, we have defined a route with a path prop of ‘/contact’, therefore we can create a <Link> component with a to prop of ‘/contact’ to allow the user to navigate to the contact page. The same procedure goes for all of the components to navigate through the different pages.
We will now use <Link> to navigate to different pages based on the routes and pathnames we have defined in the App component as shown below:
import "./App.css";
import React from "react";
import { Link } from "react-router-dom";
import Home from "../src/Pages/Home";
import Courses from "../src/Pages/Courses";
import Live from "../src/Pages/Live";
import Contact from "../src/Pages/Contact";
function App() {
return (
<nav>
<ul>
<Link to="/" class="list">
Home
</Link>
<Link to="/course" class="list">
Courses
</Link>
<Link to="/live" class="list">
Live course
</Link>
<Link to="/contact" class="list">
Contact
</Link>
</ul>
</nav>
);
}
export default App;
Final Code
If you followed and coded along with us through the blog, then your App.js file should look like this:
import "./App.css";
import React from "react";
import { Link, Route, Routes } from "react-router-dom";
import Home from "../src/Pages/Home";
import Courses from "../src/Pages/Courses";
import Live from "../src/Pages/Live";
import Contact from "../src/Pages/Contact";
function App() {
return (
<div className="container">
<nav>
<ul>
<Link to="/" class="list">
Home
</Link>
<Link to="/course" class="list">
Courses
</Link>
<Link to="/live" class="list">
Live course
</Link>
<Link to="/contact" class="list">
Contact
</Link>
</ul>
</nav>
{/* Defining routes path and rendering components as element */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/course" element={<Courses />} />
<Route path="/live" element={<Live />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</div>
);
}
export default App;
Output
Conclusion
Setting up React Router in your application is a straightforward process that can greatly enhance the user experience of your app. With React Router, you can easily define and manage routes, and create clickable links that change the URL without refreshing the whole page.
At this point, you have seen how setup React Router to navigate to different pages in your web application. Not only this, but you also saw how routing works and rendered the components in react.
By following these simple steps, you can add powerful routing functionality to your React application, allowing the users to easily navigate through different pages of your web app. Hope you learned a lot from this blog but don’t stop learning here!
Are you interested in Building an Application using React?
Learn to Build an App using React in just 90 days through Zen Class Career Program & Become a Full-stack Developer with 100% Job Placement Support.
Are you ready to take your coding skills to the next level? Then join our Full Stack Development Bootcamp by Zen Class and become a master in the art of full stack. We’ll teach you the latest technologies and best practices to help you become a full-stack developer.
Our comprehensive course will cover HTML, CSS, JavaScript, React, Node.js, MongoDB, and more. With our expert instructor, mentors from product-based companies, and hands-on real-world projects, you’ll be able to build dynamic web applications and take your coding career to the next level.
Sign up today and unleash your coding potential!
FAQs
Q1. How do I add react router v6?
Ans. Here are the few steps to add react router v6:
Step 1: Installing React Router.
Step 2: Adding React Router.
Step 3: Routing and Rendering components
Q2. What is react router v6?
Ans. React Router v6 is a popular and powerful routing library for React applications. It provides a declarative, component-based approach to routing and handles the common tasks of dealing with URL params, redirects, and loading data.
Q3. How to install react router dom in your application?
Ans. You just need to type the command npm install react-router-dom@6 in the preferred location in the console window and hit enter. This installs the react router dom of version 6 in your application and you’re good to go.
Время на прочтение
8 мин
Количество просмотров 406K
Автор @pshrmn ⬝ Оригинальная статья ⬝
Время чтения
: 10 минут
React Router v4 — это переработанный вариант популярного React дополнения. Зависимые от платформы конфигурации роутов из прошлой версии были удалены и теперь всё является простыми компонентами.
Этот туториал покрывает всё что вам нужно для создания веб-сайтов с React Router. Мы будем создавать сайт для локальной спортивной команды.
Хочешь посмотреть демку?
Установка
React Router v4 был разбит на 3 пакета:
react-router
router-dom
react-router-native
react-router предоставляет базовые функции и компоненты для работы в двух окружениях(Браузере и react-native)
Мы будем создавать сайт который будет отображаться в браузере, поэтому нам следует использовать react-router-dom. react-router-dom экспортирует из react-router все функции поэтому нам нужно установить только react-router-dom.
npm install --save react-router-dom
Router
При старте проекта вам нужно определить какой тип роутера использовать. Для браузерных проектов есть BrowserRouter и HashRouter компоненты. BrowserRouter — следует использовать когда вы обрабатываете на сервере динамические запросы, а HashRouter используйте когда у вас статический веб сайт.
Обычно предпочтительнее использовать BrowserRouter, но если ваш сайт расположен на статическом сервере(от перев. как github pages), то использовать HashRouter это хорошее решение проблемы.
Наш проект предполагает использование бекенда поэтому мы будем использовать BrowserRouter.
История — History
Каждый Router создает объект history который хранит путь к текущему location[1] и перерисовывает интерфейс сайта когда происходят какие то изменения пути.
Остальные функции предоставляемые в React Router полагаются на доступность объекта history через context, поэтому они должны рендериться внутри компонента Router.
Заметка: Компоненты React Router не имеющие в качестве предка компонент Router не будут работать, так как не будет доступен context.
Рендеринг Router
Компонент Router ожидает только один элемент в качестве дочернего. Что бы работать в рамках этого условия, удобно создать компонент <App/> который рендерить всё ваше приложение(это так же важно для серверного рендеринга).
import { BrowserRouter } from 'react-router-dom';
ReactDOM.render((
<BrowserRouter>
<App />
</BrowserRouter>
), document.getElementById('root'))
App компонент
Наше приложение начинается с <App/> компонента который мы разделим на две части. <Header/> который будет содержать навигационные ссылки и <Main/> который будет содержать контент роутов.
// Этот компонент будет отрендерен с помощью нашего <Router>
const App = () => (
<div>
<Header />
<Main />
</div>
)
Routes
<Route/> компонент это главный строительный блок React Router’а. В том случае если вам нужно рендерить элемент в зависимости от pathname URL’ов, то следует использовать компонент <Route/>
Path — путь
<Route /> принимает path в виде prop который описывает определенный путь и сопоставляется с location.pathname.
<Route path='/roster'/>
В примере выше <Route/> сопоставляет location.pathname который начинается с /roster[2]. Когда текущий location.pathname сопоставляется положительно с prop path то компонент будет отрендерен, а если мы не можем их сопоставить, то Route ничего не рендерит[3].
<Route path='/roster'/>
// Когда location.pathname это '/', prop path не совпадает
// Когда location.pathname это '/roster' или '/roster/2', prop path совпадает
// Если установлен exact prop. Совпадает только строгое сравнение '/roster', но не
// '/roster/2'
<Route exact path='/roster'/>
Заметка: Когда речь идет о пути React Router думает только о пути без домена. Это значит, что в адресе:
http://www.example.com/my-projects/one?extra=false
React Router будет видеть только /my-projects/one
Сопоставление пути
npm пакет path-to-regexp компилирует prop path в регулярное выражение и сопоставляет его против location.pathname. Строки path имеют более сложные опции форматирования чем объясняются здесь. Вы можете почитать документацию.
Когда пути сопоставляются создается объект match который содержит свойства:
- url — сопоставляемая часть текущего location.pathname
- path — путь в компоненте Route
- isExact — path в Route === location.pathname
- params — объект содержит значения из path которые возвращает модуль path-to-regexp
Заметка: Можете поиграться с тестером роутов и посмотреть как создается объект match.
Заметка: path в Route должен быть абсолютным[4].
Создание наших роутов
Компонент Route может быть в любом месте в роутере, но иногда нужно определять, что рендерить в одно и тоже место. В таком случае следует использовать компонент группирования Route’ов — <Switch/>. <Switch/> итеративно проходит по дочерним компонентам и рендерит только первый который подходит под location.pathname.
У нашего веб-сайта пути которые мы хотим сопоставлять такие:
- / — Главная страница
- /roster — Страница команд
- /roster/:number — Страница профиля игрока по номеру
- /schedule — Расписание игр команды
По порядку сопоставления путей в нашем приложении, все что нам нужно сделать это создать компонент Route с prop path который мы хотим сопоставить.
<Switch>
<Route exact path='/' component={Home}/>
{/* Оба /roster и /roster/:number начинаются с /roster */}
<Route path='/roster' component={Roster}/>
<Route path='/schedule' component={Schedule}/>
</Switch>
Что делает рендер компонента Route?
У Route есть 3 props’a которые описывают каким образом выполнить рендер сопоставляя prop path с location.pathname и только один из prop должен быть представлен в Route:
- component — React компонент. Когда роут удовлетворяется сопоставление в path, то он возвращает переданный component (используя функцию React.createElement).
- render — функция которая должна вернуть элемент React. Будет вызвана когда удовлетворится сопоставление в path. Render довольно похож на component, но используется для inline рендеринга и подстановки необходимых для элемента props[5].
- children — в отличие от предыдущих двух props children будет всегда отображаться независимо от того сопоставляется ли path или нет.
<Route path='/page' component={Page} />
const extraProps = { color: 'red' }
<Route path='/page' render={(props) => (
<Page {...props} data={extraProps}/>
)}/>
<Route path='/page' children={(props) => (
props.match
? <Page {...props}/>
: <EmptyPage {...props}/>
)}/>
В типичных ситуациях следует использовать component или render. Children prop может быть использован, но лучше ничего не делать если path не совпадает с location.pathname.
Элементу отрендеренному Route будет передано несколько props. match — объект сопоставления path с location.pathname, location объект[6] и history объект(созданный самим роутом)[7].
Main
Сейчас мы опишем основную структуру роутера. Нам просто нужно отобразить наши маршруты. Для нашего приложения мы будем использовать компонент <Switch/> и компонент <Route/> внутри нашего компонента <Main/> который поместит сгенерированный HTML удовлетворяющий сопоставлению path внутри.
<Main/> DOM узла(node)
import { Switch, Route } from 'react-router-dom'
const Main = () => (
<main>
<Switch>
<Route exact path='/' component={Home}/>
<Route path='/roster' component={Roster}/>
<Route path='/schedule' component={Schedule}/>
</Switch>
</main>
)
Заметка: Route для главной страницы содержит prop exact, благодаря которому пути сравниваются строго.
Унаследованные роуты
Профиль игрока /roster/:number не включен в <Switch/>. Вместо этого он будет рендериться компонентом <Roster/> который рендериться всякий раз когда путь начинается с /roster.
В компоненте Roster мы создадим компоненты для двух путей:
- /roster — с prop exact
- /roster/:number — этот route использует параметр пути, который будет отловлен после /roster
const Roster = () => (
<Switch>
<Route exact path='/roster' component={FullRoster}/>
<Route path='/roster/:number' component={Player}/>
</Switch>
)
Может быть полезным группирование роутов которые имеют общие компоненты, что позволяет упростить родительские маршруты и позволяет отображать контент который относиться к нескольким роутам.
К примеру <Roster/> может быть отрендерен с заголовком который будет отображаться во всех роутах которые начинаются с /roster.
const Roster = () => (
<div>
<h2>This is a roster page!</h2>
<Switch>
<Route exact path='/roster' component={FullRoster}/>
<Route path='/roster/:number' component={Player}/>
</Switch>
</div>
)
Параметры в path
Иногда нам требуется использовать переменные для получения какой либо информации. К примеру, роут профиля игрока, где нам требуется получить номер игрока. Мы сделали это добавив параметр в prop path.
:number часть строки в /roster/:number означает, что часть path после /roster/ будет получена в виде переменной и сохранится в match.params.number. К примеру путь /roster/6 сгенерирует следующий объект с параметрами:
{ number: '6' // Любое переданное значение интерпретируется как строка}
Компонент <Player/> будет использовать props.match.params для получения нужной информации которую следует отрендерить.
// API возращает информацию об игроке в виде объекта
import PlayerAPI from './PlayerAPI'
const Player = (props) => {
const player = PlayerAPI.get(
parseInt(props.match.params.number, 10)
)
if (!player) {
return <div>Sorry, but the player was not found</div>
}
return (
<div>
<h1>{player.name} (#{player.number})</h1>
<h2>{player.position}</h2>
</div>
)
Заметка: Вы можете больше изучить о параметрах в путях в пакете path-to-regexp
Наряду с компонентом <Player/> наш веб-сайт использует и другие как <FullRoster/>, <Schedule/> и <Home/>.
const FullRoster = () => (
<div>
<ul>
{
PlayerAPI.all().map(p => (
<li key={p.number}>
<Link to={`/roster/${p.number}`}>{p.name}</Link>
</li>
))
}
</ul>
</div>
)
const Schedule = () => (
<div>
<ul>
<li>6/5 @ Спартак</li>
<li>6/8 vs Зенит</li>
<li>6/14 @ Рубин</li>
</ul>
</div>
)
const Home = () => (
<div>
<h1>Добро пожаловать на наш сайт!</h1>
</div>
)
Ссылки
Последний штрих, наш сайт нуждается в навигации между страницами. Если мы создадим обычные ссылки то страница будет перезагружаться. React Router решает эту проблему компонентом <Link/> который предотвращает перезагрузку. Когда мы кликаем на <Link/> он обновляет URL и React Router рендерит нужный компонент без обновления страницы.
import { Link } from 'react-router-dom'
const Header = () => (
<header>
<nav>
<ul>
<li><Link to='/'>Home</Link></li>
<li><Link to='/roster'>Roster</Link></li>
<li><Link to='/schedule'>Schedule</Link></li>
</ul>
</nav>
</header>
)
<Link/> использует prop to для описания URL куда следует перейти. Prop to может быть строкой или location объектом (который состоит из pathname, search, hash, state свойств). Если это строка то она конвертируется в location объект.
<Link to={{ pathname: '/roster/7' }}>Player #7</Link>
Заметка: Пути в компонентах <Link/> должны быть абсолютными[4].
Работающий пример
Весь код нашего веб сайта доступен по этому адресу на codepen.
Route готов!
Надеюсь теперь вы готовы погрузиться в изучение деталей маршрутизации веб приложений.
Мы использовали самые основные компоненты которые вам понадобятся при создании собственных веб приложений (<BrowserRouter.>, <Route.>, and <Link.>), но есть еще несколько компонентов и props которые здесь не рассмотрены. К счастью у React Router есть прекрасная документация где вы можете найти более подробное объяснение компонентов и props. Так же в документации предоставляются работающие примеры с исходным кодом.
Пояснения
[1] — Объект location описывает разные части URL’a
// стандартный location
{ pathname: '/', search: '', hash: '', key: 'abc123' state: {} }
[2] — Вы можете использовать компонент <Route/> без path. Это полезно для передачи методов и переменных которые храняться в context.
[3] — Если вы используете prop children то route будет отрендерен даже если path и location.pathname не совпадают.
[4] — Сейчас ведется работа над относительными путями в <Route/> и <Link/>. Относительные <Link/> более сложные чем могут показаться, они должны быть разрешены используя свой родительский объект match, а не текущий URL.
[5] — Это stateless компонент. Внутри есть большая разница между render и component. Component использует React.createElement для создания компонента, в то время как render используется как функция. Если бы вы определили inline функцию и передали через нее props то это было бы намного медленнее чем с использованием функции render.
<Route path='/one' component={One}/>
// React.createElement(props.component)
<Route path='/two' render={() => <Two />}/>
// props.render()
[6] — Компоненты <Route/> и <Switch/> могут оба использовать prop location. Это позволяет сопоставлять их с path, который фактически отличается от текущего URL’а.
[7] — Так же передают staticContext, но он полезен только при рендере на сервере.
In this article, we will talk about the new version of react-router dom that was released on January 18, 2023, and the differences between this current version and the previous versions, its advantages and disadvantages and how to use it in your projects.
How to Install React Router
Firstly Create a react Application using the below syntax
npx create-react-app myapp
Enter fullscreen mode
Exit fullscreen mode
After creating your react application, Simply type npm install react-router-dom in your project terminal to install React Router, and then wait for the installation to finish.
Use the yarn add react-router-dom command if you’re using yarn.
How to Set Up React Router Dom
After the installation, your react application should look something like this
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
Enter fullscreen mode
Exit fullscreen mode
Now that we want to make our router globally visible, we first create a component called Router after that, we then add some boilerplate code that every useful react component must have. The boilerplate code will look something like this.
import React from 'react'
function Router() {
return (
<div>
Router
</div>
);
}
export default Router;
Enter fullscreen mode
Exit fullscreen mode
After creating our boilerplate code, we want to create the pages that we want to be rendered in our route. Firstly, we Create a pages folder in our src folder that is created for us by default when we create our react application. Inside our pages folder, let us create three components and call them Home, Blog and About. So our React app directory should look something like this.
Now, Let us write the boilerplate code that we wrote for our Router Component inside each of the components that we created inside our pages folder, but not forgetting to change the name Router to the name of the page. For Example, the Home Component should look something like this
import React from 'react'
function Home() {
return (
<div>
Home
</div>
);
}
export default Home;
Enter fullscreen mode
Exit fullscreen mode
After Creating the pages that we want to be used in our route, we then import the createBrowserRouter function, The pages that we created earlier and the React-Router dom RouterProvider from the react-router-dom package. An array of routes is provided as a parameter to the createBrowserRouter method. Each route is an object made up of a path, to which the route belongs, and an element, which is the component that is displayed to the user when they are on that route.
Now, let us create three routes inside of the createBrowserFunction and using the paths and elements as shown below
import {createBrowserRouter,RouterProvider} from "react-router-dom";
import Home from "./pages/Home";
import Blog from "./pages/Blog";
import About from "./pages/About";
const routerData = createBrowserRouter([
{ path: "/", element: <Home /> },
{ path: "/about", element: <About /> },
{ path: "/blog", element: <Blog /> },
]);
function Router() {
return (
<div>
Router
</div>
);
}
export default Router
Enter fullscreen mode
Exit fullscreen mode
Now, this is saying that when the user visits the initial route, display the home Component, When he visits the about route, display the About Component and the same goes for the Blog Component. Now we have set up our routerData, How do we Link it inside out Application, This is where the RouterProvider that we imported earlier comes into play. Inside the Router Component that we created Earlier, we return the RouterProvider component while passing the routerData as a prop into it. Your entire Router component should look something like this
import {createBrowserRouter,RouterProvider} from "react-router-dom";
import Home from "./pages/Home";
import Blog from "./pages/Blog";
import About from "./pages/About";
const routerData = createBrowserRouter([
{ path: "/", element: <Home /> },
{ path: "/about", element: <About /> },
{ path: "/blog", element: <Blog /> },
]);
function Router() {
return <RouterProvider router={routerData} />;
}
export default Router
Enter fullscreen mode
Exit fullscreen mode
Now we import this Router inside our App component and rather than wrapping it using a BrowserRouter around our application as done in previous versions, we just insert it directly into the top of our App component. Your App Component should then look something like this
import React from 'react'
import Router from './Router'
function App() {
return (
<div>
<Router/>
App
</div>
);
}
export default App;
Enter fullscreen mode
Exit fullscreen mode
And like that, we have successfully used react router dom version 6.7.0 to create routes in our application.
Pros and Cons of React Router Dom version 6.7.0
Pros:
-
The
unmountOnExitprop allows developers to specify whether a component should be unmounted when the route is exited, which can improve performance and reduce memory usage. -
The
useNavigatehook provides a way to programmatically navigate to a different route in a React application, which can make it easier to handle user interactions in functional components and hooks. -
It addresses a bug that caused the
<Link>component to not correctly handle relative paths in some cases, making the app navigate to an incorrect location. -
It fixes an issue that caused the
<Switch>component to not correctly match routes in some cases, making the app navigate to the wrong route when there were multiple routes that matched the current location. -
It includes a change to the way the
<Route>component handles thepathprop, which allows developers to use thepathprop to specify a string or an array of strings, making it more flexible and easy to use.
Cons:
-
React Router 6.7.0 is not backwards compatible with previous versions, so developers need to update their code to the new syntax and API.
-
It is compatible with the latest version of React, React 17, so developers need to make sure they have the latest version of React installed before upgrading to React Router 6.7.0
-
As React Router is a third party library, sometimes it may not fully align with the latest version of React, which can cause some bugs.
Overall, React Router DOM version 6.7.0 is a powerful tool for client-side routing in React applications, with many new features and improvements that make it easier to use and more versatile. However, developers should be aware of the potential issues and be prepared to update their code and dependencies.
In this tutorial, we’ll talk about what React Router is and how to use it. Then we’ll discuss its features and how to use them in your React app to navigate to and render multiple components.
Prerequisites
- A React app
- A good understanding of what components are in React.
- Node.js installed.
Here’s an interactive scrim about how to set up React Router and route to other components:
React as a Single Page Application (SPA)
You need to understand how pages are rendered in a React app before diving into routing. This section is aimed at beginners – you can to skip it if you already understand what a SPA is and how it relates to React.
In non-single page applications, when you click on a link in the browser, a request is sent to the server before the HTML page gets rendered.
In React, the page contents are created from our components. So what React Router does is intercept the request being sent to the server and then injects the contents dynamically from the components we have created.
This is the general idea behind SPAs which allows content to be rendered faster without the page being refreshed.
When you create a new project, you’ll always see an index.html file in the public folder. All the code you write in your App component which acts as the root component gets rendered to this HTML file. This means that there is only one HTML file where your code will be rendered to.
What happens when you have a different component you would prefer to render as a different page? Do you create a new HTML file? The answer is no. React Router – like the name implies – helps you route to/navigate to and render your new component in the index.html file.
So as a single page application, when you navigate to a new component using React Router, the index.html will be rewritten with the component’s logic.
How to Install React Router
To install React Router, all you have to do is run npm install react-router-dom@6 in your project terminal and then wait for the installation to complete.
If you are using yarn then use this command: yarn add react-router-dom@6.
The first thing to do after installation is complete is to make React Router available anywhere in your app.
To do this, open the index.js file in the src folder and import BrowserRouter from react-router-dom and then wrap the root component (the App component) in it.
This is what the index.js looked like initially:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
After making changes with React Router, this is what you should have:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from "react-router-dom";
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
All we did was replace React.StrictMode with BrowserRouter which was imported from react-router-dom. Now the router features are accessible from any part of your app.
How to Route to Other Components
We are finally done setting things up, so now we’ll look at routing to and rendering different components.
Step 1 — Create multiple components
We’ll create the following Home, About, and Contact components like this:
function Home() {
return (
<div>
<h1>This is the home page</h1>
</div>
);
}
export default Home;
import React from 'react'
function About() {
return (
<div>
<h1>This is the about page</h1>
</div>
)
}
export default Aboutimport React from 'react'
function Contact() {
return (
<div>
<h1>This is the contact page</h1>
</div>
)
}
export default ContactStep 2 — Define routes
Since the App component acts as the root component where our React code gets rendered from initially, we will be creating all our routes in it.
Don’t worry if this does not make much sense – you’ll understand better after looking at the example below.
import { Routes, Route } from "react-router-dom"
import Home from "./Home"
import About from "./About"
import Contact from "./Contact"
function App() {
return (
<div className="App">
<Routes>
<Route path="/" element={ <Home/> } />
<Route path="about" element={ <About/> } />
<Route path="contact" element={ <Contact/> } />
</Routes>
</div>
)
}
export default App
We first imported the features we’ll be using – Routes and Route. After that, we imported all the components we needed to attach a route to. Now let’s break down the process.
Routes acts as a container/parent for all the individual routes that will be created in our app.
Route is used to create a single route. It takes in two attributes:
path, which specifies the URL path of the desired component. You can call this pathname whatever you want. Above, you’ll notice that the first pathname is a backslash (/). Any component whose pathname is a backslash will get rendered first whenever the app loads for the first time. This implies that theHomecomponent will be the first component to get rendered.element, which specifies the component the route should render.
All we have done now is define our routes and their paths, and attach them to their respective components.
If you are coming from version 5 then you’ll notice that we’re not using exact and switch, which is awesome.
Step 3 — Use Link to navigate to routes
If you have been coding along up to this point without any errors, your browser should be rendering the Home component.
We will now use a different React Router feature to navigate to other pages based on those routes and pathnames we created in the App component. That is:
import { Link } from "react-router-dom";
function Home() {
return (
<div>
<h1>This is the home page</h1>
<Link to="about">Click to view our about page</Link>
<Link to="contact">Click to view our contact page</Link>
</div>
);
}
export default Home;
The Link component is similar to the anchor element (<a>) in HTML. Its to attribute specifies which path the link takes you to.
Recall that we created the pathnames listed in the App component so when you click on the link, it will look through your routes and render the component with the corresponding pathname.
Always remember to import Link from react-router-dom before using it.
Conclusion
At this point, we have seen how to install, set up and use the basic features of React Router to navigate to different pages in your app. This pretty much covers the basics for getting started, but there are a lot more cooler features.
For example, you can use useNavigate to push users to various pages, and you can use useLocation to get the current URL. Alright, we won’t start another tutorial at the end of the article.
You can check out more features in the React Router documentation.
You can find me on Twitter @ihechikara2. Subscribe to my newsletter for free learning resources.
Happy coding!
Learn to code for free. freeCodeCamp’s open source curriculum has helped more than 40,000 people get jobs as developers. Get started































