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
import { createRoot } from "react-dom/client" ;
import { BrowserRouter } from "react-router" ;
import App from "./App" ;
createRoot ( document . getElementById ( "root" )). render (
< BrowserRouter >
< App />
</ BrowserRouter >
);
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 >
);
}
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
Simple Routes
With Navigation
Active Links
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>:
Basic Nesting
Multi-level Nesting
Layout Routes
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 >
);
}
< Routes >
< Route path = "/" element = { < Root /> } >
< Route index element = { < Home /> } />
< Route path = "dashboard" element = { < Dashboard /> } >
< Route index element = { < DashboardHome /> } />
< Route path = "settings" element = { < Settings /> } />
< Route path = "profile" element = { < Profile /> } />
</ Route >
</ Route >
</ Routes >
{ /* Routes without a path create layouts */ }
< Routes >
< Route element = { < MarketingLayout /> } >
< Route index element = { < Home /> } />
< Route path = "contact" element = { < Contact /> } />
</ Route >
< Route path = "projects" element = { < ProjectsLayout /> } >
< Route index element = { < ProjectsList /> } />
< Route path = ":id" element = { < Project /> } />
</ Route >
</ Routes >
Dynamic Routes
Handle dynamic URL segments with URL parameters:
URL Parameters
Multiple Parameters
Optional 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 > ;
}
Navigation
Link Component
Programmatic Navigation
Navigate Component
import { Link } from "react-router" ;
function Navigation () {
return (
< nav >
< Link to = "/" > Home </ Link >
< Link to = "/about" > About </ Link >
< Link to = "/teams/123" > Team 123 </ Link >
{ /* Relative links */ }
< Link to = "../" > Up one level </ Link >
< Link to = "settings" > Settings (relative) </ Link >
</ nav >
);
}
import { useNavigate } from "react-router" ;
function LoginForm () {
const navigate = useNavigate ();
const handleSubmit = async ( e ) => {
e . preventDefault ();
await login ( formData );
navigate ( "/dashboard" );
};
return (
< form onSubmit = { handleSubmit } >
{ /* form fields */ }
< button type = "submit" > Login </ button >
< button onClick = { () => navigate ( - 1 ) } > Back </ button >
</ form >
);
}
import { Navigate } from "react-router" ;
function ProtectedRoute ({ user , children }) {
if ( ! user ) {
return < Navigate to = "/login" replace /> ;
}
return children ;
}
function App () {
return (
< Routes >
< Route
path = "/dashboard"
element = {
< ProtectedRoute user = { user } >
< Dashboard />
</ ProtectedRoute >
}
/>
</ Routes >
);
}
Reading URL Data
Location
Search Params
Match Routes
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
< 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 >
);
}
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 > ;
}
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
Integrate React Query, SWR, or Apollo for data fetching.
Implement protected routes with conditional rendering or Navigate redirects.
Use React.lazy() and Suspense for code splitting.