The React frontend is where users experience the MERN Blog โ the post feed they scroll through, the editor they write in, the comment section where conversations happen. Building it well means applying every frontend principle from Chapters 14โ21: AuthContext for shared auth state, React Router for navigation with protected routes, custom hooks for data fetching and socket connections, controlled form components with validation, and a responsive layout that works across screen sizes. In this lesson you will assemble the complete React frontend, connecting each component to the Express API built in Lesson 2.
Application Architecture โ React
// src/main.jsx โ provider composition at the root
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import AppProviders from '@/context/AppProviders';
import App from './App';
import './index.css';
createRoot(document.getElementById('root')).render(
<BrowserRouter>
<AppProviders> {/* ThemeProvider โ AuthProvider โ NotificationProvider */}
<App />
</AppProviders>
</BrowserRouter>
);
// src/App.jsx โ route map
import { Routes, Route } from 'react-router-dom';
import PageLayout from '@/components/layout/PageLayout';
import ProtectedRoute from '@/components/auth/ProtectedRoute';
// ... page imports ...
export default function App() {
return (
<PageLayout>
<Routes>
{/* Public */}
<Route path="/" element={<HomePage />} />
<Route path="/posts/:id" element={<PostDetailPage />} />
<Route path="/tags/:tag" element={<TagPage />} />
<Route path="/search" element={<SearchPage />} />
<Route path="/users/:id" element={<UserProfilePage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/verify-email" element={<VerifyEmailPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
<Route path="/reset-password/:token" element={<ResetPasswordPage />} />
{/* Protected */}
<Route path="/dashboard" element={<ProtectedRoute><DashboardPage /></ProtectedRoute>} />
<Route path="/posts/new" element={<ProtectedRoute><CreatePostPage /></ProtectedRoute>} />
<Route path="/posts/:id/edit" element={<ProtectedRoute><EditPostPage /></ProtectedRoute>} />
<Route path="/settings" element={<ProtectedRoute><SettingsPage /></ProtectedRoute>} />
{/* Admin */}
<Route path="/admin" element={<ProtectedRoute requiredRole="admin"><AdminPage /></ProtectedRoute>} />
{/* 404 */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
</PageLayout>
);
}
PageLayout component wraps every page โ it renders the Header, the NotificationBanner, and the main content area. Placing the layout outside the Routes means the header and notification system are always present regardless of which page is shown. The Outlet pattern (nested routes with a layout route) achieves the same thing โ use whichever feels cleaner for your structure.The Complete PostDetailPage
// src/pages/PostDetailPage.jsx
import { useState, useEffect } from 'react';
import { useParams, Link, useNavigate } from 'react-router-dom';
import DOMPurify from 'dompurify';
import postService from '@/services/postService';
import api from '@/services/api';
import { useAuth } from '@/context/AuthContext';
import { usePostSocket } from '@/hooks/useSocket';
import { useNotifications } from '@/context/NotificationContext';
import CommentSection from '@/components/posts/CommentSection';
function PostDetailPage() {
const { id } = useParams();
const navigate = useNavigate();
const { user } = useAuth();
const { notify } = useNotifications();
const { on } = usePostSocket(id); // joins 'post:id' room on Socket.io
const [post, setPost] = useState(null);
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Fetch post and comments
useEffect(() => {
const controller = new AbortController();
const load = async () => {
try {
setPost(null);
setLoading(true);
const [postRes, commentsRes] = await Promise.all([
postService.getById(id, controller.signal),
api.get(`/posts/${id}/comments`, { signal: controller.signal }),
]);
setPost(postRes.data.data);
setComments(commentsRes.data.data);
} catch (err) {
if (!err.name?.includes('Cancel')) {
setError(err.response?.status === 404 ? 'Post not found' : 'Failed to load');
}
} finally {
setLoading(false);
}
};
load();
return () => controller.abort();
}, [id]);
// Track view count (increment server-side via GET /api/posts/:id)
useEffect(() => {
if (post) document.title = `${post.title} | MERN Blog`;
return () => { document.title = 'MERN Blog'; };
}, [post]);
// Live comments via Socket.io
useEffect(() => {
return on('new-comment', ({ comment }) => {
setComments(prev =>
prev.some(c => c._id === comment._id) ? prev : [...prev, comment]
);
});
}, [on]);
const handleDelete = async () => {
if (!window.confirm('Delete this post permanently?')) return;
try {
await postService.remove(id);
notify.success('Post deleted');
navigate('/dashboard');
} catch (err) {
notify.error(err.response?.data?.message || 'Delete failed');
}
};
if (loading) return <Spinner message="Loading post..." />;
if (error) return <ErrorMessage message={error} />;
if (!post) return null;
const isOwner = user && (user._id === post.author?._id || user.role === 'admin');
return (
<article className="post-detail">
{post.coverImage && (
<img src={post.coverImage} alt={post.title} className="post-detail__cover" />
)}
<div className="post-detail__meta">
<Avatar src={post.author?.avatar} name={post.author?.name} size={40} />
<span>{post.author?.name}</span>
<time>{new Date(post.createdAt).toLocaleDateString()}</time>
<span>{post.viewCount} views</span>
<div className="post-detail__tags">
{post.tags?.map(tag => (
<Link key={tag} to={`/tags/${tag}`} className="tag-badge">#{tag}</Link>
))}
</div>
</div>
<h1 className="post-detail__title">{post.title}</h1>
<div
className="post-detail__body"
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(post.body) }}
/>
{isOwner && (
<div className="post-detail__actions">
<Link to={`/posts/${id}/edit`} className="btn btn--secondary">Edit</Link>
<button onClick={handleDelete} className="btn btn--danger">Delete</button>
</div>
)}
<CommentSection postId={id} comments={comments} setComments={setComments} />
</article>
);
}
Dark Mode with ThemeContext
// src/context/ThemeContext.jsx
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(
() => localStorage.getItem('theme') || 'light'
);
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = useCallback(
() => setTheme(t => t === 'light' ? 'dark' : 'light'),
[]
);
const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
// CSS variables in index.css for theme switching:
// :root[data-theme='light'] { --bg: #ffffff; --text: #111827; --primary: #3b82f6; }
// :root[data-theme='dark'] { --bg: #111827; --text: #f9fafb; --primary: #60a5fa; }
Common Mistakes
Mistake 1 โ Rendering raw HTML without sanitising
โ Wrong โ XSS vulnerability:
<div dangerouslySetInnerHTML={{ __html: post.body }} />
// If post.body contains <script>stealTokens()</script> โ XSS attack
โ Correct โ always sanitise before rendering:
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(post.body) }} /> // โ
Mistake 2 โ Duplicating auth logic in every component
โ Wrong โ checking localStorage for a token in multiple components:
const token = localStorage.getItem('token'); // in Header, PostCard, Dashboard...
โ Correct โ read from AuthContext everywhere:
const { user } = useAuth(); // consistent, reactive, single source of truth โ
Mistake 3 โ Not handling the post loading state (shows blank page)
โ Wrong โ rendering the post before it is fetched:
return <h1>{post.title}</h1>; // TypeError: Cannot read properties of null
โ Correct โ guard clauses before the main render:
if (loading) return <Spinner />;
if (error) return <ErrorMessage message={error} />;
if (!post) return null;
return <h1>{post.title}</h1>; // โ safe
Quick Reference โ Key Component Decisions
| Component | Key Dependencies | Key State |
|---|---|---|
| HomePage | postService, useSearchParams | posts, loading, page |
| PostDetailPage | useParams, postService, usePostSocket, DOMPurify | post, comments, loading |
| CreatePostPage | api, useNavigate, ImageUploader, TagInput | formData, errors, loading |
| DashboardPage | api, useAuth | myPosts, loading |
| SettingsPage | api, useAuth, ImageUploader | formData, loading |
| AdminPage | api (admin endpoints) | users, stats |