React Router
React Router enables client side routing (update the URL from a link click without making another request for a new page from the server - instead it can immediately render new UI and make data requests with fetch to update the page with new information)
Client side routing is enabled by creating a
Router
and linking-to/submitting pages with <Link>
and <Form>
🏔️ Overview: https://reactrouter.com/en/main/start/overview
📖 Tutorial: https://reactrouter.com/en/main/start/tutorial
Minimal Example
import React from "react"; import { createRoot } from "react-dom/client"; import { createBrowserRouter, RouterProvider, Route, Link, } from "react-router-dom"; const router = createBrowserRouter([ // <--- Create Router { path: "/", element: ( <div> <h1>Hello World</h1> <Link to="about">About Us</Link> {/* // <--- Link to other places */} </div> ), }, { path: "about", element: <div>About</div>, }, ]); createRoot(document.getElementById("root")).render( <RouterProvider router={router} /> {/* <--- Use the Router */} );
Nested Routes
It can do Nested Routes, handle to Error pages, etc.
const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, // <-- Error page children: [ { path: "contacts/:contactId", element: <Contact />, }, ], }, ]);
Children (nested routes) will be rendered in an
<Outlet>
import { Outlet } from "react-router-dom"; export default function Root() { return ( <> {/* all the other elements */} <div id="detail"> <Outlet /> </div> </> ); }
Loading Data
There are 2 APIs for loading data:
loader
and useLoaderData
- Create and export a loader function
import { getContacts } from "../contacts"; export async function loader() { const contacts = await getContacts(); return { contacts }; }
- Hook it up to the route
import Root, { loader as rootLoader } from "./routes/root"; const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, loader: rootLoader, // <---- This is where the loader goes! children: [ { index: true, element: <Index /> }, // <-- Default (index) Route { 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> </> ); }
Form → Data Writes + action
If we use React Router’s
<Form>
instead of HTML’s <form>
, the submit request will not be sent to the server, but it will use client side routing and send it to a route action
instead!import { useLoaderData, Form } from "react-router-dom"; export async function action() { // <--- export the action await createContact(); } export default function Root() { const { contacts } = useLoaderData(); return ( <> <Form method="post"> <button type="submit">New</button> </Form> {/* other code to display the contacts */} </> ); }
import Root, { loader as rootLoader, action as rootAction, } from "./routes/root"; const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, loader: rootLoader, action: rootAction, // <--- Set the action on the Route! children: [ { path: "contacts/:contactId", element: <Contact />, }, ], }, ]);
Each field in the FormData is accessible with
formData.get(name)
, so in a form like this:<input placeholder="First" aria-label="First name" type="text" name="first" defaultValue={contact.first} />
you could access first and last name like this:
export async function action({ request, params }) { const formData = await request.formData(); const firstName = formData.get("first"); const lastName = formData.get("last"); // ... return redirect(`/contacts/${params.contactId}`); // <--- change location }
If we use
<fetcher.Form>
instead we get a form that we can submit without the URL changing and without the history stack being affected.Active Link Styling
Using
<NavLink>
instead of <Link>
allows us to show whether a link isActive
or isPending
<NavLink to={`contacts/${contact.id}`} className={({ isActive, isPending }) => isActive ? "active" : isPending ? "pending" : "" } > {contact.first || contact.last ? ( <> {contact.first} {contact.last} </> ) : ( <i>No Name</i> )} {" "} {contact.favorite && <span>★</span>} </NavLink>
Note how we’re 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
Normally React Router will leave the old page up as data is loading for the next page. This can feel unresponsive. We can use the
useNavigation
hook to fix this! useNavigation
returns the current navigation state: idle
, submitting
or loading
In this example we grey out the div via
className=’loading’
while navigation.state === "loading"
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> </> ); }
useNavigate
Can be used to send the user back in the browsing history!
import { useNavigate } from "react-router-dom"; // ... <button type="button" onClick={() => {navigate(-1);}}>Cancel</button>
*Any way to contact you. Email, Telegram id, etc.