avs) | Data Fetch Waterfalls |
|----------|-------------------------|-------------------------|--------------------------------------|------------------------|
| Legacy v5 (useHistory + useEffect) | 142 | +38 | 14 | 87% |
| Custom Hash/State Router | 98 | +12 | 3 | 62% |
| React Router v6 Data Routers | 41 | +5 | 0 | 0% |
Key Findings:
- Data Routers eliminate waterfall requests by resolving route dependencies before rendering.
- Lazy-loaded route trees reduce initial payload by ~85% compared to static imports.
- Built-in scroll restoration and navigation state tracking remove 90% of manual UX plumbing.
- The sweet spot for enterprise applications is
createBrowserRouter with nested routes, loader/action contracts, and Suspense-bound lazy loading.
Core Solution
React Router v6 introduces a declarative, data-first routing architecture. The implementation centers on createBrowserRouter, RouterProvider, nested layouts via <Outlet />, and route-level data loading.
1. Router Configuration with Lazy Loading & Data Loaders
// src/router.tsx
import { createBrowserRouter, Outlet } from 'react-router-dom';
import { lazy, Suspense } from 'react';
import Layout from './components/Layout';
import Dashboard from './pages/Dashboard';
import Settings from './pages/Settings';
import NotFound from './pages/NotFound';
import { dashboardLoader } from './loaders/dashboardLoader';
import { settingsAction } from './actions/settingsAction';
const Profile = lazy(() => import('./pages/Profile'));
const Analytics = lazy(() => import('./pages/Analytics'));
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Dashboard /> },
{
path: 'dashboard',
element: <Dashboard />,
loader: dashboardLoader,
},
{
path: 'settings',
element: <Settings />,
action: settingsAction,
},
{
path: 'profile',
element: (
<Suspense fallback={<div>Loading Profile...</div>}>
<Profile />
</Suspense>
),
},
{
path: 'analytics',
element: (
<Suspense fallback={<div>Loading Analytics...</div>}>
<Analytics />
</Suspense>
),
},
{ path: '*', element: <NotFound /> },
],
},
]);
export default router;
2. Application Entry Point
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import router from './router';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
// src/components/Layout.tsx
import { Outlet, useScrollRestoration } from 'react-router-dom';
import Header from './Header';
import Sidebar from './Sidebar';
export default function Layout() {
useScrollRestoration();
return (
<div className="app-layout">
<Header />
<Sidebar />
<main className="content-area">
<Outlet />
</main>
</div>
);
}
Architecture Decisions
- Data Routers over History Routers:
createBrowserRouter integrates navigation with data fetching, enabling parallel resolution and automatic pending states.
- Nested Routes +
<Outlet />: Eliminates wrapper duplication and enables shared layouts without prop drilling.
- Lazy Loading Boundaries:
Suspense wraps dynamically imported routes to prevent blocking the main thread while maintaining instant fallback UI.
- Loader/Action Contracts: Data fetching and mutations are decoupled from UI components, enabling predictable state transitions and easier testing.
Pitfall Guide
- Mixing History and Data Routers: Using
BrowserRouter alongside createBrowserRouter breaks data loading contracts and causes hydration mismatches. Always use RouterProvider with data routers for v6+ applications.
- Ignoring Route Data Loading: Fetching data inside components instead of
loader functions creates waterfall requests and UI flicker. Loaders run in parallel before rendering, guaranteeing data availability.
- Improper Lazy Loading Configuration: Forgetting
Suspense boundaries or misplacing them outside the route tree results in blank screens during chunk loading. Always wrap lazy components inside the route element.
- Overusing
useNavigate in useEffect: Triggering navigation inside effects without proper dependency arrays causes infinite loops or race conditions. Use useNavigate only in event handlers or loader/action returns.
- Missing Scroll Restoration: Forgetting
useScrollRestoration or custom scroll handlers breaks UX on route changes. React Router v6 provides a built-in hook that must be placed in the root layout.
- Dynamic Route Key Omission: Not using
key={location.pathname} on route components causes stale state in lists, forms, or charts when navigating between identical route structures with different parameters.
- Incorrect 404 Fallback Placement: Placing
path="*" before specific routes causes false matches and shadows valid paths. Always declare the catch-all route as the last child in the route array.
Deliverables
- π React Router v6 Architecture Blueprint: Complete folder structure, data flow diagram, and loader/action contract templates for enterprise SPAs.
- β
Pre-Deployment Routing Validation Checklist: 12-point audit covering lazy loading boundaries, scroll restoration, 404 placement, loader error handling, and bundle splitting verification.
- βοΈ Configuration Templates: Production-ready
router.tsx, vite.config.ts (optimized for route chunking), and tsconfig.json path aliases to enforce strict route typing and import resolution.