Routing Fundamentals
Routing connects URLs to components, enabling multi-page experiences in single-page applications.Declarative Routes
Define routes as components for clear structure
Dynamic Segments
URL parameters for dynamic content
Nested Routes
Hierarchical routing with shared layouts
Navigation Guards
Protect routes with authentication checks
Route Configuration
- React Router
- Next.js App Router
- TanStack Router
React Router Setup
The most popular routing solution for React applications.import { createBrowserRouter, RouterProvider } from 'react-router-dom';
// Define routes configuration
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{
index: true,
element: <HomePage />,
},
{
path: 'about',
element: <AboutPage />,
},
{
path: 'products',
element: <ProductsLayout />,
children: [
{
index: true,
element: <ProductsList />,
},
{
path: ':productId',
element: <ProductDetail />,
loader: productLoader,
},
],
},
{
path: 'dashboard',
element: <ProtectedRoute><Dashboard /></ProtectedRoute>,
},
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
React Router v6+ uses data routers with loaders and actions for better data fetching patterns.
Next.js File-Based Routing
Next.js uses the file system for routing with special file conventions.app/
├── page.tsx # / route
├── about/
│ └── page.tsx # /about route
├── products/
│ ├── page.tsx # /products route
│ ├── [id]/
│ │ └── page.tsx # /products/:id route
│ └── layout.tsx # Shared layout
├── dashboard/
│ ├── layout.tsx # Dashboard layout
│ ├── page.tsx # /dashboard route
│ ├── settings/
│ │ └── page.tsx # /dashboard/settings
│ └── profile/
│ └── page.tsx # /dashboard/profile
└── layout.tsx # Root layout
// app/products/[id]/page.tsx
interface PageProps {
params: { id: string };
searchParams: { [key: string]: string | string[] | undefined };
}
export default async function ProductPage({ params }: PageProps) {
const product = await fetchProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
</div>
);
}
Next.js App Router provides server components by default, improving initial load performance.
TanStack Router
Type-safe routing with built-in data loading and route params validation.import { createRouter, createRoute, createRootRoute } from '@tanstack/react-router';
// Root route
const rootRoute = createRootRoute({
component: RootLayout,
});
// Index route
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: HomePage,
});
// Dynamic route with loader
const productRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/products/$productId',
loader: async ({ params }) => {
const product = await fetchProduct(params.productId);
return { product };
},
component: ProductDetail,
});
// Create router
const routeTree = rootRoute.addChildren([
indexRoute,
productRoute,
]);
const router = createRouter({ routeTree });
// Type-safe navigation
function MyComponent() {
const navigate = useNavigate();
const goToProduct = (id: string) => {
navigate({
to: '/products/$productId',
params: { productId: id }, // Fully typed!
});
};
}
Navigation Patterns
Programmatic Navigation
import { useNavigate, useLocation } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
const location = useLocation();
const handleSubmit = async (credentials: Credentials) => {
try {
await login(credentials);
// Redirect to the page user was trying to access
const from = location.state?.from?.pathname || '/dashboard';
navigate(from, { replace: true });
} catch (error) {
toast.error('Login failed');
}
};
return <form onSubmit={handleSubmit}>{/* form fields */}</form>;
}
Link Components
import { Link, NavLink } from 'react-router-dom';
// Basic link
<Link to="/about">About Us</Link>
// Link with state
<Link
to="/products/123"
state={{ from: 'search-results' }}
>
View Product
</Link>
// NavLink with active styling
<NavLink
to="/dashboard"
className={({ isActive }) =>
isActive ? 'nav-link active' : 'nav-link'
}
>
Dashboard
</NavLink>
// Relative navigation
<Link to="..">Back to list</Link>
<Link to="./edit">Edit</Link>
Use
<Link> for navigation instead of <a> tags to avoid full page reloads in SPAs.Search Params and Filters
import { useSearchParams } from 'react-router-dom';
function ProductsList() {
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get('category') || 'all';
const sort = searchParams.get('sort') || 'name';
const page = parseInt(searchParams.get('page') || '1', 10);
const updateFilter = (key: string, value: string) => {
setSearchParams(prev => {
const next = new URLSearchParams(prev);
next.set(key, value);
next.set('page', '1'); // Reset page on filter change
return next;
});
};
const clearFilters = () => {
setSearchParams({});
};
return (
<div>
<select
value={category}
onChange={(e) => updateFilter('category', e.target.value)}
>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<select
value={sort}
onChange={(e) => updateFilter('sort', e.target.value)}
>
<option value="name">Name</option>
<option value="price">Price</option>
<option value="date">Date Added</option>
</select>
<button onClick={clearFilters}>Clear Filters</button>
<ProductGrid category={category} sort={sort} page={page} />
</div>
);
}
Dynamic Routes
Route Parameters
import { useParams, useLoaderData } from 'react-router-dom';
// Route configuration
{
path: '/blog/:slug',
element: <BlogPost />,
loader: async ({ params }) => {
return fetchBlogPost(params.slug);
},
}
// Component using params
function BlogPost() {
const { slug } = useParams<{ slug: string }>();
const post = useLoaderData() as BlogPost;
return (
<article>
<h1>{post.title}</h1>
<p className="meta">Posted on {post.date}</p>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Optional Parameters
// Route with optional segment
{
path: '/shop/:category?',
element: <Shop />,
}
function Shop() {
const { category } = useParams<{ category?: string }>();
// Handle both /shop and /shop/electronics
const products = useProducts(category || 'all');
return <ProductGrid products={products} />;
}
Wildcard Routes
// Catch-all route for documentation
{
path: '/docs/*',
element: <Documentation />,
}
function Documentation() {
const location = useLocation();
// location.pathname: "/docs/getting-started/installation"
const path = location.pathname.replace('/docs/', '');
const content = useDocContent(path);
return <MarkdownRenderer content={content} />;
}
Route Guards & Protection
Authentication Guard
import { Navigate, useLocation } from 'react-router-dom';
function ProtectedRoute({ children }: { children: ReactNode }) {
const { user, isLoading } = useAuth();
const location = useLocation();
if (isLoading) {
return <LoadingSpinner />;
}
if (!user) {
// Redirect to login, save attempted location
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
}
// Usage
{
path: '/dashboard',
element: (
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
),
}
Role-Based Access
interface RoleGuardProps {
children: ReactNode;
requiredRoles: string[];
fallback?: ReactNode;
}
function RoleGuard({ children, requiredRoles, fallback }: RoleGuardProps) {
const { user } = useAuth();
const hasRequiredRole = requiredRoles.some(role =>
user?.roles.includes(role)
);
if (!hasRequiredRole) {
return fallback || <Navigate to="/unauthorized" replace />;
}
return <>{children}</>;
}
// Usage
<RoleGuard requiredRoles={['admin', 'editor']}>
<AdminPanel />
</RoleGuard>
Navigation Confirmation
import { useBlocker } from 'react-router-dom';
function FormWithConfirmation() {
const [formData, setFormData] = useState({});
const [isDirty, setIsDirty] = useState(false);
// Block navigation if form has unsaved changes
const blocker = useBlocker(
({ currentLocation, nextLocation }) =>
isDirty && currentLocation.pathname !== nextLocation.pathname
);
useEffect(() => {
if (blocker.state === 'blocked') {
const confirm = window.confirm(
'You have unsaved changes. Are you sure you want to leave?'
);
if (confirm) {
blocker.proceed();
} else {
blocker.reset();
}
}
}, [blocker]);
return (
<form onChange={() => setIsDirty(true)}>
{/* form fields */}
</form>
);
}
Always save navigation blockers for scenarios where losing data would be problematic, like unsaved forms.
Nested Routes & Layouts
Shared Layouts
import { Outlet } from 'react-router-dom';
// Dashboard layout with sidebar
function DashboardLayout() {
return (
<div className="dashboard">
<DashboardSidebar />
<main className="dashboard-content">
{/* Child routes render here */}
<Outlet />
</main>
</div>
);
}
// Route configuration
{
path: '/dashboard',
element: <DashboardLayout />,
children: [
{ index: true, element: <DashboardHome /> },
{ path: 'analytics', element: <Analytics /> },
{ path: 'settings', element: <Settings /> },
{
path: 'team',
element: <TeamLayout />,
children: [
{ index: true, element: <TeamList /> },
{ path: ':teamId', element: <TeamDetail /> },
],
},
],
}
Outlet Context
interface DashboardContextType {
isSidebarOpen: boolean;
toggleSidebar: () => void;
}
function DashboardLayout() {
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const context: DashboardContextType = {
isSidebarOpen,
toggleSidebar: () => setIsSidebarOpen(prev => !prev),
};
return (
<div className="dashboard">
<DashboardSidebar isOpen={isSidebarOpen} />
<Outlet context={context} />
</div>
);
}
// Child route accessing context
function Analytics() {
const { isSidebarOpen, toggleSidebar } = useOutletContext<DashboardContextType>();
return (
<div>
<button onClick={toggleSidebar}>Toggle Sidebar</button>
{/* Analytics content */}
</div>
);
}
Loading States & Error Handling
Route Loaders
// Data loader for route
async function productLoader({ params }: LoaderFunctionArgs) {
const product = await fetchProduct(params.productId);
if (!product) {
throw new Response('Not Found', { status: 404 });
}
return { product };
}
// Route configuration
{
path: '/products/:productId',
element: <ProductDetail />,
loader: productLoader,
errorElement: <ProductError />,
}
// Component using loaded data
function ProductDetail() {
const { product } = useLoaderData() as { product: Product };
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
</div>
);
}
Error Boundaries
import { useRouteError, isRouteErrorResponse } from 'react-router-dom';
function ErrorPage() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div className="error-page">
<h1>{error.status}</h1>
<p>{error.statusText}</p>
{error.data?.message && <p>{error.data.message}</p>}
</div>
);
}
return (
<div className="error-page">
<h1>Oops!</h1>
<p>Sorry, an unexpected error occurred.</p>
</div>
);
}
Best Practices
1. Use Route-Based Code Splitting
1. Use Route-Based Code Splitting
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const router = createBrowserRouter([
{
path: '/dashboard',
element: (
<Suspense fallback={<LoadingSpinner />}>
<Dashboard />
</Suspense>
),
},
]);
2. Centralize Route Paths
2. Centralize Route Paths
// routes.ts
export const ROUTES = {
HOME: '/',
PRODUCTS: '/products',
PRODUCT_DETAIL: (id: string) => `/products/${id}`,
DASHBOARD: '/dashboard',
SETTINGS: '/dashboard/settings',
} as const;
// Usage
<Link to={ROUTES.PRODUCT_DETAIL('123')}>View Product</Link>
3. Preserve Scroll Position
3. Preserve Scroll Position
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
// Add to app root
<Router>
<ScrollToTop />
<Routes>{/* routes */}</Routes>
</Router>
4. Handle 404s Gracefully
4. Handle 404s Gracefully
{
path: '*',
element: <NotFoundPage />,
}
function NotFoundPage() {
return (
<div className="not-found">
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<Link to="/">Go Home</Link>
</div>
);
}
Good routing makes navigation intuitive and URLs shareable. Invest time in getting your routing structure right early.