Building the Complete React Frontend

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>
  );
}
Note: The 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.
Tip: Build and style the core layout (Header, Footer, main area, responsive breakpoints) before building any page content. Every page sits inside this layout โ€” getting it right first means every subsequent page looks consistent immediately. Then build the most visited page first (HomePage with post list), verify it against the API, and move to post detail, then auth forms. Save admin and settings pages for last.
Warning: The PostDetailPage is the most complex page in the MERN Blog โ€” it combines useParams (post ID from URL), useEffect (fetch post + comments), usePostSocket (real-time comments + typing), DOMPurify (sanitise HTML body), and auth checks (show edit/delete to owner). Build it in stages: fetch and display the post first, then add comments, then add Socket.io. Avoid adding everything at once or debugging becomes very difficult.

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

🧠 Test Yourself

The PostDetailPage fetches a post and renders dangerouslySetInnerHTML={{ __html: post.body }}. A security audit flags this. What specifically is the risk and what is the fix?