Frontend Capstone — React Application Walkthrough

The React frontend is organised around three separation principles: pages are thin — they compose hooks and components with minimal logic; hooks contain all stateful and side-effect logic; and components are pure rendering functions that receive data as props. This makes every layer independently testable and replaceable. The RTK Query API slice provides a single source of truth for server data; Zustand handles auth state across the app without a Provider wrapper; custom hooks compose these primitives into feature-specific interfaces.

Frontend Directory Structure

frontend/src/
├── main.jsx              # App entry point, providers, Sentry init
├── App.jsx               # Route tree (public + protected routes)
├── config.js             # Environment variable config module
│
├── store/                # Redux store
│   ├── index.js          # configureStore (RTK Query + UI slices)
│   ├── apiSlice.js       # createApi: all blog API endpoints + hooks
│   ├── authSlice.js      # Redux auth slice (if using Redux for auth)
│   └── uiSlice.js        # UI state (modals, sidebar)
│
├── stores/               # Zustand stores
│   └── authStore.js      # User, accessToken, login, logout, init
│
├── context/              # React Context providers
│   └── ToastContext.jsx  # ToastProvider, useToast hook
│
├── hooks/                # Custom hooks library
│   ├── useDebounce.js
│   ├── useLocalStorage.js
│   ├── useMediaQuery.js
│   ├── useOnline.js
│   ├── useForm.js
│   ├── usePagination.js
│   ├── useInfiniteScroll.js
│   ├── useWebSocket.js
│   ├── useNotifications.js
│   ├── useLiveComments.js
│   └── usePostFeed.js    # Composed: debounce + RTK Query + URL params
│
├── services/             # Axios instances (non-RTK calls, e.g. auth)
│   └── auth.js           # authApi: login, register, me, logout
│
├── utils/                # Pure utility functions
│   ├── apiErrors.js      # normaliseApiError, extractFieldErrors
│   ├── dates.js          # formatRelativeDate, formatAbsoluteDate
│   └── slugify.js        # slugify(text)
│
├── components/           # Reusable UI components
│   ├── ui/               # Primitives
│   │   ├── Button.jsx
│   │   ├── Input.jsx     # forwardRef, isInvalid prop
│   │   ├── FormField.jsx # label + input + error wrapper
│   │   ├── TagInput.jsx  # multi-value tag selector
│   │   ├── DropZone.jsx  # drag-and-drop file upload
│   │   ├── Image.jsx     # lazy loading + fallback + skeleton
│   │   └── ToastContainer.jsx
│   ├── auth/
│   │   ├── ProtectedRoute.jsx
│   │   └── LogoutButton.jsx
│   ├── post/
│   │   ├── PostCard.jsx
│   │   ├── PostList.jsx
│   │   └── PostListSkeleton.jsx
│   └── layout/
│       ├── Header.jsx    # Nav + notification bell + user menu
│       └── Footer.jsx
│
└── pages/                # Route-level page components (thin!)
    ├── HomePage.jsx       # usePostFeed → PostList + Pagination
    ├── PostDetailPage.jsx # RTK Query + useLiveComments
    ├── PostEditorPage.jsx # useForm + CoverImageSection
    ├── LoginPage.jsx
    ├── RegisterPage.jsx
    ├── DashboardPage.jsx  # user's own posts
    └── ProfilePage.jsx    # avatar upload + settings
Note: The services/ directory holds the Axios-based auth service that makes calls outside of RTK Query — specifically the login, register, and token refresh endpoints. These are not in the RTK Query API slice because they have side effects (storing tokens, navigating) that are better handled in the Zustand store’s action functions. For every other endpoint (posts, comments, users, notifications), RTK Query’s apiSlice is the authoritative client.
Tip: The config.js module is the single place all environment variables are read and validated. No other file should read import.meta.env directly. This means you can see all configuration in one place, add runtime validation (throw if a required variable is missing), and mock the config in tests by replacing the module. Keep it small and focused — it should only export constants, not functions.
Warning: Keep page components thin. A page component should ideally be 30–60 lines: one hook call for data, one or two layout elements, and a handful of component renders. If a page grows beyond 100 lines, extract a named section component (PostEditorFormSection, CommentFormSection) or a custom hook. Page components that contain form handling, API call logic, and complex conditional rendering become impossible to test and understand.

App.jsx — Route Tree

// src/App.jsx
import { Routes, Route } from "react-router-dom";
import { useEffect }     from "react";
import { useAuthStore }  from "@/stores/authStore";
import Header            from "@/components/layout/Header";
import ProtectedRoute    from "@/components/auth/ProtectedRoute";
import HomePage          from "@/pages/HomePage";
import PostDetailPage    from "@/pages/PostDetailPage";
import PostEditorPage    from "@/pages/PostEditorPage";
import LoginPage         from "@/pages/LoginPage";
import RegisterPage      from "@/pages/RegisterPage";
import DashboardPage     from "@/pages/DashboardPage";
import ProfilePage       from "@/pages/ProfilePage";

export default function App() {
    const init = useAuthStore((s) => s.init);
    useEffect(() => { init(); }, [init]);   // validate stored token on mount

    return (
        <>
            <Header />
            <main className="max-w-5xl mx-auto px-4 py-8">
                <Routes>
                    {/* Public routes */}
                    <Route path="/"              element={<HomePage />} />
                    <Route path="/posts/:postId" element={<PostDetailPage />} />
                    <Route path="/login"         element={<LoginPage />} />
                    <Route path="/register"      element={<RegisterPage />} />

                    {/* Protected routes */}
                    <Route element={<ProtectedRoute />}>
                        <Route path="/dashboard"         element={<DashboardPage />} />
                        <Route path="/profile"           element={<ProfilePage />} />
                        <Route path="/posts/new"         element={<PostEditorPage />} />
                        <Route path="/posts/:postId/edit" element={<PostEditorPage />} />
                    </Route>
                </Routes>
            </main>
        </>
    );
}

🧠 Test Yourself

The PostEditorPage handles both /posts/new and /posts/:postId/edit. How does it know whether to call the create (POST) or update (PATCH) endpoint?