
React Router Declarative Mode
- 567 installs
- 138 repo stars
- Updated June 18, 2026
- remix-run/agent-skills
react-router-declarative-mode is a Claude Code skill that generates correct React Router v6.4+ JSX route declarations, navigation components, and URL value hooks without data loaders for developers who need BrowserRouter
About
react-router-declarative-mode is a Remix-run agent skill for React Router's declarative mode using BrowserRouter, Routes, Route, Link, NavLink, useNavigate, and URL param hooks. The skill steers agents away from loaders, actions, and data-router APIs so generated code matches the simplest React Router v6.4+ pattern for SPAs. Developers reach for react-router-declarative-mode when scaffolding or refactoring route trees, active navigation, and query or path param reads in React apps that do not need server-driven data loading. It is MIT-licensed and ships from remix-run/agent-skills as a focused routing reference for JSX-first React projects.
- Generates declarative <BrowserRouter>, <Routes>, and <Route> configurations
- Produces Link, NavLink, and useNavigate patterns with correct active states
- Handles useParams, useSearchParams, and useLocation hooks accurately
- Loads targeted reference files: routing.md, navigation.md, url-values.md
- Enforces React Router declarative mode without Remix data APIs
React Router Declarative Mode by the numbers
- 567 all-time installs (skills.sh)
- +16 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #589 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/remix-run/agent-skills --skill react-router-declarative-modeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 567 |
|---|---|
| repo stars | ★ 138 |
| Last updated | June 18, 2026 |
| Repository | remix-run/agent-skills ↗ |
How do you set up React Router declarative routes?
Generate correct React Router v6.4+ JSX route declarations, navigation components, and URL value hooks without data loaders.
Who is it for?
React developers building SPAs with React Router v6.4+ declarative mode who want JSX routes and navigation without data loaders.
Skip if: Teams using React Router data routers with loaders, actions, or framework-mode routing should skip this skill.
When should I use this skill?
The user configures BrowserRouter routes, Link or NavLink navigation, or URL param hooks without React Router loaders or actions.
What you get
JSX route trees, Link and NavLink navigation, useNavigate calls, and useParams or useSearchParams hook usage without loaders or actions.
- JSX route configuration
- navigation components
- URL param hook usage
Files
React Router Declarative Mode
Declarative mode is React Router's simplest mode using <BrowserRouter>, <Routes>, and <Route> for basic client-side routing without data loading features like loaders or actions.
When to Apply
- Using
<BrowserRouter>for routing - Configuring routes with
<Routes>and<Route> - Navigating with
<Link>,<NavLink>, oruseNavigate - Reading URL params with
useParams - Working with search params using
useSearchParams - Accessing location with
useLocation
References
Load the relevant reference for detailed guidance on the specific API/concept:
| Reference | Use When |
|---|---|
references/routing.md | Configuring routes, nested routes, dynamic params |
references/navigation.md | Links, NavLink active states, programmatic nav |
references/url-values.md | Reading params, search params, location |
Critical Patterns
These are the most important patterns to follow. Load the relevant reference for full details.
Basic Route Setup
Configure routes with JSX using <Routes> and <Route>:
import { BrowserRouter, Routes, Route } from "react-router";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="dashboard" element={<Dashboard />}>
<Route index element={<DashboardHome />} />
<Route path="settings" element={<Settings />} />
</Route>
<Route path="users/:userId" element={<User />} />
</Routes>
</BrowserRouter>
);
}NavLink Active States
Use NavLink for navigation with active styling:
import { NavLink } from "react-router";
function Nav() {
return (
<nav>
<NavLink
to="/"
end
className={({ isActive }) => (isActive ? "active" : "")}
>
Home
</NavLink>
<NavLink
to="/dashboard"
className={({ isActive }) => (isActive ? "active" : "")}
>
Dashboard
</NavLink>
</nav>
);
}Reading URL Params
Use useParams to read dynamic route segments:
import { useParams } from "react-router";
function User() {
const { userId } = useParams();
return <h1>User {userId}</h1>;
}Working with Search Params
Use useSearchParams for query string values:
import { useSearchParams } from "react-router";
function SearchResults() {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get("q");
return (
<div>
<input
value={query || ""}
onChange={(e) => setSearchParams({ q: e.target.value })}
/>
<p>Results for: {query}</p>
</div>
);
}Further Documentation
If anything related to React Router is not covered in these references, you can search the official documentation:
https://reactrouter.com/docs
Navigation
React Router provides several ways to navigate between routes in declarative mode.
Link Component
Basic navigation between routes:
import { Link } from "react-router";
function Nav() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/products/123">Product</Link>
</nav>
);
}Link Props
| Prop | Purpose | Example |
|---|---|---|
to | Destination path (string or object) | to="/dashboard" |
replace | Replace current history entry | replace |
state | Pass state to the destination | state={{ from: "home" }} |
<Link to="/dashboard" replace state={{ from: "home" }}>
Dashboard
</Link>NavLink Component
Link with active state awareness for styling navigation:
import { NavLink } from "react-router";
function Nav() {
return (
<nav>
<NavLink to="/" end>
Home
</NavLink>
<NavLink to="/products">Products</NavLink>
</nav>
);
}Automatic Active Class
NavLink automatically adds an .active class when active, so you can style it with CSS:
a.active {
color: blue;
font-weight: bold;
}For custom class names, use the className function:
<NavLink
to="/products"
className={({ isActive }) => (isActive ? "nav-active" : "nav-link")}
>
Products
</NavLink>NavLink Props
| Prop | Purpose | Example |
|---|---|---|
end | Only match exact path (not prefixes) | <NavLink to="/" end> |
className | String or function with { isActive } | className={({ isActive }) => ...} |
style | Object or function with { isActive } | style={({ isActive }) => ...} |
children | Can be a function with { isActive } | {({ isActive }) => <span>...</span>} |
Styling Active Links
// With className function
<NavLink
to="/dashboard"
className={({ isActive }) =>
isActive ? "nav-link active" : "nav-link"
}
>
Dashboard
</NavLink>
// With style function
<NavLink
to="/dashboard"
style={({ isActive }) => ({
fontWeight: isActive ? "bold" : "normal",
color: isActive ? "blue" : "gray",
})}
>
Dashboard
</NavLink>
// With children function
<NavLink to="/messages">
{({ isActive }) => (
<span className={isActive ? "active" : ""}>
Messages {isActive && "✓"}
</span>
)}
</NavLink>The end Prop
Without end, NavLink matches if the current URL starts with the to path:
// ❌ Without end: both are active at /dashboard/settings
<NavLink to="/">Home</NavLink> // active at / AND /dashboard
<NavLink to="/dashboard">Dashboard</NavLink> // active at /dashboard AND /dashboard/settings
// ✅ With end: exact matching
<NavLink to="/" end>Home</NavLink> // only active at /
<NavLink to="/dashboard" end>Dashboard</NavLink> // only active at /dashboarduseNavigate Hook
Programmatic navigation for situations where the user is _not_ directly clicking a link:
import { useNavigate } from "react-router";
function LoginForm() {
const navigate = useNavigate();
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
await login();
navigate("/dashboard");
}
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
<button type="submit">Login</button>
</form>
);
}Important: PreferLinkorNavLinkfor user-initiated navigation. They provide better UX including keyboard events, accessibility, right-click menus, and "open in new tab". ReserveuseNavigatefor:
>
- After form submissions complete
- Logging out after inactivity
- Redirects based on data/conditions
- Timed UIs (quizzes, etc.)
Navigate Options
const navigate = useNavigate();
// Basic navigation
navigate("/dashboard");
// Replace history entry (back button skips this page)
navigate("/login", { replace: true });
// Pass state to destination
navigate("/checkout", { state: { cartId: "abc" } });
// Go back/forward in history
navigate(-1); // Go back one page
navigate(-2); // Go back two pages
navigate(1); // Go forward one pageWhen to Use Each Approach
| Approach | Use Case |
|---|---|
<Link> | Standard navigation, SEO-friendly links |
<NavLink> | Navigation menus with active state styling |
useNavigate | After async operations, in event handlers |
Relative Navigation
Paths without a leading / are relative to the current route:
// Current URL: /products/123
<Link to="reviews"> // → /products/123/reviews
<Link to="../456"> // → /products/456
<Link to=".."> // → /productsRelative Path vs Route
By default, .. is relative to the route hierarchy. Use relative="path" for URL path relativity:
// Route: /products/:id, Current URL: /products/123
<Link to=".."> // → / (parent route)
<Link to=".." relative="path"> // → /products (parent path segment)Passing State
Pass data to the destination without putting it in the URL:
// Sending state
<Link to="/checkout" state={{ cartId: "abc123", from: "/cart" }}>
Checkout
</Link>;
// Or with useNavigate
navigate("/checkout", { state: { cartId: "abc123" } });Reading state at the destination - see url-values.md.
Anti-Patterns
// ❌ DON'T: Use anchor tags for internal navigation
<a href="/about">About</a> // causes full page reload
// ✅ DO: Use Link for client-side navigation
<Link to="/about">About</Link>// ❌ DON'T: Use window.location for navigation
function handleClick() {
window.location.href = "/dashboard"; // full page reload
}
// ✅ DO: Use useNavigate for programmatic navigation
function handleClick() {
navigate("/dashboard"); // client-side navigation
}See Also
- routing.md - Route configuration
- url-values.md - Reading location state
- React Router Navigation Documentation
Routing
In declarative mode, routes are configured using JSX with <Routes> and <Route> components wrapped in a <BrowserRouter>.
Basic Setup
import { BrowserRouter, Routes, Route } from "react-router";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="contact" element={<Contact />} />
</Routes>
</BrowserRouter>
);
}Route Component Props
| Prop | Purpose | Example |
|---|---|---|
path | URL pattern to match | path="users/:id" |
element | Component to render | element={<User />} |
index | Default child route (no path) | <Route index element={...} /> |
children | Nested routes | <Route path="dashboard">...</Route> |
Nested Routes
Child routes are nested inside parent routes. Use <Outlet /> in the parent to render the matched child:
import { Routes, Route, Outlet } from "react-router";
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<nav>
<Link to="settings">Settings</Link>
<Link to="profile">Profile</Link>
</nav>
<Outlet />
</div>
);
}
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="dashboard" element={<Dashboard />}>
<Route index element={<DashboardHome />} />
<Route path="settings" element={<Settings />} />
<Route path="profile" element={<Profile />} />
</Route>
</Routes>
</BrowserRouter>
);
}This creates:
/dashboard→<Dashboard>with<DashboardHome />in the outlet/dashboard/settings→<Dashboard>with<Settings />in the outlet/dashboard/profile→<Dashboard>with<Profile />in the outlet
Layout Routes
Routes without a path act as layout wrappers:
<Routes>
<Route element={<MarketingLayout />}>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="contact" element={<Contact />} />
</Route>
<Route element={<AppLayout />}>
<Route path="dashboard" element={<Dashboard />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>All routes under a layout route render inside that layout's <Outlet />.
Index Routes
Index routes render at the parent's URL (the "default" child):
<Routes>
<Route path="/" element={<Home />} />
<Route path="dashboard" element={<Dashboard />}>
<Route index element={<DashboardHome />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>/→<Home />/dashboard→<Dashboard>with<DashboardHome />in outlet/dashboard/settings→<Dashboard>with<Settings />in outlet
Index routes cannot have children.
Route Prefixes
A Route with path but without element adds a path prefix to its children without introducing a layout:
<Routes>
{/* /projects/... without a shared layout */}
<Route path="projects">
<Route index element={<ProjectsHome />} />
<Route path=":projectId" element={<Project />} />
<Route path=":projectId/edit" element={<EditProject />} />
</Route>
</Routes>This is useful when routes share a path prefix but don't need a shared layout component.
Dynamic Segments
Segments starting with : are dynamic and captured as params:
<Route path="users/:userId" element={<User />} />
<Route path="posts/:postId/comments/:commentId" element={<Comment />} />Access params with useParams():
import { useParams } from "react-router";
function User() {
const { userId } = useParams();
return <h1>User {userId}</h1>;
}Optional Segments
Add ? to make a segment optional:
<Route path=":lang?/products" element={<Products />} />
// matches /products and /en/products
<Route path="users/:userId/edit?" element={<User />} />
// matches /users/123 and /users/123/editSplats (Catch-All)
Match any remaining path with *:
<Route path="files/*" element={<FileViewer />} />import { useParams } from "react-router";
function FileViewer() {
const { "*": filePath } = useParams();
// filePath = "docs/intro.md" for /files/docs/intro.md
return <div>Viewing: {filePath}</div>;
}404 Catch-All
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>Anti-Pattern: Flat Routes
// ❌ DON'T: Flat structure when routes share layout
function App() {
return (
<Routes>
<Route path="dashboard" element={<Dashboard />} />
<Route path="dashboard/settings" element={<DashboardSettings />} />
<Route path="dashboard/profile" element={<DashboardProfile />} />
</Routes>
);
}
// ✅ DO: Use nested routes with shared layout
function App() {
return (
<Routes>
<Route path="dashboard" element={<DashboardLayout />}>
<Route index element={<Dashboard />} />
<Route path="settings" element={<DashboardSettings />} />
<Route path="profile" element={<DashboardProfile />} />
</Route>
</Routes>
);
}Nested routes:
- Share layout code via
<Outlet /> - Reduce code duplication
- Make it clear which routes are related
- Enable layout-level state persistence
Linking Basics
Use <Link> for navigation between routes:
import { Link } from "react-router";
function Nav() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/users/123">User 123</Link>
</nav>
);
}See navigation.md for full details on navigation patterns.
See Also
- navigation.md - Link, NavLink, and useNavigate
- url-values.md - Reading params and search params
- React Router Documentation
URL Values
React Router provides hooks to read values from the current URL: route params, search params (query string), and the location object.
useParams
Read dynamic segments from the route path:
// Route: <Route path="users/:userId" element={<User />} />
// URL: /users/123
import { useParams } from "react-router";
function User() {
const { userId } = useParams();
// userId = "123"
return <h1>User {userId}</h1>;
}Multiple Params
// Route: <Route path="teams/:teamId/members/:memberId" element={<Member />} />
// URL: /teams/abc/members/456
function Member() {
const { teamId, memberId } = useParams();
// teamId = "abc", memberId = "456"
return (
<h1>
Member {memberId} of Team {teamId}
</h1>
);
}Splat Params
// Route: <Route path="files/*" element={<FileViewer />} />
// URL: /files/docs/intro.md
function FileViewer() {
const { "*": filePath } = useParams();
// filePath = "docs/intro.md"
return <div>Viewing: {filePath}</div>;
}Type Safety
Params are always string | undefined. Parse and validate as needed:
function User() {
const { userId } = useParams();
// ❌ DON'T: Assume params exist or are numbers
const id = parseInt(userId); // userId could be undefined
// ✅ DO: Handle undefined and validate
if (!userId) {
return <div>User ID required</div>;
}
const id = parseInt(userId, 10);
if (isNaN(id)) {
return <div>Invalid user ID</div>;
}
return <h1>User {id}</h1>;
}useSearchParams
Read and update the query string (?key=value):
// URL: /search?q=react&page=2
import { useSearchParams } from "react-router";
function SearchResults() {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get("q"); // "react"
const page = searchParams.get("page"); // "2"
const missing = searchParams.get("foo"); // null
return <div>Searching for: {query}</div>;
}searchParams is a URLSearchParams object:
Updating Search Params
function SearchForm() {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get("q") || "";
function handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
const isFirstSearch = !searchParams.has("q");
if (value) {
setSearchParams(
{ q: value },
{ replace: !isFirstSearch }, // Replace while typing, push on first search
);
} else {
setSearchParams({}, { replace: true });
}
}
return (
<input
type="search"
value={query}
onChange={handleSearch}
placeholder="Search..."
/>
);
}Tip: Use replace: true for incremental updates like type-ahead search. This prevents the back button from cycling through every keystroke.Navigation Options
setSearchParams accepts navigation options as a second argument:
function Filters() {
const [searchParams, setSearchParams] = useSearchParams();
function applyFilter(category: string) {
setSearchParams(
{ category },
{
replace: true, // Don't add to history (back button skips)
preventScrollReset: true, // Keep scroll position
},
);
}
return (
<button onClick={() => applyFilter("electronics")}>Electronics</button>
);
}| Option | Purpose |
|---|---|
replace | Replace history entry instead of pushing |
preventScrollReset | Keep current scroll position after navigation |
state | Pass state to the new location |
Preserving Other Params
When updating search params programmatically, preserve existing params:
function SortDropdown() {
const [searchParams, setSearchParams] = useSearchParams();
const sort = searchParams.get("sort") || "name";
function handleSortChange(newSort: string) {
// ❌ DON'T: Overwrite all params
setSearchParams({ sort: newSort });
// ✅ DO: Preserve existing params (like page, filters)
setSearchParams((prev) => {
prev.set("sort", String(newSort));
return prev;
});
}
return (
<select value={sort} onChange={(e) => handleSortChange(e.target.value)}>
<option value="name">Name</option>
<option value="date">Date</option>
</select>
);
}Note: For pagination and other navigation controls, prefer the Link-based approach below for better accessibility.
Link-based Pagination (Preferred)
For pagination and similar controls, prefer <Link> over buttons with setSearchParams:
import { Link, useSearchParams } from "react-router";
function Pagination({
currentPage,
totalPages,
}: {
currentPage: number;
totalPages: number;
}) {
const [searchParams] = useSearchParams();
// Build URL that preserves existing search params
function getPageUrl(page: number): string {
const params = new URLSearchParams(searchParams);
params.set("page", String(page));
return `?${params.toString()}`;
}
return (
<nav aria-label="Pagination">
{currentPage > 1 && (
<Link to={getPageUrl(currentPage - 1)}>Previous</Link>
)}
<span>
Page {currentPage} of {totalPages}
</span>
{currentPage < totalPages && (
<Link to={getPageUrl(currentPage + 1)}>Next</Link>
)}
</nav>
);
}Why prefer Links over buttons:
- Accessibility: Real anchor elements work with screen readers and keyboard navigation
- Right-click: Users can open in new tab or copy link
- Middle-click: Opens in new tab automatically
- Browser features: Back/forward, bookmarking work naturally
- SEO: Search engines can discover paginated content
// ❌ DON'T: Use buttons for navigation that changes URL
<button onClick={() => setSearchParams({ page: "2" })}>
Page 2
</button>
// ✅ DO: Use Links for URL-changing navigation
<Link to="?page=2">Page 2</Link>Multiple Values
// URL: /products?tag=react&tag=typescript
function Products() {
const [searchParams] = useSearchParams();
// Get all values for a key
const tags = searchParams.getAll("tag");
// tags = ["react", "typescript"]
return <div>Tags: {tags.join(", ")}</div>;
}Default Values
function ProductList() {
const [searchParams] = useSearchParams({
page: "1",
sort: "name",
});
const page = searchParams.get("page"); // "1" if not in URL
const sort = searchParams.get("sort"); // "name" if not in URL
return (
<div>
Page {page}, sorted by {sort}
</div>
);
}useLocation
Access the full location object:
import { useLocation } from "react-router";
function CurrentPath() {
const location = useLocation();
return <pre>{JSON.stringify(location, null, 2)}</pre>;
}Location Object Properties
| Property | Type | Description | Example |
|---|---|---|---|
pathname | string | The path portion of the URL | "/users/123" |
search | string | The query string (including ?) | "?q=react&page=2" |
hash | string | The hash portion (including #) | "#section-1" |
state | any | State passed during navigation | { from: "/home" } |
key | string | Unique key for this location | "default" or random key |
// URL: /search?q=react#results
// Navigated with state: { from: "/home" }
function SearchPage() {
const location = useLocation();
console.log(location.pathname); // "/search"
console.log(location.search); // "?q=react"
console.log(location.hash); // "#results"
console.log(location.state); // { from: "/home" }
console.log(location.key); // "abc123" (unique key)
}Reading Navigation State
State passed via <Link state={...}> or navigate(path, { state }):
// Sender
<Link to="/checkout" state={{ cartId: "abc", from: "/cart" }}>
Checkout
</Link>;
// Receiver
function Checkout() {
const location = useLocation();
const { cartId, from } = location.state || {};
return (
<div>
<h1>Checkout</h1>
{from && <Link to={from}>← Back</Link>}
<p>Cart: {cartId}</p>
</div>
);
}Watching Location Changes
import { useEffect } from "react";
import { useLocation } from "react-router";
function Analytics() {
const location = useLocation();
useEffect(() => {
// Track page views on route change
trackPageView(location.pathname);
}, [location]);
return null;
}Combining Hooks
// URL: /products/shoes?color=red&size=10#reviews
function ProductPage() {
const { category } = useParams(); // "shoes"
const [searchParams] = useSearchParams();
const location = useLocation();
const color = searchParams.get("color"); // "red"
const size = searchParams.get("size"); // "10"
const hash = location.hash; // "#reviews"
return (
<div>
<h1>{category}</h1>
<p>
Color: {color}, Size: {size}
</p>
{hash === "#reviews" && <Reviews />}
</div>
);
}See Also
- routing.md - Route configuration and dynamic segments
- navigation.md - Passing state with Link and navigate
- React Router Hooks Documentation
Related skills
How it compares
Pick react-router-declarative-mode for JSX-only SPA routing; choose data-router skills when loaders, actions, or server-driven routes are required.
FAQ
What React Router version does react-router-declarative-mode target?
react-router-declarative-mode targets React Router v6.4+ declarative mode with BrowserRouter, Routes, Route, Link, NavLink, and URL hooks. The skill explicitly avoids loaders, actions, and data-router APIs for simpler SPA routing.
Does react-router-declarative-mode cover React Router data loaders?
react-router-declarative-mode does not cover loaders or actions. The skill focuses on declarative JSX routing, Link and NavLink navigation, and URL param hooks for client-side SPAs without data loading features.