---
name: sdlc-react-routing
description: |
Routing libraries for React SPAs: React Router v6/v7 (most common), TanStack Router (typed, modern), wouter (minimal). Detect what's installed and apply matching patterns. Lazy loading, navigation guards, typed params.
Use this skill to:
- Configure routes (declarative or file-based).
- Use navigation hooks (useNavigate, useParams, useSearchParams).
- Lazy-load routes for code splitting.
- Implement protected routes / auth guards.
- Type-safe params via Zod or framework's built-ins.
Do NOT use this skill for:
- General React conventions (see react-conventions).
- State management (see react-state-management).
- Form handling (see react-forms).
- Next.js routing (different model — see nextjs-plugin).
paths: ["src/**/*.tsx", "src/**/*.jsx"]
---
# React Routing Patterns
Choose the routing library based on what's already in `package.json`. Don't introduce a new one without BA approval.
## Detection
| Marker (in dependencies) | Library |
|---|---|
| `react-router-dom` | React Router v6 (≤6.x) or v7 |
| `@tanstack/react-router` | TanStack Router |
| `wouter` | wouter (minimal alternative) |
| (none) | Single-page; introduce only if BA spec adds multi-page |
## React Router v6 / v7
The de-facto standard. v7 unified `react-router-dom` and the framework features; for SPA usage the API is similar.
### Declarative routes (most common)
```tsx
// src/main.tsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import App from './App';
import UsersList from './pages/UsersList';
import UserDetail from './pages/UserDetail';
import Login from './pages/Login';
import NotFound from './pages/NotFound';
const router = createBrowserRouter([
{
path: '/',
element: ,
errorElement: ,
children: [
{ index: true, element: },
{ path: 'users', element: },
{ path: 'users/:id', element: },
{ path: 'login', element: },
],
},
]);
ReactDOM.createRoot(document.getElementById('root')!).render(
);
```
`` is the layout; `` inside renders the matched child route:
```tsx
// src/App.tsx
import { Outlet } from 'react-router-dom';
export default function App() {
return (
);
}
```
### Navigation hooks
```tsx
import { Link, NavLink, useNavigate, useParams, useSearchParams, useLocation } from 'react-router-dom';
// Declarative — Link or NavLink
Users
isActive ? 'active' : ''}>Users
// Programmatic
const navigate = useNavigate();
navigate('/users'); // push
navigate('/users', { replace: true }); // replace
navigate(-1); // back
// Params from dynamic segments
const { id } = useParams<{ id: string }>();
// Search params (query string)
const [searchParams, setSearchParams] = useSearchParams();
const filter = searchParams.get('filter') ?? 'all';
setSearchParams({ filter: 'active' });
// Current location
const location = useLocation();
console.log(location.pathname, location.search, location.hash);
```
### Loaders and actions (data router APIs)
In React Router v6.4+ / v7, route loaders fetch data before render:
```tsx
// src/routes/users.tsx
import { LoaderFunction, useLoaderData } from 'react-router-dom';
export const loader: LoaderFunction = async () => {
const res = await fetch('/api/users');
if (!res.ok) throw new Response('Failed', { status: 500 });
return res.json();
};
export function UsersList() {
const users = useLoaderData() as User[];
return {users.map((u) => - {u.name}
)}
;
}
```
Wire in route config:
```ts
{ path: 'users', element: , loader: usersLoader }
```
For mutations:
```tsx
import { ActionFunction, redirect, useActionData } from 'react-router-dom';
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
// ... validate, save
return redirect('/users');
};
```
Loaders/actions are an alternative to TanStack Query — pick one per project. For most projects with TanStack Query, skip loaders; use `useQuery` inside the route component.
### Lazy-loaded routes
```tsx
import { lazy } from 'react';
const UsersList = lazy(() => import('./pages/UsersList'));
// In route config
{
path: 'users',
element: (
}>
),
}
```
For file-based code splitting, route config can use lazy loader:
```ts
{ path: 'users', lazy: () => import('./routes/users') }
```
The imported module exports `{ Component, loader, action }`.
### Protected routes (auth guards)
```tsx
// src/routes/RequireAuth.tsx
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
export function RequireAuth() {
const { user } = useAuth();
const location = useLocation();
if (!user) return ;
return ;
}
```
Wrap protected routes:
```ts
{
element: ,
children: [
{ path: 'dashboard', element: },
{ path: 'settings', element: },
],
}
```
### Type-safe params with Zod
```tsx
import { useParams } from 'react-router-dom';
import { z } from 'zod';
const ParamsSchema = z.object({ id: z.string().uuid() });
function UserDetail() {
const params = useParams();
const parsed = ParamsSchema.safeParse(params);
if (!parsed.success) throw new Error('Invalid params');
const { id } = parsed.data; // string (UUID)
// ...
}
```
For library-level type safety, see TanStack Router below.
## TanStack Router
Typed, modern. Code-based or file-based routing. Built-in search-param parsing and validation.
### File-based setup
```ts
// src/routeTree.gen.ts (generated by @tanstack/router-plugin)
// src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router';
export const Route = createRootRoute({
component: () => (
),
});
// src/routes/users.tsx
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/users')({
component: UsersPage,
loader: () => fetchUsers(),
});
function UsersPage() {
const users = Route.useLoaderData();
return {users.map((u) => - {u.name}
)}
;
}
// src/routes/users.$id.tsx — dynamic segment
export const Route = createFileRoute('/users/$id')({
parseParams: (params) => ({ id: z.string().uuid().parse(params.id) }),
component: UserDetail,
});
```
`Route.useParams()` returns the parsed (typed) params — no runtime check needed at the consumer.
### Search params validation
```ts
export const Route = createFileRoute('/users')({
validateSearch: z.object({
filter: z.enum(['all', 'active', 'archived']).default('all'),
page: z.number().int().min(1).default(1),
}),
component: UsersPage,
});
// In component
const { filter, page } = Route.useSearch();
```
Type-safe, validated search params at the route level.
### Navigation
```tsx
import { Link, useNavigate } from '@tanstack/react-router';
Active users;
const navigate = useNavigate();
navigate({ to: '/users/$id', params: { id: '123' }, search: { tab: 'overview' } });
```
Navigation is fully type-checked — wrong path or missing param is a compile error.
## wouter
Minimal alternative (`pnpm add wouter`). ~3 KB. Pattern-based.
```tsx
import { Route, Switch, Link, useLocation } from 'wouter';
function App() {
return (
{(params) => }
);
}
const [location, setLocation] = useLocation();
setLocation('/users');
```
Good for very small apps. Misses nested routes with shared layouts (build them via composition).
## Code splitting strategy
Routes are the natural boundary. Don't over-split:
- Each top-level route → split.
- Heavy modal/wizard inside a route → split.
- Small page → don't bother.
For React Router:
```ts
{ path: 'users', lazy: () => import('./routes/users') }
```
For TanStack Router with file-based routes — splitting is built into the plugin.
## Layout patterns
Layouts compose via parent routes:
```ts
// React Router
{
element: ,
path: '/dashboard',
children: [
{ index: true, element: },
{ path: 'analytics', element: },
{ path: 'settings', element: },
],
}
```
`` renders `` for children. Persists across navigation within the layout.
## Anti-patterns
- ❌ Mixing `` with `` — full page reload defeats SPA benefits.
- ❌ Storing data already in URL params in component state too — keep ONE source of truth.
- ❌ Reading `window.location.search` directly — use `useSearchParams` or framework hook.
- ❌ Programmatic navigation deep in business logic — pass a callback or use a hook at the page level.
- ❌ Loaders that fetch data already cached by TanStack Query — pick one strategy per project.
- ❌ Forgetting to wrap lazy-loaded routes in `` — error boundary catches the thrown promise oddly.
- ❌ `useNavigate` inside a `useEffect` for redirects — use loaders or `` instead, or run on event handlers.
- ❌ Long route trees in one file — split by feature.