Modern Routing Navigation Implementation
1. React Router v6 Nested Routes
| Feature | API | Description | Benefit |
|---|---|---|---|
| BrowserRouter | <BrowserRouter> |
HTML5 history API for clean URLs without hash (#) | SEO-friendly, native browser navigation, back/forward support |
| Routes & Route | <Routes><Route path element /> |
Declarative route configuration with element-based rendering | Type-safe, composable, no render props needed |
| Nested Routes NEW | <Outlet /> |
Parent routes render child routes using Outlet component | Layout composition, shared UI, hierarchical routing |
| useNavigate | navigate('/path') |
Programmatic navigation hook replacing useHistory | Simpler API, TypeScript support, relative navigation |
| useParams & useSearchParams | useParams(), useSearchParams() |
Access URL parameters and query strings in components | Type-safe parameter extraction, URLSearchParams API |
| Index Routes | <Route index element /> |
Default child route when parent path matches exactly | Dashboard defaults, tab navigation, nested layouts |
Example: React Router v6 Nested Routes
import { BrowserRouter, Routes, Route, Outlet, Link, useParams } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
{/* Nested routes */}
<Route path="dashboard" element={<Dashboard />}>
<Route index element={<DashboardHome />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
{/* Dynamic segments */}
<Route path="users/:userId" element={<UserDetail />} />
{/* Catch-all */}
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
);
}
// Layout with Outlet
function Layout() {
return (
<div>
<nav>
<Link to="/">Home</Link>
<Link to="/dashboard">Dashboard</Link>
</nav>
<main>
<Outlet /> {/* Child routes render here */}
</main>
</div>
);
}
// Dashboard with nested routes
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<nav>
<Link to="/dashboard">Overview</Link>
<Link to="/dashboard/profile">Profile</Link>
<Link to="/dashboard/settings">Settings</Link>
</nav>
<Outlet /> {/* Nested child routes */}
</div>
);
}
// Access params
function UserDetail() {
const { userId } = useParams();
const [searchParams] = useSearchParams();
const tab = searchParams.get('tab') || 'overview';
return <div>User: {userId}, Tab: {tab}</div>;
}
// Programmatic navigation
function LoginForm() {
const navigate = useNavigate();
const handleSubmit = async (credentials) => {
await login(credentials);
navigate('/dashboard', { replace: true });
};
}
v6 Changes: Removed
<Switch> (use <Routes>), removed
render/component props (use element), removed useHistory (use useNavigate), relative
paths by default
2. Next.js App Router File-based
| Concept | File Convention | Description | Feature |
|---|---|---|---|
| App Router NEW | app/ directory |
File-system based routing with React Server Components support | Server components, streaming, layouts, loading states |
| page.js | app/blog/page.js |
Defines route segment UI, makes path publicly accessible | Route endpoints, unique per segment |
| layout.js | app/layout.js |
Shared UI that wraps child layouts and pages, preserves state | Persistent navigation, shared headers, nested layouts |
| loading.js | app/blog/loading.js |
Loading UI with Suspense, instant loading state | Automatic loading boundaries, streaming support |
| error.js | app/blog/error.js |
Error boundary UI, automatically wraps route segment | Granular error handling, error recovery |
| Dynamic Routes | [slug]/page.js |
Dynamic segments using square brackets, catch-all routes | Blog posts, product pages, user profiles |
| Route Groups | (group)/page.js |
Organize routes without affecting URL structure | Team organization, different layouts, logical grouping |
Example: Next.js App Router File Structure
app/
├── layout.js # Root layout (required)
├── page.js # Home page (/)
├── loading.js # Global loading UI
├── error.js # Global error boundary
├── not-found.js # 404 page
├── (marketing)/ # Route group (doesn't affect URL)
│ ├── about/
│ │ └── page.js # /about
│ └── contact/
│ └── page.js # /contact
├── dashboard/
│ ├── layout.js # Dashboard layout
│ ├── page.js # /dashboard
│ ├── loading.js # Dashboard loading
│ ├── error.js # Dashboard errors
│ └── [teamId]/
│ ├── page.js # /dashboard/[teamId]
│ └── settings/
│ └── page.js # /dashboard/[teamId]/settings
└── blog/
├── [...slug]/ # Catch-all: /blog/a, /blog/a/b, etc.
│ └── page.js
└── [[...slug]]/ # Optional catch-all: includes /blog
└── page.js
// app/layout.js - Root Layout (Required)
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<nav>Global Navigation</nav>
{children}
<footer>Global Footer</footer>
</body>
</html>
);
}
// app/dashboard/layout.js - Nested Layout
export default function DashboardLayout({ children }) {
return (
<div>
<aside>Dashboard Sidebar</aside>
<main>{children}</main>
</div>
);
}
// app/dashboard/[teamId]/page.js - Dynamic Route
export default function TeamPage({ params }) {
return <h1>Team: {params.teamId}</h1>;
}
// Generate static params at build time
export async function generateStaticParams() {
const teams = await fetchTeams();
return teams.map(team => ({ teamId: team.id }));
}
// app/dashboard/loading.js - Loading UI
export default function Loading() {
return <div>Loading dashboard...</div>;
}
// app/dashboard/error.js - Error Boundary
'use client';
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
Special Files
| File | Purpose |
|---|---|
layout.js |
Shared UI for segment |
page.js |
Route endpoint UI |
loading.js |
Loading UI with Suspense |
error.js |
Error boundary |
template.js |
Re-render on navigation |
not-found.js |
404 UI |
route.js |
API endpoint |
Navigation
// Link component
import Link from 'next/link';
<Link href="/dashboard">Dashboard</Link>
<Link href={`/blog/${slug}`}>Post</Link>
// Programmatic navigation
'use client';
import { useRouter } from 'next/navigation';
function Button() {
const router = useRouter();
return (
<button onClick={() => router.push('/dashboard')}>
Go to Dashboard
</button>
);
}
// Prefetch on hover (default)
<Link href="/slow-page" prefetch={true}>
Prefetched Link
</Link>
App Router Benefits
- Server Components: Zero-bundle components with direct DB access
- Streaming: Progressive rendering with Suspense boundaries
- Layouts: Persistent UI that doesn't re-render on navigation
- File Conventions: loading.js, error.js for automatic boundaries
3. Vue Router Composition API
| Feature | API | Description | Use Case |
|---|---|---|---|
| createRouter | createRouter({ routes }) |
Create router instance with history mode and route configuration | SPA routing, history management, route guards |
| useRouter & useRoute | useRouter(), useRoute() |
Composition API hooks for router instance and current route | Programmatic navigation, route information access |
| Nested Routes | children: [] |
Child routes with nested RouterView components | Complex layouts, multi-level navigation |
| Named Views | <RouterView name="sidebar" /> |
Multiple RouterView components in same route | Multi-panel layouts, sidebar + main content |
| Route Meta | meta: { auth: true } |
Attach custom metadata to routes for guards, breadcrumbs | Authentication, permissions, analytics tracking |
| Lazy Loading | component: () => import() |
Code-split route components with dynamic imports | Optimize initial bundle, load routes on-demand |
Example: Vue Router with Composition API
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
},
{
path: '/dashboard',
component: () => import('../layouts/Dashboard.vue'),
meta: { requiresAuth: true },
children: [
{
path: '',
name: 'DashboardHome',
component: () => import('../views/DashboardHome.vue')
},
{
path: 'profile',
name: 'Profile',
component: () => import('../views/Profile.vue')
},
{
path: 'settings',
name: 'Settings',
component: () => import('../views/Settings.vue'),
meta: { title: 'Settings' }
}
]
},
{
path: '/user/:id',
name: 'UserDetail',
component: () => import('../views/UserDetail.vue'),
props: true // Pass route params as props
}
];
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition;
return { top: 0 };
}
});
export default router;
// Component using Composition API
<script setup>
import { useRouter, useRoute } from 'vue-router';
import { computed } from 'vue';
const router = useRouter();
const route = useRoute();
// Access route params
const userId = computed(() => route.params.id);
const query = computed(() => route.query);
// Programmatic navigation
const navigateToDashboard = () => {
router.push({ name: 'Dashboard' });
};
const goBack = () => {
router.back();
};
// Navigation with params
const viewUser = (id) => {
router.push({
name: 'UserDetail',
params: { id },
query: { tab: 'profile' }
});
};
</script>
<template>
<div>
<RouterLink to="/dashboard">Dashboard</RouterLink>
<RouterLink :to="{ name: 'Profile' }">Profile</RouterLink>
<RouterView />
</div>
</template>
4. Code Splitting Route-based Lazy Loading
| Strategy | Implementation | Description | Benefit |
|---|---|---|---|
| Route-based Splitting | React.lazy(() => import()) |
Split bundles at route boundaries, load on navigation | Smaller initial bundle, faster TTI, pay-as-you-go loading |
| Suspense Boundary | <Suspense fallback> |
Show loading state while lazy component loads | Better UX, graceful loading, error boundaries |
| Prefetching | <link rel="prefetch"> |
Load route bundle during browser idle time | Instant navigation, zero loading delay |
| Preloading | <link rel="preload"> |
High-priority loading for critical next routes | Faster subsequent navigation, predictive loading |
| Magic Comments | /* webpackChunkName */ |
Name chunks for better debugging and caching | Named bundles, cache optimization, debugging |
Example: Route-based Code Splitting
// React Router with lazy loading
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Lazy load route components
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import(
/* webpackChunkName: "dashboard" */
/* webpackPrefetch: true */
'./pages/Dashboard'
));
const Profile = lazy(() => import('./pages/Profile'));
const Settings = lazy(() => import('./pages/Settings'));
// Loading component
function LoadingSpinner() {
return <div className="spinner">Loading...</div>;
}
function App() {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
// Next.js automatic code splitting
// pages/dashboard.js - automatically code-split
export default function Dashboard() {
return <div>Dashboard</div>;
}
// Vue Router lazy loading
const routes = [
{
path: '/dashboard',
component: () => import(
/* webpackChunkName: "dashboard" */
'./views/Dashboard.vue'
)
}
];
// Prefetch on hover
import { useEffect } from 'react';
function NavLink({ to, children }) {
const prefetchRoute = () => {
// Prefetch route component
import(/* webpackPrefetch: true */ `./pages/${to}`);
};
return (
<a
href={to}
onMouseEnter={prefetchRoute}
onFocus={prefetchRoute}
>
{children}
</a>
);
}
5. Dynamic Import Module Loading
| Technique | Syntax | Description | Use Case |
|---|---|---|---|
| Dynamic Import | import('module') |
Promise-based module loading at runtime, creates separate chunk | Conditional loading, on-demand features, reduce initial bundle |
| Conditional Loading | if (condition) import() |
Load modules based on runtime conditions (feature flags, user role) | A/B testing, premium features, device-specific code |
| Import on Interaction | onClick={() => import()} |
Lazy load when user interacts with UI element | Modals, tooltips, rich editors, heavy components |
| Import on Visibility | IntersectionObserver |
Load module when element enters viewport | Below-fold content, infinite scroll, image galleries |
| Named Exports | import().then(m => m.fn) |
Access specific exports from dynamically imported module | Tree-shaking with dynamic imports, selective loading |
Example: Dynamic Import Patterns
// Basic dynamic import
button.addEventListener('click', async () => {
const module = await import('./heavyModule.js');
module.initialize();
});
// React component lazy loading
function Editor() {
const [EditorComponent, setEditorComponent] = useState(null);
const loadEditor = async () => {
const { default: Editor } = await import('react-quill');
setEditorComponent(() => Editor);
};
useEffect(() => {
loadEditor();
}, []);
if (!EditorComponent) return <div>Loading editor...</div>;
return <EditorComponent />;
}
// Conditional loading based on feature flag
async function loadFeature() {
if (featureFlags.newDashboard) {
const { NewDashboard } = await import('./features/NewDashboard');
return NewDashboard;
} else {
const { OldDashboard } = await import('./features/OldDashboard');
return OldDashboard;
}
}
// Load on viewport intersection
function LazyImage({ src }) {
const [imageSrc, setImageSrc] = useState(null);
const imgRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
async ([entry]) => {
if (entry.isIntersecting) {
// Load image processing library when needed
const { processImage } = await import('./imageUtils');
const processed = await processImage(src);
setImageSrc(processed);
observer.disconnect();
}
},
{ threshold: 0.1 }
);
if (imgRef.current) observer.observe(imgRef.current);
return () => observer.disconnect();
}, [src]);
return <img ref={imgRef} src={imageSrc} />;
}
// Named export extraction
const loadChart = async (type) => {
const chartModule = await import('chart.js');
switch (type) {
case 'bar':
return new chartModule.BarChart();
case 'line':
return new chartModule.LineChart();
case 'pie':
return new chartModule.PieChart();
}
};
// Webpack magic comments for optimization
const Dashboard = () => import(
/* webpackChunkName: "dashboard" */
/* webpackPreload: true */
/* webpackExports: ["default", "config"] */
'./Dashboard'
);
6. Navigation Guards Authentication Middleware
| Guard Type | Implementation | Description | Use Case |
|---|---|---|---|
| Global Guards | router.beforeEach() |
Run before every route navigation, app-wide checks | Authentication, analytics, scroll restoration |
| Route Guards | beforeEnter |
Per-route validation before entering specific route | Permission checks, data prefetching, redirects |
| Component Guards | beforeRouteEnter |
Inside component, access to component instance | Component-level validation, cleanup |
| Protected Routes | meta: { requiresAuth } |
Route metadata for conditional access control | Private pages, role-based access, subscription gates |
| Middleware Pattern | middleware.js |
Composable middleware functions executed in sequence | Complex auth flows, multi-step validation |
Vue Router Guards
// Global guard
router.beforeEach(async (to, from, next) => {
// Check authentication
if (to.meta.requiresAuth) {
const isAuthenticated = await checkAuth();
if (!isAuthenticated) {
return next({ name: 'Login', query: { redirect: to.fullPath } });
}
}
// Check permissions
if (to.meta.roles) {
const userRole = getUserRole();
if (!to.meta.roles.includes(userRole)) {
return next({ name: 'Forbidden' });
}
}
next();
});
// Route-level guard
const routes = [
{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => {
if (isAdmin()) {
next();
} else {
next('/unauthorized');
}
}
}
];
// Component guard
export default {
beforeRouteEnter(to, from, next) {
// Can't access `this` yet
fetchUserData(to.params.id).then(user => {
next(vm => {
vm.user = user;
});
});
},
beforeRouteLeave(to, from, next) {
if (this.hasUnsavedChanges) {
const confirm = window.confirm('Discard changes?');
next(confirm);
} else {
next();
}
}
};
React Router Guards
// Protected Route wrapper
function ProtectedRoute({ children }) {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
// Role-based guard
function RoleGuard({ roles, children }) {
const { user } = useAuth();
if (!roles.includes(user.role)) {
return <Navigate to="/unauthorized" replace />;
}
return children;
}
// Usage
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />
<Route path="/admin" element={
<ProtectedRoute>
<RoleGuard roles={['admin']}>
<Admin />
</RoleGuard>
</ProtectedRoute>
} />
</Routes>
// Hook-based guard
function useAuthGuard() {
const { user } = useAuth();
const navigate = useNavigate();
useEffect(() => {
if (!user) {
navigate('/login');
}
}, [user, navigate]);
return user;
}
Example: Next.js Middleware
// middleware.js (Next.js 13+)
import { NextResponse } from 'next/server';
export function middleware(request) {
const token = request.cookies.get('session')?.value;
const { pathname } = request.nextUrl;
// Protected routes
if (pathname.startsWith('/dashboard')) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Verify token
const user = verifyToken(token);
if (!user) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
// Admin routes
if (pathname.startsWith('/admin')) {
const user = verifyToken(token);
if (user?.role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
}
// Add custom headers
const response = NextResponse.next();
response.headers.set('x-middleware-cache', 'no-cache');
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*']
};
Guard Best Practices
- Server-side validation: Always verify on server, guards can be bypassed
- Token refresh: Handle expired tokens, automatic refresh
- Loading states: Show loading during async guard checks
- Redirect chains: Avoid infinite redirects, track navigation history
- Error handling: Graceful degradation on guard failures
Security Note: Client-side guards are for UX only. Always
enforce authentication and authorization on the server. Guards prevent navigation, not API access.