React Router — overview

Jeff P
29 min readJan 24, 2024

--

What is is and how it works

React Router is a crucial library in the world of React, enabling developers to create complex, dynamic, and navigable web applications.

It was developed and is maintained by a community of open-source developers. While it doesn’t have a single individual or organization associated with its development, it is a widely used and well-maintained project with contributions from various developers over time.

It serves as a tool for building single-page applications (SPAs) and managing client-side routing.

The real beauty of React Router is that when you switch between URLS (pages) the entire page does NOT reload! Instead a different component or set of components is rendered, which means the navigation experience switching between “pages” is so much quicker and seamless. It makes navigating between pages feel like you are using an app, rather than waiting for a page to load up with a blank screen whilst the server fetches the new page. In essence, it’s just a far nicer user experience!

React Router enables the creation of routes, which are essentially mappings between URLs and React components. When a user navigates to a specific URL, React Router ensures that the corresponding component is rendered, providing a seamless and dynamic user experience.

Installing React Router

Simply use the following command:

npm install react-router-dom@6

This will install version 6 of React Router

How React Router Works

React Router works by providing a set of components that are used to define routes within a React application. These components include BrowserRouter, Route, Switch, and Link, among others. Here's an overview of how React Router works:

  1. BrowserRouter: The BrowserRouter component should be wrapped around the entire application. It uses the HTML5 History API to synchronize the URL with the displayed content. It listens for changes to the URL and renders the appropriate component based on the current route.
import { BrowserRouter } from "react-router-dom";
import "./App.css";

function App() {
return (
<BrowserRouter>

// something goes here

</BrowserRouter>
);
}

export default App;
  1. Routes: The Routes component is used to wrap multiple Route components. It ensures that only the first matching route is rendered. This is particularly useful when defining routes with similar path patterns.

Note: <Routes> was introduced to React Router in version 6 — Previously you would used <Switch> to wrap multiple Route componments. It worked slightly differently in version 5. In version 6, you can now nest routes and define layouts more intuitively.

import { BrowserRouter, Routes } from "react-router-dom";
import "./App.css";

function App() {
return (
<BrowserRouter>
<Routes>

// something goes here

</Routes>
</BrowserRouter>
);
}

export default App;

3. Route: The Route component defines a mapping between a URL path and a React component. When the URL matches the specified path, the corresponding component is rendered.

We supply both a “path” and an “element” to each Route. The homepage is always just the “/” and the other pages use “/[page-name]” to signify that the page comes after the forward slash of the site.

So for example, to get to the pricing “page”, the Browser URL would show mywebsite.com/pricing

import { BrowserRouter, Route, Routes } from "react-router-dom";
import "./App.css";
import Home from "./pages/Home";
import Product from "./pages/Product";
import Pricing from "./pages/Pricing";

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/product" element={<Product />} />
<Route path="/pricing" element={<Pricing />} />
</Routes>
</BrowserRouter>
);
}

export default App;

Note how the entire page component is supplied to the element in curly braces { }

<Route path="/product" element={<Product />} />

In earlier versions of React Router (v5), you didn’t present it this way…. it would’ve looked like this, using “component” instead of “element”

<Route path="/pricing" component={Pricing} />

The “element” prop allows you to pass a JSX element directly, which is more in line with the typical way React components are rendered using JSX. It feels more natural and consistent with React’s component-based architecture.

PageNotFound

To deal with 404 errors where a page doesn’t exist, you should also include a “catch-all” route which will direct a user to a PageNotFound.jsx page component…

import { BrowserRouter, Route, Routes } from "react-router-dom";
import "./App.css";
import Home from "./pages/Home";
import Product from "./pages/Product";
import Pricing from "./pages/Pricing";
import PageNotFound from "./pages/PageNotFound";

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/product" element={<Product />} />
<Route path="/pricing" element={<Pricing />} />

<Route path="*" element={<PageNotFound />} />

</Routes>
</BrowserRouter>
);
}

export default App;

With these things in place you should be able to manually change the URL in the browser to toggle “pages”

Creating page links between pages

As well as manually changing the URL in the broswer, you will of course also want to be able to click on links in pages to switch between them. This is where you use <NavLink> or just <Link>

NavLink (or Link): The Navink component creates clickable links within your application that navigate to specific routes. It generates anchor tags with the appropriate href attributes, allowing users to easily move between different views.

import { NavLink } from "react-router-dom";

export default function Home() {
return (
<div>
<h1>Home</h1>

<NavLink to="/product">Product</NavLink>
<NavLink to="/pricing">Pricing</NavLink>
</div>
);
}

<NavLink> is an extension of <Link> with additional features for styling and indicating the active route.

It allows you to apply CSS classes or styles to the navigation link when the associated route is active (i.e., the link matches the current URL).

You can use the activeClassName and activeStyle props to specify the styles or classes to be applied to the active link.

<NavLink> is especially useful when you want to highlight the currently active route in your navigation menu.

        <li>
<NavLink to="/about" activeClassName="active">
About
</NavLink>
</li>
<li>
<NavLink to="/contact" activeClassName="active">
Contact
</NavLink>
</li>

Nested Routes

You can also have routes inside other routes. When you’re building a complex application with multiple levels of routing, you often want to nest routes inside one another. For example, you might have a layout component for a dashboard with a sidebar menu, and within that dashboard, you have different sections with their own routes.

Parent-Child Relationship: To create this nested structure, you define child routes within the parent route component. Child routes are routes that should be rendered within a specific area of the parent route’s layout. This is where <Outlet> comes into play.

Firstly you want to specify the routes, and to do this you define them inside another route, only this time there is an opening and closing tag for the “parent” route…

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Homepage />} />
<Route path="/product" element={<Product />} />
<Route path="/pricing" element={<Pricing />} />
<Route path="/login" element={<Login />} />

// parent route
<Route path="/app" element={<AppLayout />}>
// the child (nested) routes
<Route index element={<p>List of cities</p>} />
<Route path="cities" element={<p>cities</p>} />
<Route path="countires" element={<p>countries</p>} />
<Route path="form" element={<p>Form</p>} />
</Route>

<Route path="*" element={<PageNotFound />} />
</Routes>
</BrowserRouter>
);
}

So we can see that we’ve nested routes for four routes inside the parent route of /app

what this means is that if we went to mywebsite.com/app/ then we could also have routes for:

mywebsite.com/app/cities

mywebsite.com/app/countries

mywebsite.com/app/form

There’s a couple of things to note here…. firstly, we don’t use a forward slash with these nested routes, because we’re not showing a “page” after the main domain (as we do with the other routes that don’t have nested routes inside them)

secondly, we can specify a “default” component page, which we want to load up when we just go to mywebsite.com/app/ and to load this one, we use the “index” prop

<Route index element={<p>List of cities</p>} />

So the element is what will load up in this case, which can be either an element OR another component…

<Route index element={<AnotherComponent />} />

In terms of using the <Outlet> component, it’s a bit like using {children} only for routes, because the <Outlet> component takes the place of the routes that are in between the opening and closing parent route:

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Homepage />} />
<Route path="/product" element={<Product />} />
<Route path="/pricing" element={<Pricing />} />
<Route path="/login" element={<Login />} />
<Route path="/app" element={<AppLayout />}> // parent route

// routes represented by <outlet>
<Route index element={<p>List of cities</p>} />
<Route path="cities" element={<p>cities</p>} />
<Route path="countires" element={<p>countries</p>} />
<Route path="form" element={<p>Form</p>} />

</Route>

<Route path="*" element={<PageNotFound />} />
</Routes>

Params and Query strings

You can also store state in the URL which means if you share the URL with someone else, the page will load with the state you wanted the person to see. For example, on a shopping page, where there are t-shirts, in multiple colours, and you want the person you shared the URL with to immediately load up the red version of the t-shirt.

Here’s an example of loading up a page that shows details on Lisbon (in the URL this is “lisbon”) with a query string showing a pointer on the map of the actual GPS position.

In order to set this up, the first thing we want to do is create a Route that utilizes the route parameter placeholder…. here’s an example:

          <Route
path="cities"
element={<CityList cities={cities} isLoading={isLoading} />}
/>
<Route path="cities/:id" element={<City />} />

The special part here is the :id as this is the placeholder which is used to indicate that a part of the URL is dynamic and can vary.

Now in the example above, if there’s a match on cities, then the <CityList> component will be rendered… i.e. if someone clicks on the cities button….

So now moving to the dynamic route for cities…

<Route path="cities/:id" element={<City />} />

We’ve supplied the <City /> component as the element, so what we’re saying here is that when the URL matches this dynamic path, then render the <City /> component.

We do this by wrapping each of the CityList items (which represented by multiple <CityItem /> component instances in a Link route….

  return (
<li>
<Link className={styles.cityItem} to={`${id}`}>
{/* Add SVG image of flag to fix issue with flag emoji not showing up */}
<span className={styles.emoji}>
{flagSVG ? (
<img
src={`data:image/svg+xml;utf8,${encodeURIComponent(flagSVG)}`}
alt={`Flag of ${country}`}
width="28"
/>
) : (
emoji
)}
</span>
<h3 className={styles.name}>{cityName}</h3>
<time className={styles.date}>{formatDate(date)}</time>
<button className={styles.deleteBtn}>&times;</button>
</Link>
</li>
);
}

export default CityItem;

So in the code above we have multiple CityItem components, which is comprised of a SVG flag image, a h3 city name, the date, and a delete button…

Notice that everything in the list item has been enclosed inside a <Link> route …

<Link className={styles.cityItem} to={`${id}`}>

What’s important here is the “to” part of the Link route, where we’re saying that the “to” should be a text string (hence the reason for using ``) and the string value should be whatever id we pass in…

Let’s look at the code in its entirety fo a better understanding….

import styles from "./CityItem.module.css";
import { useEffect, useState } from "react";
import emojiToCountryCode from "../../data/emojiToCountryCode";
import { Link } from "react-router-dom";

const formatDate = (date) => {
return new Intl.DateTimeFormat("en", {
day: "numeric",
month: "long",
year: "numeric",
}).format(new Date(date));
};

function CityItem({ city }) {
const { cityName, country, emoji, date, id } = city;

// State to store the fetched SVG
const [flagSVG, setFlagSVG] = useState(null); // State to store the fetched SVG

useEffect(() => {
if (emoji) {
// Get the lowercase text country code from the emoji property
const countryCode = emojiToCountryCode[emoji];
if (countryCode) {
fetch(
`https://raw.githubusercontent.com/lipis/flag-icons/main/flags/4x3/${countryCode}.svg`
)
.then((response) => response.text())
.then((svg) => {
setFlagSVG(svg);
})
.catch((error) => {
console.error("Error fetching SVG:", error);
});
}
}
}, [emoji]);

return (
<li>
<Link className={styles.cityItem} to={`${id}`}>
{/* Add SVG image of flag to fix issue with flag emoji not showing up */}
<span className={styles.emoji}>
{flagSVG ? (
<img
src={`data:image/svg+xml;utf8,${encodeURIComponent(flagSVG)}`}
alt={`Flag of ${country}`}
width="28"
/>
) : (
emoji
)}
</span>
<h3 className={styles.name}>{cityName}</h3>
<time className={styles.date}>{formatDate(date)}</time>
<button className={styles.deleteBtn}>&times;</button>
</Link>
</li>
);
}

export default CityItem;

The return part of the CityItem component is of course what we want rendered to the screen, but notice that we’re passing in props of city, and then immediately de-structuring this city object….

function CityItem({ city }) {
const { cityName, country, emoji, date, id } = city;

Here’s a snippet of the city object being passed in…..

{
"cities": [
{
"cityName": "Lisbon",
"country": "Portugal",
"emoji": "🇵🇹",
"date": "2027-10-31T15:59:59.138Z",
"notes": "My favorite city so far!",
"position": {
"lat": 38.727881642324164,
"lng": -9.140900099907554
},
"id": 73930385
},
{
"cityName": "Madrid",
"country": "Spain",
"emoji": "🇪🇸",
"date": "2027-07-15T08:22:53.976Z",
"notes": "",
"position": {
"lat": 40.46635901755316,
"lng": -3.7133789062500004
},
"id": 17806751
},
{
"cityName": "Berlin",
"country": "Germany",
"emoji": "🇩🇪",
"date": "2027-02-12T09:24:11.863Z",
"notes": "Amazing 😃",
"position": {
"lat": 52.53586782505711,
"lng": 13.376933665713324
},
"id": 98443197
}
]
}

notice that each city has an id property.

Before we go any further, let’s quickly view the code of the “parent” component, which is responsible for all of the CityItem instances….

import styles from "./CityList.module.css";
import Spinner from "./Spinner";
import CityItem from "./CityItem";
import Message from "./Message";

function CityList({ cities, isLoading }) {
if (isLoading) return <Spinner />;
if (!cities.length)
return (
<Message message="Add your first city by clicking on a city on the map" />
);

return (
<ul className={styles.cityList}>
{cities.map((city) => (
<CityItem city={city} cities={cities} key={city.id} />
))}
</ul>
);
}

export default CityList;

So we can see here in our CityList component that we’re taking the cities array of objects, and then mapping through them to create our CityItem instances…

  return (
<ul className={styles.cityList}>
{cities.map((city) => (
<CityItem city={city} cities={cities} key={city.id} />
))}
</ul>
);

So each CityItem will have access to the id within the cities array of objects, and therefore when it comes to adding in the id as a text string, it will be able to do this. So for example, the first text string would be…

73930385

because this is the value for the id: property in the first mapped object.

So effectively, this is what our Link route would look like…

<Link className={styles.cityItem} to=73930385>

So everything that’s enclosed in this Link route will now be a URL that when clicked will form the dynamic URL, which means that if you were to click on the first CityItem component, the URL would change to mywebsite.com/cities/73930385

If you clicked on the second CityItem, the URL would change to mywebsite.com/cities/17806751

and so on…

You can also add a query string to the URL (and it does NOT necessarily have to be appended to a dynamic URL!) by adding the following:

<Link className={styles.cityItem} to={`${id}?lat=${position.lat}`}>

So we’ve added a ? immediately after the text string id value, and then provided a property (lat — for latitude) then the = sign, and finally the text string value of the lat property.

Here was the first object in our cities array…

{
"cityName": "Lisbon",
"country": "Portugal",
"emoji": "🇵🇹",
"date": "2027-10-31T15:59:59.138Z",
"notes": "My favorite city so far!",
"position": {
"lat": 38.727881642324164,
"lng": -9.140900099907554
},
"id": 73930385
},

so as long as we have access to the “lat” value, then we can pass in the value as a text string, which in this case would be

38.727881642324164

So going back to when we click on the first CityItem component, we’ll now see the following in the URL….

We can add more strings, simply by adding the & sign, and then continuing with more….

      <Link
className={styles.cityItem}
to={`${id}?lat=${position.lat}&lng=${position.lng}`}
>

which give us this when we click on the first item….

useParams() and useSearchParams()

useParams() is part of the React Router library, and it allows you to access the route parameters (dynamic parts of the URL) in your React components. It’s a very handy way to extract and use values from the URL within your components.

When you call useParams(), it returns an object containing the parameter values. You can then access specific parameters by their names. It’s important to note that useParams() is used for accessing route parameters (dynamic segments of the URL) defined in the route path.

useSearchParams() is also part of the React Router library, but the key difference between this hook and useParams() is that this hook is used for accessing query parameters (key-value pairs) from the URL’s query string.

We’ll take a look at useSearchParams() first…. let’s say in our Map component, we want to display the latitude and longitude of the current CityItem…. we can achieve this by taking the data directly from the URL rather than via state, simply by using the useSearchParams() hook.

In order to do this, we firstly need to import useSearchParams, and then we need to set it up in much the same way as we do for useState() where we specify the piece of state that will store the params from the URL, and the second part is the function that would allow us to update that state.

We then use the .get() method to specify the exact query string we want from the query string (or else useSearchParams thinks that you want the ENTIRE object!)

import styles from "./Map.module.css";
import { useSearchParams } from "react-router-dom";

export default function Map() {
const [searchParams, setSearchParams] = useSearchParams();

const lat = searchParams.get("lat");
const lng = searchParams.get("lng");

return (
<div className={styles.mapContainer}>
<h1>Map</h1>
<h1>
lat: {lat}, lng: {lng}
</h1>
</div>
);
}

We then render the component, and those details are passed into the component, purely from the URL!

Bi-Directional flow

You can also set the state using the setSearchParams() function, which means not only the data on the page component will be updated, but also the URL itself! The beauty of this is that if the URL is updated, then all the components that are obtaining the data from the URL will also immediately update!

So just for example, if we created a button in our map component as follows:

import styles from "./Map.module.css";
import { useSearchParams } from "react-router-dom";

export default function Map() {
const [searchParams, setSearchParams] = useSearchParams();

console.log(searchParams);

const lat = searchParams.get("lat");
const lng = searchParams.get("lng");

return (
<div className={styles.mapContainer}>
<h1>Map</h1>
<h1>
lat: {lat}, lng: {lng}
</h1>
<button onClick={() => setSearchParams({ lat: 5, lng: 10 })}>
update lat/lng
</button>
</div>
);
}

If we click on this button it will pass in the new object values into the searchParams URL “state”, which will update the URL, AND any components relying on the URL for state data!

useNavigate()

This is another react hook that is used to programmatically load a different URL, which isn’t necessarily tied to a click event. For example, you might want the URL to change when a piece of state changes.

To use the useNavigate() hook we first assign it to a const…

const navigate = useNavigate();

We then simply define where we want the user to be taken when this function is called. For example, for an onClick, you could use it like this…

        <Button type="primary">Add</Button>
<Button
type="back"
onClick={(e) => {
e.preventDefault();
navigate("/app/counties");
}}
>
&larr; Back
</Button>

You can also use the function to navigate back one page (just like a back button in your browser) by passing in the value of -1

<Button type="primary">Add</Button>
<Button
type="back"
onClick={(e) => {
e.preventDefault();
navigate(-1);
}}
>
&larr; Back
</Button>

likewise, a forward button, would use the value of 1 instead of -1

We can also use the <Navigate /> component from React Router to set up a route for the “index” route, and when this route is matched, it will navigate to the preferred route while replacing the current URL in the history stack.

For example…

          <Route index element={<Navigate to="/app/cities" replace />} />
<Route
path="cities"
element={<CityList cities={cities} isLoading={isLoading} />}
/>

the replace part, will ensure that when you click on the back button, it will go back to the previous page (i.e. index)

React Router v6.4

We’re now going to look at a special function called createBrowserRouter()

createBrowserRouter is a function introduced in React Router version 6 that simplifies the process of creating a router with built-in support for various features like lazy loading, automatic code splitting, and handling of error boundaries. This approach is part of the newer API that aims to make route configuration more declarative and straightforward compared to older versions of React Router.

How createBrowserRouter Works

With createBrowserRouter, you define your routes as an array of route objects, each specifying the path and the component to render. This router is then passed to a <RouterProvider> component, which takes care of rendering the routes.

Here’s a basic example of using createBrowserRouter:

import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Root from './ui/Root';
import Home from './ui/Home';
import About from './features/About';

// Define routes as an array of objects
const router = createBrowserRouter([
{
path: '/',
element: <Root />,
children: [
{ path: 'home', element: <Home /> },
{ path: 'about', element: <About /> },
],
},
]);

function App() {
return <RouterProvider router={router} />;
}

Here the <Root> component would be considred as a “layout route” as it is the parent of all the children routes. This basically means that when you switch pages, the layout route will always be seen.

A layout route is a route that serves as a container or layout for other nested routes. It typically includes components that are common across multiple pages, such as navigation bars, footers, or sidebars.

Let’s take a closer look at an App.jsx file where this might be implemented:

import { RouterProvider, createBrowserRouter } from "react-router-dom";
import Home from "./ui/Home";
import Menu from "./features/menu/Menu";
import Cart from "./features/cart/Cart";
import CreateOrder from "./features/order/CreateOrder";
import Order from "./features/order/Order";
import AppLayout from "./ui/AppLayout";

const router = createBrowserRouter([
{
element: <AppLayout />,
children: [
{
path: "/", element: <Home />,
},
{
path: "/menu", element: <Menu />,
},
{
path: "/cart", element: <Cart />,
},
{
path: "/order/new", element: <CreateOrder />,
},
{
path: "/order/:id", element: <Order />,
},
],
},
]);

function App() {
return <RouterProvider router={router} />;
}

export default App;

So we can see that various roots have been defined as “children” to AppLayout. Let’s look at AppLayout:

import Header from "./Header";
import CartOverview from "../features/cart/CartOverview";
import { Outlet } from "react-router-dom";

export default function AppLayout() {
return (
<div>
<Header />

<main>
<Outlet />
</main>

<CartOverview />
</div>
);
}

So AppLayout has a few components inside it, including the <Header> the <CartOverview> and also another component we will be familiar with… <Outlet>

<Outlet> is used for nested routes. Remember, it’s a bit like using {children} only for routes.

So going back to createBrowserRouter, if a user visits the main page (“/” then they should expect to see the <Home> component nested inside AppLayout, at the position where <Outlet> is shown…. it would be a bit like this in AppLayout:

import Header from "./Header";
import CartOverview from "../features/cart/CartOverview";
import { Outlet } from "react-router-dom";

export default function AppLayout() {
return (
<div>
<Header />

<main>
<Home />
</main>

<CartOverview />
</div>
);
}

..and if they navigated to “/menu” , then they should expect to see the <Menu> component nested inside AppLAyout, at the position where <Outlet> is shown…. so once again, it would be a bit like this in AppLayout:

import Header from "./Header";
import CartOverview from "../features/cart/CartOverview";
import { Outlet } from "react-router-dom";

export default function AppLayout() {
return (
<div>
<Header />

<main>
<Menu />
</main>

<CartOverview />
</div>
);
}

Let’s see this in the browser to confirm….

and if we go to the menu

and if we go to order/new

So you can see that whatever “child” URL route we go to, we’ll see the AppLayout component show it’s own components, as well as the components specified as “children” via the <Outlet> component.

useLoaderData()

useLoaderData is a hook provided by react-router-dom, which is used to access data loaded by a loader function in a route where it is defined. This hook is part of the React Router library, which is commonly used for handling routing in React applications. The concept of loaders and the useLoaderData hook are features that were introduced in React Router version 6.

Loaders are functions that you define in your route configuration to load data asynchronously before rendering the component associated with the route. They run when the route is matched, before the component renders, making it possible to have the data ready for the component by the time it renders. This approach is beneficial for improving user experience by reducing loading states and flashing content, and it can also be useful for server-side rendering scenarios.

We use first of all use loader() to go and fetch the data we want….

export async function loader() {
const menu = await getMenu();
return menu;
}

You can see that we’re awaiting data from a function caled getMenu() — Let’s look at this function:

const API_URL = 'https://react-fast-pizza-api.onrender.com/api';

export async function getMenu() {
const res = await fetch(`${API_URL}/menu`);

// fetch won't throw error on 400 errors (e.g. when URL is wrong), so we need to do it manually. This will then go into the catch block, where the message is set
if (!res.ok) throw Error('Failed getting menu');

const { data } = await res.json();
return data;
}

so we’re retrieving the data from https://react-fast-pizza-api.onrender.com/api/menu

and returning this data.

So going back to our loader(), we’re assigning this data to the const menu.

Notice how we export this loader() ?

export async function loader() {
const menu = await getMenu();
return menu;
}

This is so that we can import it into our createBrowserRouter as a “loader”

So in our App file (where createBrowserRouter resides) we not only import our Menu component, but we also import our loader. It’s common practice to rename it on import so that it’s obvious what it’s for, hence in the code below we rename our loader function to “MenuLoader”

import Menu, { loader as MenuLoader } from "./features/menu/Menu";

now that we’ve imported it, we can add it to createBrowserRouter as a “loader” for our Menu component…

const router = createBrowserRouter([
{
element: <AppLayout />,
children: [
{
path: "/",
element: <Home />,
},
{
path: "/menu",
element: <Menu />,
loader: MenuLoader,
},

So what’s happening here is that the “loader” can load data asynchronously before rendering the component associated with the route.

They run when the route is matched, before the component renders, making it possible to have the data ready for the component by the time it renders. This approach is beneficial for improving user experience by reducing loading states and flashing content

Accessing the loading data

Finally, inside your component, you use the useLoaderData hook to access the data loaded by the loader function.

function Menu() {
const menu = useLoaderData();
console.log(menu);
return <h1>Menu</h1>;
}

based on the example above, you’d now see the menu data in the console when the Menu component loads.

Now that we have access to the menu, we could do something like this:

import { useLoaderData } from "react-router-dom";
import { getMenu } from "../../services/apiRestaurant";
import MenuItem from "./MenuItem";

function Menu() {
const menu = useLoaderData();
console.log(menu);

return (
<ul>
{menu.map((item) => (
<MenuItem pizza={item} key={item.id} />
))}
</ul>
);
}

export async function loader() {
const menu = await getMenu();
return menu;
}

export default Menu;

so here we get the menu data from useLoaderData, then we map over each array item, and for each one create a MenuItem component where we pass in props of pizza which is the object in the array.

Let’s have a look at MenuItem

function MenuItem({ pizza }) {
const { id, name, unitPrice, ingredients, soldOut, imageUrl } = pizza;

return (
<li>
<img src={imageUrl} alt={name} />
<div>
<p>{name}</p>
<p>{ingredients.join(", ")}</p>
<div>
{!soldOut ? <p>{formatCurrency(unitPrice)}</p> : <p>Sold out</p>}
</div>
</div>
</li>
);
}

export default MenuItem;

We then de-structure the pizza object, and use it to get the imageUrl, the name, the ingredients, the soldOut boolean, and the the unitPrice.

Now on screen we’d see multiple MenuItem instances…one for each piza object…

The useNavigation() hook

The useNavigation() hook, which is not to be confused with useNavigate()! is a special hook that can help us track whehter our pages are loading, or are idle (i.e they have loaded!)

We can look at the state of useNavigation() in the console to see what we get when we move to the menu page, which is the component that will retireve data from an external api, and will therefore take a fraction of a second to load…

export default function AppLayout() {
const navigation = useNavigation();
console.log(navigation);

return (
<div>
<Header />

<main>
<Outlet />
</main>

<CartOverview />
</div>
);
}

notice we get a loading state first and then an idle state (we can ignore that they both show twice, as this is just to do with development mode when we use <React.StrictMode>)

So the fact that we have access to this information, means we can use it to our advantage to load a spinner, whilst the state is in its “loading” phase, and then switch to showing our component once it gets to the “idle” phase.

import { Outlet } from "react-router-dom";
import { useNavigation } from "react-router-dom";
import Header from "./Header";
import CartOverview from "../features/cart/CartOverview";
import Loader from "./Loader";

export default function AppLayout() {
const navigation = useNavigation();
console.log(navigation);
const isLoading = navigation.state === "loading";

return (
<div className="layout">
{isLoading && <Loader />}

<Header />

<main>
<Outlet />
</main>

<CartOverview />
</div>
);
}

So if navigation.state is “loading”, then isLoading will be set to true, and if isLoading is true then we’ll show the <Loader> component.

Error handling

Inside our createBroswerRouter we can also handle when users navigate to routes that do not exist, by using the errorElement option:

const router = createBrowserRouter([
{
element: <AppLayout />,
errorElement: <Error />,
children: [
{
path: "/",
element: <Home />,
},

So what we’re saying here, is that if a route doesn’t exist, then show our <Error> component.

import { useNavigate } from 'react-router-dom';

function Error() {
const navigate = useNavigate();

return (
<div>
<h1>Something went wrong 😢</h1>
<p>%MESSAGE%</p>
<button onClick={() => navigate(-1)}>&larr; Go back</button>
</div>
);
}

export default NotFound;

We can also get the actual error message by using the hook useRouteError()

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

function NotFound() {
const navigate = useNavigate();
const error = useRouteError();

return (
<div>
<h1>Something went wrong 😢</h1>
<p>{error.data}</p>
<button onClick={() => navigate(-1)}>&larr; Go back</button>
</div>
);
}

export default NotFound;

So we’re retrieving the data value from the error object

Params

In React Router version 6, URL parameters (often referred to as “params”) are used to capture values from the URL that can be used in your component to dynamically render content based on the route. For example, if you have a route to display user details that includes a user ID in the URL, you can capture that ID using params.

When you define a route in React Router v6, you specify parameters using the colon (:) syntax in the path prop of your route. Each parameter you define in the route path becomes available as a key in the params object.

For example:

      {
path: "/order/:orderId",
element: <Order />,
loader: OrderIdLoader,
},

So here, :orderId is the parameter.

To access params in your component, you then use the useParams() hook provided by React Router. This hook returns an object containing all the parameters for the current route, accessible by their names as specified in the route path.

So if you have a route defined as /order/:orderId, the :orderId part is a route parameter placeholder. When a user navigates to a URL that matches this pattern, for example, /order/1234, then 1234 is captured as the value of orderId.

The loader function receives an object as its argument, which includes params among other properties. The params object contains ALL route parameters as key-value pairs, where each key is the name of a parameter defined in the route path, and its value is the corresponding part of the URL.

Therefore, in the loader function, params.orderId accesses the value of orderId captured from the URL. So, if the URL is http://localhost:5173/order/1234, params.orderId would be 1234.

export async function loader({ params }) {
const order = await getOrder(params.orderId);
return order;
}

Therefore order equals 1234

Writing data to api with the React Router <Form> component

When ever the React Router Form is submitted, React Router will call the action() function and it will pass in the request that was submitted.

export async function action({ request }) {
const formData = await request.formData();
}

formData is provided by the browser.

We also always need to return something from the function, so we can do this….

export async function action({ request }) {
const formData = await request.formData();
console.log("formData", formData);

return {
status: 200,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ success: true }),
};
}

Whenever there is a new form submission on this route..

        path: "/order/new",
element: <CreateOrder />,
action: createOrderAction,
},

then this “createOrderAction” action will be called.

We renamed the action to be createOrderAction here in App.jsx when we imported the action:

import CreateOrder, {
action as createOrderAction,
} from "./features/order/CreateOrder";

So now, whenever we submit something in our React Router Form, React Router will call the action() function and it will pass in the request that was submitted.

function CreateOrder() {
// const [withPriority, setWithPriority] = useState(false);
const cart = fakeCart;

return (
<div>
<h2>Ready to order? Let's go!</h2>

<Form method="POST">
<div>
<label>First Name</label>
<input type="text" name="customer" required />
</div>

<div>
<label>Phone number</label>
<div>
<input type="tel" name="phone" required />
</div>
</div>

<div>
<label>Address</label>
<div>
<input type="text" name="address" required />
</div>
</div>

<div>
<input
type="checkbox"
name="priority"
id="priority"
// value={withPriority}
// onChange={(e) => setWithPriority(e.target.checked)}
/>
<label htmlFor="priority">Want to yo give your order priority?</label>
</div>

<div>
<button>Order now</button>
</div>
</Form>
</div>
);
}


export async function action({ request }) {
const formData = await request.formData();
console.log("formData", formData);

return {
status: 200,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ success: true }),
};
}

and our action function knows which form it is asociated with because of what’s in our createBrowserRouter object…

        path: "/order/new",
element: <CreateOrder />,
action: createOrderAction,
},

So whenever there is a form submission at /order/new the action function will be called in the <CreateOrder> function component.

When we receive the formData, we’ll also need to convert it to an object, and we do this as follows:

const data = Object.fromEntries(formData.entries());

Object.fromEntries() is a JavaScript statement that converts a FormData object into a plain JavaScript object.

Essentially, this converts key-value pairs into a JavaScript object where each key is associated with its corresponding value.

It basically converts something that looks like this:

[["name", "John"], ["age", "30"], ["sex", "male"]]

into something that looks like this:

{ name: "John Doe", age: "30", sex: "male" }

We can also pass objects into our form submission, rather than just input field information, by using a “hidden” input field in the form.

So for example, we could add this just before our form button….

        <div>
<input
type="checkbox"
name="priority"
id="priority"
// value={withPriority}
// onChange={(e) => setWithPriority(e.target.checked)}
/>
<label htmlFor="priority">Want to yo give your order priority?</label>
</div>

<div>
<input type="hidden" name="cart" value={JSON.stringify(cart)} />
<button>Order now</button>
</div>
</Form>
</div>
);
}

Note that we need to stringify our cart object first before we pass it into the form.

In terms of dispatching the data from the form, we first need to model it. So we’ll need to convert the cart data back into an object using JSON.parse() and also for our checkbox, we’ll need to set this to a boolean true or false depending on if it was ticked.

export async function action({ request }) {
const formData = await request.formData();
const data = Object.fromEntries(formData.entries());
console.log("formData", formData);

const order = {
...data,
priority: data.priority === "on",
cart: JSON.parse(data.cart),
};

console.log(order);

return {
status: 200,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ success: true }),
};
}

So we have spread out our order, then set “priority” to true IF the value was equal to “on”. We’ve also parsed the data.cart JSON-formatted string into a JavaScript object.

Now that our order data has been shaped, it is ready to be submitted.

To submitted the order data, we’ll pass it to the api with our createOrder() function.

export async function action({ request }) {
const formData = await request.formData();
const data = Object.fromEntries(formData.entries());
console.log("formData", formData);

const order = {
...data,
priority: data.priority === "on",
cart: JSON.parse(data.cart),
};

const newOrder = await createOrder(order);

return redirect(`/order/${newOrder.id}`);
}

export default CreateOrder;

here’s the createOrder() function:


const API_URL = 'https://react-fast-pizza-api.onrender.com/api';

export async function createOrder(newOrder) {
try {
const res = await fetch(`${API_URL}/order`, {
method: 'POST',
body: JSON.stringify(newOrder),
headers: {
'Content-Type': 'application/json',
},
});

if (!res.ok) throw Error();
const { data } = await res.json();
return data;
} catch {
throw Error('Failed creating your order');
}
}

So you can see that ultimately this returns some data after the POST request is completed, and this data is what we’ll use to perform a redirect inside our action, using the redirect() function provided by React Router

const newOrder = await createOrder(order);

return redirect(`/order/${newOrder.id}`);

So basically once the order has been posted, we’ll then redirect the URL to /order/12345 or whatever the order id happens to be.

useActionData()

The useActionData hook is specifically designed to fetch data related to an action after a form submission. This data often includes results from the server, such as validation errors or other feedback related to the form submission. In this case, formErrors would contain any errors returned by the action, allowing the component to conditionally render UI elements based on the presence of these errors (as seen in your previous snippet).

Here’s how we’d set it up:

  const formErrors = useActionData();

and now we can conditionally render something if there is an error:

        <div>
<label>Phone number</label>
<div>
<input type="tel" name="phone" required />
</div>
{formErrors?.phone && <p>{formErrors.phone}</p>}
</div>

in terms of getting those errors in the first place, we can log them by checking if there are any object keys added, based on the number of keys being greater than 0 (i.e. errors exist!)

  if (Object.keys(errors).length > 0) {
return errors;
}

const newOrder = await createOrder(order);

const errors = {};
if (!isValidPhone(order.phone)) errors.phone = "Invalid phone number";

So in the code above we run the isValidPhone function to check if the phone number being submitted in our form is valid, and if it’s not, then errors.phone will become “Invalid phone number”

Here’s the isValidPhone function for checking a number, using regex

// https://uibakery.io/regex-library/phone-number
const isValidPhone = (str) =>
/^\+?\d{1,4}?[-.\s]?\(?\d{1,3}?\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,9}$/.test(
str
);

In terms of how this function works….

  • ^: Asserts the start of the line.
  • \+?: Optionally matches a plus sign + at the beginning, allowing for international codes.
  • \d{1,4}: Matches between 1 to 4 digits. This could represent the country code.
  • [-.\s]?: Optionally matches a dash -, dot ., or whitespace character, allowing for separators in the phone number.
  • \(?: Optionally matches an opening parenthesis (, accommodating formats that include an area code in parentheses.
  • \d{1,3}: Matches between 1 to 3 digits, likely for an area or city code.
  • \)?: Optionally matches a closing parenthesis ).
  • The pattern repeats with variations to allow for different segments of a phone number, accommodating a wide range of international phone number formats.
  • $: Asserts the end of the line.
  • .test(str): This method is called on the regex pattern to test whether the string str matches the pattern. It returns true if the string matches.

useFetcher()

The useFetcher hook was introduced in React Router 6. It provides a way to interact with the router’s data loading capabilities, enabling components to trigger navigation and data loading without rendering a new route component. This hook is especially useful for tasks like submitting forms, loading more data on demand, or managing interactions that require server-side data fetching or updates without a full page reload.

You first use the hook by calling it within a React component. This gives you access to a fetcher object.

const fetcher = useFetcher();

.load

with the useFetcher hook from React Router, it triggers a request to load data from the specified endpoint. For example..

fetcher.load("/menu")

So in this example, it triggers a request to load data from the specified endpoint (in this case, /menu). Once this request completes and if it’s successful, the fetched data is stored within the fetcher object. Specifically, the data becomes accessible through the fetcher.data property.

If we want access to this data when the page first loads, we’d typically use it inside a useEffect() like this….

  const fetcher = useFetcher();

useEffect(() => {
if (!fetcher.data && fetcher.state === "idle") fetcher.load("/menu");
console.log("fetcher", fetcher);
}, [fetcher]);

What where saying here is that if there’s not already any data in the fecther object (i.e. it’s still empty) and also the current fetcher “state” is idle, then go ahead and fetch the menu data and load it into the fetcher object.

Here’s what our Menu component might be:

import { useLoaderData } from 'react-router-dom';
import { getMenu } from '../../services/apiRestaurant';
import MenuItem from './MenuItem';

function Menu() {
const menu = useLoaderData();

return (
<ul className="divide-y divide-stone-200 px-2">
{menu.map((pizza) => (
<MenuItem pizza={pizza} key={pizza.id} />
))}
</ul>
);
}

export async function loader() {
const menu = await getMenu();
return menu;
}

export default Menu;

You can see in this component we have access to the menu

so we have full access to the pizza object now, which includes things like pizza ingredients. All of this is now stored in our fetcher object.

Based on our useEffect, this is what you might see if you did a console log….

So the fetcher object was initially empty and in an idle state. The fetcher then fetched the data for menu, so it transitioned to a loading state. Once it had all the data it then when back to its idle state, and the data had now populated the object.

We can now take advantage of this data. For example:

      <ul className="dive-stone-200 divide-y border-b border-t">
{cart.map((item) => (
<OrderItem
item={item}
key={item.pizzaId}
isLoadingIngredients={fetcher.state === "loading"}
ingredients={
fetcher?.data?.find((pizza) => pizza.id == item.pizzaId)
?.ingredients ?? []
}
/>
))}
</ul>

So what we’re saying is to map through the cart array, and create an OrderItem where we’ll pass props of an item, a key, and ingredients, where the ingredients the ingredients array will be passed where the pizzaId matches the item pizzaId.

Also, not how optional chaining is used here and the nullish coalescing operator (??) to ensure that “something” will be returned, even if fetcher is null or undefined. In these cases, the nullish coalescing operator will return an empty array [].

--

--

Jeff P

I tend to write about anything I find interesting. There’s not much more to it than that really :-)