React Router Cheat Sheet & Reference

React Router Cheat Sheet & Reference

This guide is part of the “Cheat Sheets & References for Developers & Engineers” series. This series is designed to provide comprehensive references for popular technologies!

React Router is a popular library for building single-page applications (SPAs) in React. It allows developers to create dynamic, client-side routing that is both powerful and flexible.

With React Router, you can easily navigate between different views and components without needing a server to handle each request. This makes for a faster, more responsive user experience that is essential for modern web applications.

This React Router Cheat Sheet & Reference will serve as a comprehensive guide for developers of all skill levels, providing you with a quick reference to the most commonly used features and syntax of React Router.

# Getting Started

Install

$ npm create vite@latest my-app --\
     --template react
# follow instructions
$ cd my-app
$ npm install react-router-dom \
    localforage \
    match-sorter \
    sort-by
$ npm run dev

Add Router

import React from "react";
import ReactDOM from "react-dom/client";
import {
  createBrowserRouter,
  RouterProvider,
} from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/",
    element: <div>Hello world!</div>,
  },
]);

const root=document.getElementById('root');

ReactDOM.createRoot(root).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

Root Route

import Root from "./routes/root";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
  },
]);
 

Error Page Handling

import ErrorPage from "./error-page";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
  },
]);

Contact UI

import Contact from "./routes/contact";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
  },
  {
    path: "contacts/:contactId",
    element: <Contact />,
  },
]);

Nested Routes

src/main.jsx

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
      },
    ],
  },
]);

src/routes/root.jsx

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

export default function Root() {
  return (
    <>
      {/* All other elements */}
      <div id="detail">
        <Outlet />
      </div>
    </>
  );
}
 

Client Routing

import {
  Outlet, Link
} from "react-router-dom";

export default function Root() {
  return (
    <ul>
      <li>
        <Link to={`contacts/1`}>
          Your Name
        </Link>
      </li>
      <li>
        <Link to={`contacts/2`}>
          Your Team
        </Link>
      </li>
    </ul>
  );
}

Contact Form

Create 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 };
}
export default function Root() {
  const { contacts } = useLoaderData();
  return (
    <div id="sidebar">
      <h1>React Router Contacts</h1>
      <Form method="post">
        <button type="submit">New</button>
      </Form>
    </div>
  );
}

Import and set the action on the route

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 />,
      },
    ],
  },
]);

URL Parameters (loader)

[
  {
    path: "contacts/:contactId",
    element: <Contact />,
  },
];

The :contactId URL segment. The colon (:) has a special meaning and turns it into a "dynamic segment"

import {
  useLoaderData
} from "react-router-dom";
import { getContact } from "../contacts";

export async function loader({ params }) {
  return getContact(params.contactId);
}

export default function Contact() {
  const contact = useLoaderData();
  // existing code
}

Configure the loader on the route

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,
      },
    ],
  },
]);
 

Update Data

import {
  Form, useLoaderData
} from "react-router-dom";

export default function EditContact() {
  const contact = useLoaderData();

  return (
    <Form method="post" id="contact-form">
      <label>
        <span>Twitter</span>
        <input
          type="text"
          name="twitter"
          placeholder="@elonmusk"
          defaultValue={contact.twitter}
        />
      </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>
  );
}

Adding a new edit route

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,
      },
    ],
  },
]);
import {
  NavLink,
} from "react-router-dom";

<NavLink
  to={`contacts/${contact.id}`}
  className={({ isActive, isPending }) =>
    isActive
      ? "active"
      : isPending
      ? "pending"
      : ""
  }
>
  {/* other code */}
</NavLink>

Global Pending UI

import {
  useNavigation,
} from "react-router-dom";

export default function Root() {
  const { contacts } = useLoaderData();
  const navigation = useNavigation();

  return (
    <div
      id="detail"
      className={
        navigation.state === 'loading'
          ? 'loading' : ''
      }
    >
      <Outlet />
    </div>
  );
}
 

Update Contacts Using FormData

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}`);
}

Connecting actions to routes

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,
      },
    ],
  },
]);

Delete Records

<Form
  method="post"
  action="destroy"
  onSubmit={(event) => {
    if (!confirm("Please confirm that you want to delete this record")) {
      event.preventDefault();
    }
  }}
>
  <button type="submit">Delete</button>
</Form>

Add destruction action

import {redirect} from "react-router-dom";
import {deleteContact} from "../contacts";

export async function action({ params }) {
  await deleteContact(params.contactId);
  return redirect("/");
}

Adding a destroy route to the routing configuration

import {
  action as destroyAction
} from "./routes/destroy";

const router = createBrowserRouter([
  {
    path: "/",
    children: [
      {
       path: "contacts/:contactId/destroy",
       action: destroyAction,
      },
    ],
  },
]);
 

Contextual Errors

export async function action({ params }) {
  throw new Error("something went wrong");
  await deleteContact(params.contactId);
  return redirect("/");
}

Let's create a contextual error message for the destroy route.

[
  {
    path: "contacts/:contactId/destroy",
    action: destroyAction,
    errorElement: <div>Oops! There is a mistake.</div>,
  },
];

Home Routing

import Index from "./routes/index";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    loader: rootLoader,
    action: rootAction,
    children: [
      { index: true, element: <Index /> },
    ],
  },
]);

Cancel Button

import {
  Form,
  useLoaderData,
  redirect,
  useNavigate,
} from "react-router-dom";

export default function Edit() {
  const contact = useLoaderData();
  const navigate = useNavigate();

  return (
    <Form method="post" id="contact-form">
      <div>
        <button type="submit">Save</button>
        <button
          type="button"
          onClick={() => {
            navigate(-1);
          }}
        >
          Cancel
        </button>
      </div>
    </Form>
  );
}
 

Fetch Submissions Using Client-side Routing

Changing form to Form

<Form id="search-form" role="search">
  <input
    id="q"
    aria-label="Search contacts"
    placeholder="Search"
    type="search"
    name="q"
  />
</Form>

If there is a URLSearchParams filter list

export async function loader({ request }) {
  const url = new URL(request.url);
  const q = url.searchParams.get("q");
  const contacts = await getContacts(q);
  return { contacts };
}

Sync URL to Form State

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 (
    <Form id="search-form" role="search">
      <input
        id="q"
        aria-label="Search contacts"
        placeholder="Search"
        type="search"
        name="q"
        defaultValue={q}
      />
      {/* existing code */}
    </Form>
  );
}
 

Sync Input Values with URL Search Params

import { useEffect } from "react";

export default function Root() {
  const { contacts, q } = useLoaderData();
  const navigation = useNavigation();

  useEffect(() => {
    document.getElementById("q").value = q;
  }, [q]);
}

Submit Changes Forms

import {
  useSubmit,
} from "react-router-dom";

export default function Root() {
  const { contacts, q } = useLoaderData();
  const navigation = useNavigation();
  const submit = useSubmit();

  return (
    <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>
  );
}

# Routers

Choose a Router

In v6.4, new routers have been introduced to support the new data APIs.

createBrowserRouter
createMemoryRouter
createHashRouter

The following routers do not support the data API.

BrowserRouter
MemoryRouter
HashRouter
NativeRouter
StaticRouter

Routing Example

The easiest way to quickly update to v6.4 is to get help from createRoutesFromElements, so you don't need to convert the element to a Route object.

import {
  createBrowserRouter,
  createRoutesFromElements,
  Route,
  RouterProvider,
} from "react-router-dom";

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<Root />}>
      <Route path="dashboard" element={<Dashboard />} />
      {/* ... etc. */}
    </Route>
  )
);

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);
 

createBrowserRouter

import * as React from "react";
import * as ReactDOM from "react-dom";
import {
  createBrowserRouter,
  RouterProvider,
} from "react-router-dom";

import Root, {rootLoader} from "./root";
import Team, {teamLoader} from "./team";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader,
    children: [
      {
        path: "team",
        element: <Team />,
        loader: teamLoader,
      },
    ],
  },
]);

const root=document.getElementById('root');

ReactDOM.createRoot(root).render(
  <RouterProvider router={router} />
);

Type Declaration

function createBrowserRouter(
  routes: RouteObject[],
  opts?: {
    basename?: string;
    window?: Window;
  }
): RemixRouter;

Routes

createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader,
    children: [
      {
        path: "events/:id",
        element: <Event />,
        loader: eventLoader,
      },
    ],
  },
]);

basename is used when you cannot deploy to the root of the domain, but to a subdirectory

createBrowserRouter(routes, {
  basename: "/app",
});

createBrowserRouter(routes, {
  basename: "/app",
});
<Link to="/" />;
// results in <a href="/app" />

createBrowserRouter(routes, {
  basename: "/app/",
});
<Link to="/" />;
// results in <a href="/app/" />

createHashRouter

import * as React from "react";
import * as ReactDOM from "react-dom";
import {
  createHashRouter,
  RouterProvider,
} from "react-router-dom";

import Root, { rootLoader } from "./root";
import Team, { teamLoader } from "./team";

const router = createHashRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader,
    children: [
      {
        path: "team",
        element: <Team />,
        loader: teamLoader,
      },
    ],
  },
]);

const root=document.getElementById('root');

ReactDOM.createRoot(root).render(
  <RouterProvider router={router} />
);
 

createMemoryRouter

import {
  RouterProvider,
  createMemoryRouter,
} from "react-router-dom";
import * as React from "react";
import {
  render,
  waitFor,
  screen,
} from "@testing-library/react";
import "@testing-library/jest-dom";
import CalendarEvent from "./event";

test("event route", async () => {
  const FAKE_EVENT = { name: "Test events" };
  const routes = [
    {
      path: "/events/:id",
      element: <CalendarEvent />,
      loader: () => FAKE_EVENT,
    },
  ];

  const router=createMemoryRouter(routes,{
    initialEntries: ["/", "/events/123"],
    initialIndex: 1,
  });

  render(
    <RouterProvider router={router} />
  );

  await waitFor(
    () => screen.getByRole("heading")
  );
  expect(screen.getByRole("heading"))
    .toHaveTextContent(
    FAKE_EVENT.name
  );
});

initialEntries

createMemoryRouter(routes, {
  initialEntries: ["/", "/events/123"],
});

initialIndex

createMemoryRouter(routes, {
  initialEntries: ["/", "/events/123"],
  initialIndex: 1,
  // start at "/events/123"
});

<RouterProvider>

import {
  createBrowserRouter,
  RouterProvider,
} from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      {
        path: "dashboard",
        element: <Dashboard />,
      },
      {
        path: "about",
        element: <About />,
      },
    ],
  },
]);

const root=document.getElementById('root');

ReactDOM.createRoot(root).render(
  <RouterProvider
    router={router}
    fallbackElement={<BigSpinner />}
  />
);

fallbackElement - If you are not the server rendering your application, DataBrowserRouter will start all matching route loaders at installation time. During this time, you can provide a fallbackElement to indicate to the user that the application is running.

<RouterProvider
  router={router}
  fallbackElement={<SpinnerOfDoom />}
/>
 

# Router Components

<Router>

Router is a low-level interface shared by all router components, such as BrowserRouter and StaticRouter. 

In the case of React, Router is a context provider that provides routing information to the rest of the application.

<BrowserRouter>

import * as React from "react";
import * as ReactDOM from "react-dom";
import {
  BrowserRouter
} from "react-router-dom";

const root=document.getElementById('root');
ReactDOM.createRoot(root).render(
  <HashRouter>
    {/* The rest of your application is here */}
  </HashRouter>,
);

BrowserRouter stores the current location in the browser's address bar using a clean URL and uses the browser's built-in history stack for navigation

 

<NativeRouter>

NativeRouter is the recommended interface for running React Router in React Native applications.

import * as React from "react";
import {
  NativeRouter
} from "react-router-native";

function App() {
  return (
    <NativeRouter>
      {/* The rest of your app is here */}
    </NativeRouter>
  );
}

<MemoryRouter>

MemoryRouter stores its location internally in an array. Unlike BrowserHistory and HashHistory, it does not rely on external sources, such as the history stack in the browser. This makes it ideal for scenarios where full control of the history stack is needed, such as testing.

import * as React from "react";
import { create } from "react-test-renderer";
import {
  MemoryRouter,
  Routes,
  Route,
} from "react-router-dom";

describe("My app", () => {
  it("renders correctly", () => {
    let renderer = create(
      <MemoryRouter initialEntries={["/users/mjackson"]}>
        <Routes>
          <Route path="users" element={<Users />}>
            <Route path=":id" element={<UserProfile />} />
          </Route>
        </Routes>
      </MemoryRouter>
    );

    expect(renderer.toJSON()).toMatchSnapshot();
  });
});
 

<HashRouter>

HashRouter for web browsers that for some reason should not (or cannot) send URLs to the server.

import * as React from "react";
import * as ReactDOM from "react-dom";
import {
  HashRouter
} from "react-router-dom";

const root=document.getElementById('root');
ReactDOM.createRoot(root).render(
  <HashRouter>
    {/* The rest of your application is here */}
  </HashRouter>,
);

<StaticRouter>

import * as React from "react";
import * as ReactDOMServer from "react-dom/server";
import { StaticRouter } from "react-router-dom/server";
import http from "http";

function requestHandler(req, res) {
  let html = ReactDOMServer.renderToString(
    <StaticRouter location={req.url}>
      {/* The rest of your app is here */}
    </StaticRouter>
  );

  res.write(html);
  res.end();
}

http.createServer(requestHandler)
    .listen(3000);
 

Links & resources