React Router Crash Course with React Invoice App

Web Development

In this React Crash course I will “run you through” the basics of react routing and I will put more focus on the newest version witch is version 6.

This is only a crash crouse so pls don’t aspect to become a react router master after this crash course.

But what you can aspect at the end of this course is that you will understand what react router is, how to set it up and how it works.

You will also have a basic understanding of the most comment used components and after a short presentation you will build a Invoice Application using React Router where you will apply what you learned, but more about the App a bit later on.

As always you can find the source code for the project down below.

Before we get in to the presentation lets first create a new react application using npx create-react-app invoice-app and until this install I will take you thorug the presentation and by the and of it you should be good to go.

What is React Router + New V6

While building a Invoice app we’ll cover:

  • Configuring Routes
  • Navigating with Link
  • Creating Links with active styling
  • Using Nested Routes for Layout
  • Using URL params for data loading
  • Using URL Search params
  • Navigate programmatically by deleting a invoice

For the styling I will use Bootstrap 5 with Bootstrap Icons, but you can use Tailwind CSS ore just regular CSS if you wish and also use Font Awesome for icons if you don’t like the bootstrap Icons.

Steps for the “React Invoice App”:

1/11 Connect the URL

First things first, we want to connect your app to the browser’s URL: import BrowserRouter as Router and render it around your whole app in the index.js.

In the App.js, let’s import Link and add some global navigation to it. aAs I sad I will use Bootstrap for styling just out of convenience because this is where CSS frameworks like Bootstrap and Tailwind CSS really thought there speed styling. As a side note if you are not familiar with Bootstrap and you wish to learn it, then my Bootstrap 5 course on Udemy will definitely help you. You can check it out in the description below.
However you can also use inline styles if you want.

3/11 Add Some Routes

Add a couple new files:

src/routes/home.jsx src/routes/invoices.jsx src/routes/about.jsx

Next lets import them in our index.js and render our app at different URLs by creating our first “Route Config” .

For this lets also import: import { BrowserRouter, Routes, Route} from "react-router-dom";

4/11 Nested Routes

You may have noticed when clicking the links that the layout in App disappears. Repeating shared layouts is a bad idea.

Let’s do some layout handling by :

  1. Nesting the routes inside of the App route

When routes have children it does two things:

It nests the URLs (“/” + “expenses” and “/” + “invoices”) It will nest the UI components for shared layout when the child route matches:

  1. Render an Outlet

Now lets go to our App.js and import import { Outlet, Link } from "react-router-dom"; and add the <Outlet /> under the nav.

5/11 Listing the Invoices

Normally you’d be fetching data from a server somewhere, but for this tutorial let’s hard code some fake data in a data.js file witch we will put in a database folder. Now we can use it in the invoices route by import { getInvoices } from "../data"; When we click an invoice link and see what happens.

6/11 Adding a “No Match” Route

If you click those links the page goes blank! That’s because none of the routes we’ve defined match a URL like the ones we’re linking to: “/invoices/123”. Before we move on, it’s good practice to always handle this “no match” case. Go back to your route config and add this: The “*” has special meaning here. It will match only when no other routes do.

7/11 Reading URL Params

In the individual invoice URLs let’s add a route for a specific invoice. For this you are going to make a new component at src/routes/invoice.js to render at those URLs:

Now let’s add it to Routes <Route path=":invoiceId" element={<Invoice />} />.

Then add an outlet to the parent layout route by import { Link, Outlet } from "react-router-dom"; witch is the src/routes/invoices.js under the nav we will insert out <Outlet />.

All we need to do now is open up the invoice component again and let’s get the :invoiceId param from the URL: by import { useParams } from "react-router-dom";

We can extend upon our invoice by extracting more data from the database, for example getting the invoice.name,invoice.number,invoice.amount by creating another function getInvoice(number) in our data.js file.

8/11 Index Routes

Next up lets click on the “Invoices” link in the global nav of your app. Notice that the main content area goes blank! We can fix this with an “index” route.

Basically the index route fills the empty space! We use the index prop instead of a path. That’s because the index route shares the path of the parent.

In other words the index will:

  • render in the parent routes outlet at the parent route’s path.
  • match when a parent route matches but none of the other children match.
  • are the default child route for a parent route.
  • render when the user hasn’t clicked one of the items in a navigation list yet.

Next let’s display the link as the active link that user is looking at. You can do this by swapping out Link for NavLink in invoices. import { NavLink, Outlet } from "react-router-dom"; and also adding a bit of conditional styling to it.

10/11 Search Params

Search params are like URL params but they sit in a different position in the URL. Instead of being in the normal URL segments separated by /, they are at the end after a ?. You’ve seen them across the web like “/login?success=1” or “/shoes?brand=nike&sort=asc&sortby=price”.

React Router has useSearchParams witch works a lot like React.useState() but stores and sets the state in the URL search params instead of in memory.

Now let’s adding a little filter on the invoices nav list.

First we need to import { useSearchParams} from "react-router-dom"; then destructure it and use the filter method to out invoice.

11/11 Navigate programmatically by Deleting a invoice

Most of the time the URL changes is in response to the user clicking a link. But if you want to change the URL. In order to navigate programmatically (for example to delete a invoice), then you need to use useNavigate this hook gives you an API to do so with a signature like this: deleting a record.

import { useParams, useNavigate } from "react-router-dom";

Let’s add a button that marks the invoice as paid and then navigates to the index route.

Source code for the Invoice App

index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";
import Invoices from "./routes/invoices";
import Invoice from "./routes/invoice";
import Home from "./routes/Home";

ReactDOM.render(
  <Router>
    <React.StrictMode>
      <Routes>
        <Route path="/" element={<App />}>
          <Route path="home" element={<Home />} />
          <Route
            index
            element={
              <main style={{ padding: "1rem" }}>
                <h1 className="display-1 mt-5 text-primary fw-bold">
                  Invoice App !
                </h1>
              </main>
            }
          />
          <Route path="invoices" element={<Invoices />}>
            {/* Index */}
            <Route
              index
              element={
                <main style={{ padding: "1rem" }}>
                  <p className="text-danger fw-bold">Select an invoice !</p>
                </main>
              }
            />
            <Route path=":invoiceId" element={<Invoice />} />
          </Route>
          {/*todo: Adding a "No Match" Route
           */}
          <Route
            path="*"
            element={
              <main className="container shadow mt-5 bg-info">
                <h2 className="display-2 p-5">There's nothing here!</h2>
                <Link to={"/"} className="nav-link">
                  Back Home
                </Link>
              </main>
            }
          />
        </Route>
      </Routes>
    </React.StrictMode>
  </Router>,
  document.getElementById("root")
);

App.js

import { Outlet, Link } from "react-router-dom";
import "./App.css";

function App() {
  return (
    <div className="App">
      <nav className="navbar navbar-expand navbar-dark bg-primary">
        <ul className="navbar-nav fs-5">
          <Link className="nav-link" to="/home">
            Home
          </Link>
          <Link className="nav-link" to="/invoices">
            Invoices
          </Link>{" "}
          <Link className="nav-link" to="/about">
            About
          </Link>
        </ul>
      </nav>
      <Outlet />
    </div>
  );
}

export default App;

db/data.js

let invoices = [
  {
    name: "Apple Inc",
    number: 1984,
    amount: "$230,800",
    due: "12/05/1995",
  },
  {
    name: "Microsoft",
    number: 1980,
    amount: "$158,000",
    due: "10/31/2000",
  },
  {
    name: "Nvidia",
    number: 2003,
    amount: "$859,500",
    due: "07/22/2003",
  },
  {
    name: "Menyhart Media",
    number: 1997,
    amount: "$134,220",
    due: "09/01/1997",
  },
  {
    name: "Tesla",
    number: 1998,
    amount: "$434,600",
    due: "01/27/2998",
  },
];

export function getInvoices() {
  return invoices;
}

export function getInvoice(number) {
  return invoices.find((invoice) => invoice.number === number);
}

// Delete
export function deleteInvoice(number) {
  invoices = invoices.filter((invoice) => invoice.number !== number);
}

routes/Home.js

import React from "react";
import { Link } from "react-router-dom";

export default function Home() {
  return (
    <div>
      <h1 className="display-1">React Router</h1>
      <Link className="nav-link" to="/about">
        About
      </Link>{" "}
    </div>
  );
}

routes/Invoices.js

import React from "react";

import { Outlet, Link, NavLink, useSearchParams } from "react-router-dom";
import { getInvoices } from "../db/invoicesDb";

export default function Invoices() {
  let invoices = getInvoices();

  //  Search Params
  let [searchParams, setSearchParams] = useSearchParams();
  return (
    <div
      className="container bg-light mt-5 rounded  p-2 shadow"
      style={{ maxWidth: "700px" }}
    >
      <h4 className="display-4 py-3 fw-bold">Invoices</h4>
      <div className="row ">
        <nav className="col">
          <input
            className="rounded-pill w-100 p-2"
            value={searchParams.get("filter") || ""}
            onChange={(event) => {
              let filter = event.target.value;
              filter ? setSearchParams({ filter }) : setSearchParams({});
            }}
            placeholder="Search"
          />
          {invoices
            .filter((invoice) => {
              let filter = searchParams.get("filter");
              if (!filter) return true;
              let name = invoice.name.toLowerCase();
              return name.startsWith(filter.toLowerCase());
            })
            .map((invoice) => (
              <NavLink
                to={`/invoices/${invoice.number}`}
                key={invoice.number}
                //   className=" d-flex shadow  nav-link btn rounded-pill bg-warning text-dark m-2 my-3"
                className={({ isActive }) =>
                  isActive
                    ? "d-flex shadow  nav-link btn rounded-pill bg-primary text-light m-2 my-3"
                    : "d-flex shadow  nav-link btn  rounded-pill bg-white border m-2 my-3"
                }
              >
                <i className="bi bi-envelope-exclamation-fill   me-2"></i>
                {invoice.name}
              </NavLink>
            ))}
        </nav>
        <Outlet className="col" />
      </div>
    </div>
  );
}

routes/invoice.js

import React from "react";
import { useParams, useNavigate } from "react-router";
import { getInvoice, deleteInvoice } from "../db/invoicesDb";

export default function Invoice() {
  let navigate = useNavigate();
  let params = useParams();
  //   return <h2>Invoice #???</h2>;
  //   return <h2>Invoice: {params.invoiceId}</h2>;
  let invoice = getInvoice(parseInt(params.invoiceId, 10));
  return (
    <section className="col position-relative m-auto">
      <div class="card shadow">
        <div class="card-header bg-primary text-white">
          {invoice.name}: {invoice.number}
        </div>
        <div class="card-body">
          <h5 class="card-title">Total Due: {invoice.amount}</h5>

          <p class="card-text">Due Date: {invoice.due}</p>
          <button
            class="btn btn-danger"
            onClick={() => {
              deleteInvoice(invoice.number);
              navigate("/invoices");
            }}
          >
            Delete
          </button>
        </div>
      </div>
    </section>
  );
}

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.