Skip to main content

Declarative Mode

Declarative Mode is the simplest way to use React Router. It enables basic routing features like matching URLs to components, navigating around your app, and providing active states - all declared with familiar React components.

Quick Start

1
Install React Router
2
npm install react-router
3
Add BrowserRouter
4
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
import App from "./App";

createRoot(document.getElementById("root")).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);
5
Add Routes
6
import { Routes, Route } from "react-router";
import Home from "./Home";
import About from "./About";

function App() {
  return (
    <Routes>
      <Route index element={<Home />} />
      <Route path="about" element={<About />} />
    </Routes>
  );
}
7
Start your dev server
8
npm run dev

Why Declarative Mode?

Declarative Mode is perfect when you:
  • Want the simplest possible routing setup
  • Are migrating from React Router v6
  • Have your own data loading solution
  • Are building a simple SPA
  • Don’t need data loading features built into the router
Declarative Mode gives you routing without the complexity of loaders, actions, or build tools.

Basic Routing

import { BrowserRouter, Routes, Route } from "react-router";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route index element={<Home />} />
        <Route path="about" element={<About />} />
        <Route path="contact" element={<Contact />} />
      </Routes>
    </BrowserRouter>
  );
}

Nested Routes and Layouts

Create layouts with nested routes using <Outlet>:
import { Routes, Route, Outlet } from "react-router";

function App() {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />} />
        <Route path="about" element={<About />} />
        <Route path="contact" element={<Contact />} />
      </Route>
    </Routes>
  );
}

function Layout() {
  return (
    <div>
      <header>
        <nav>{/* navigation */}</nav>
      </header>
      <main>
        <Outlet /> {/* Child routes render here */}
      </main>
      <footer>{/* footer */}</footer>
    </div>
  );
}

Dynamic Routes

Handle dynamic URL segments with URL parameters:
import { Routes, Route, useParams } from "react-router";

function App() {
  return (
    <Routes>
      <Route path="teams/:teamId" element={<Team />} />
      <Route path="users/:userId" element={<User />} />
    </Routes>
  );
}

function Team() {
  const { teamId } = useParams();
  return <h1>Team {teamId}</h1>;
}

Reading URL Data

import { useLocation } from "react-router";

function CurrentPath() {
  const location = useLocation();

  return (
    <div>
      <p>Current path: {location.pathname}</p>
      <p>Search params: {location.search}</p>
      <p>Hash: {location.hash}</p>
    </div>
  );
}

Common Patterns

1
Not Found (404) Pages
2
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  {/* Catch all unmatched routes */}
  <Route path="*" element={<NotFound />} />
</Routes>

function NotFound() {
  return (
    <div>
      <h1>404 - Page Not Found</h1>
      <Link to="/">Go Home</Link>
    </div>
  );
}
3
Splat Routes
4
import { useParams } from "react-router";

<Route path="files/*" element={<FileViewer />} />

function FileViewer() {
  const params = useParams();
  const filepath = params["*"]; // Everything after /files/
  return <div>Viewing: {filepath}</div>;
}
5
Multiple Route Groups
6
function App() {
  return (
    <BrowserRouter>
      <header>{/* Global header */}</header>

      {/* Main app routes */}
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard/*" element={<Dashboard />} />
      </Routes>

      <footer>{/* Global footer */}</footer>
    </BrowserRouter>
  );
}

function Dashboard() {
  return (
    <div>
      <aside>{/* Dashboard sidebar */}</aside>

      {/* Nested dashboard routes */}
      <Routes>
        <Route index element={<DashboardHome />} />
        <Route path="settings" element={<Settings />} />
        <Route path="profile" element={<Profile />} />
      </Routes>
    </div>
  );
}

Working with Data

In Declarative Mode, you handle data loading yourself:
import { useEffect, useState } from "react";
import { useParams } from "react-router";

function Team() {
  const { teamId } = useParams();
  const [team, setTeam] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    fetchTeam(teamId)
      .then(setTeam)
      .finally(() => setLoading(false));
  }, [teamId]);

  if (loading) return <div>Loading...</div>;
  return <h1>{team.name}</h1>;
}

When to Upgrade

Consider upgrading to Data Mode or Framework Mode if you need:
  • Built-in data loading with loaders
  • Form handling with actions
  • Pending UI states
  • Optimistic UI updates
  • Type safety (Framework Mode)
  • SSR/SSG (Framework Mode)
Declarative Mode is perfect for simple apps, but you can always upgrade to Data or Framework Mode as your needs grow.

Next Steps

1
Add a Data Library
2
Integrate React Query, SWR, or Apollo for data fetching.
3
Handle Authentication
4
Implement protected routes with conditional rendering or Navigate redirects.
5
Optimize Performance
6
Use React.lazy() and Suspense for code splitting.

Build docs developers (and LLMs) love