This lesson assembles the complete blog application router — all the public and protected routes, a root layout that persists across navigation, scroll restoration, a 404 page, and the navigation patterns covered throughout the chapter. The result is a router that matches the blog application’s full feature set: browsing posts, reading a post detail, filtering by tags, creating and editing posts in the dashboard, authentication, and profile management.
Complete App Router
// src/App.jsx — complete blog application router
import { Routes, Route, useLocation } from "react-router-dom";
import { useEffect } from "react";
import Layout from "@/components/layout/Layout";
import ProtectedRoute,
{ PublicOnlyRoute } from "@/components/auth/ProtectedRoute";
// Pages — lazy imported for code splitting
import { lazy, Suspense } from "react";
const HomePage = lazy(() => import("@/pages/HomePage"));
const PostDetailPage = lazy(() => import("@/pages/PostDetailPage"));
const TagPage = lazy(() => import("@/pages/TagPage"));
const LoginPage = lazy(() => import("@/pages/LoginPage"));
const RegisterPage = lazy(() => import("@/pages/RegisterPage"));
const DashboardPage = lazy(() => import("@/pages/DashboardPage"));
const PostEditorPage = lazy(() => import("@/pages/PostEditorPage"));
const ProfilePage = lazy(() => import("@/pages/ProfilePage"));
const NotFoundPage = lazy(() => import("@/pages/NotFoundPage"));
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => { window.scrollTo(0, 0); }, [pathname]);
return null;
}
function PageLoader() {
return (
<div className="flex justify-center items-center h-64">
<div className="animate-spin h-8 w-8 border-2 border-blue-500 rounded-full border-t-transparent" />
</div>
);
}
export default function App() {
return (
<>
<ScrollToTop />
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Layout />}>
{/* ── Public routes ────────────────────────────────── */}
<Route index element={<HomePage />} />
<Route path="posts/:postId" element={<PostDetailPage />} />
<Route path="posts/by-slug/:slug" element={<PostDetailPage bySlug />} />
<Route path="tags/:tagSlug" element={<TagPage />} />
{/* ── Auth: redirect logged-in users away ──────────── */}
<Route element={<PublicOnlyRoute />}>
<Route path="login" element={<LoginPage />} />
<Route path="register" element={<RegisterPage />} />
</Route>
{/* ── Protected: require login ─────────────────────── */}
<Route element={<ProtectedRoute />}>
<Route path="dashboard" element={<DashboardPage />} />
<Route path="posts/new" element={<PostEditorPage />} />
<Route path="posts/:postId/edit" element={<PostEditorPage />} />
<Route path="profile" element={<ProfilePage />} />
</Route>
{/* ── 404 ──────────────────────────────────────────── */}
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</Suspense>
</>
);
}
Note:
React.lazy() with Suspense enables code splitting — each page is a separate JavaScript chunk that is only downloaded when the user navigates to it. The initial bundle is smaller, making the first page load faster. The Suspense fallback shows the loading spinner while the chunk downloads. Vite handles the code splitting automatically when you use import() with lazy — no extra configuration needed.Tip: The route path
"posts/by-slug/:slug" must be defined before "posts/:postId" in the route tree, or React Router may match "by-slug" as the postId parameter. In React Router v6’s specificity algorithm, static segments take priority over dynamic ones, so posts/by-slug/:slug correctly wins over posts/:postId when the URL starts with /posts/by-slug/. But to be safe and explicit, order more specific routes before more general ones.Warning: When using lazy imports and Suspense, errors in the lazy-loaded component (e.g., a syntax error in PostEditorPage.jsx) will cause the Suspense boundary to enter an error state, showing nothing or crashing silently. Wrap your Suspense with an Error Boundary component to show a friendly error message when a lazy chunk fails to load (network error, code error). React’s built-in error boundary uses a class component — use a library like
react-error-boundary for a clean hook-based API.404 Not Found Page
// src/pages/NotFoundPage.jsx
import { Link, useLocation } from "react-router-dom";
export default function NotFoundPage() {
const { pathname } = useLocation();
return (
<div className="text-center py-24">
<h1 className="text-6xl font-bold text-gray-200 mb-4">404</h1>
<p className="text-xl text-gray-500 mb-2">Page not found</p>
<p className="text-sm text-gray-400 mb-8">
No match for <code>{pathname}</code>
</p>
<Link
to="/"
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
>
Go Home
</Link>
</div>
);
}
Complete Route Map
| Path | Page | Auth Required |
|---|---|---|
/ |
HomePage — post feed with filters | No |
/posts/:postId |
PostDetailPage — full post view | No |
/posts/by-slug/:slug |
PostDetailPage — by slug | No |
/tags/:tagSlug |
TagPage — posts by tag | No |
/login |
LoginPage (public only) | No (redirect if logged in) |
/register |
RegisterPage (public only) | No (redirect if logged in) |
/dashboard |
DashboardPage — user’s posts | Yes |
/posts/new |
PostEditorPage — create post | Yes |
/posts/:id/edit |
PostEditorPage — edit post | Yes |
/profile |
ProfilePage — settings | Yes |
* |
NotFoundPage | No |