React

ℹ️
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>

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
  1. Create and export a loader function
    1. import { getContacts } from "../contacts"; export async function loader() { const contacts = await getContacts(); return { contacts }; }
  1. Hook it up to the route
    1. 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 />, }, ], }, ]);
  1. Access and render the data
    1. 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>
       

Leave a comment