100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
React 19 & Ecosystem
60 minintermediate

Mid-Course Project: Full-Featured Blog Application

This mid-course project integrates everything from Modules 1–4 into one real application: a full-featured blog with routing across multiple pages, server data fetched and cached with a data library, forms for creating and editing posts, and accessibility throughout. It is the moment the individual skills — components, hooks, React 19 features, routing, data fetching, forms, and a11y — come together into a coherent product.

You will build a blog where users can browse a post list, read individual posts, and create and edit posts through validated forms, with data managed by TanStack Query and navigation handled by React Router. The emphasis is integration and good structure: a clean component tree, server state in the query cache, route-based pages, accessible forms, and responsive feedback during loading and mutations.

Analogy🏏Cricket
🏏 Think of it like cricket: Just as a full match brings together every skill practised in isolation — batting, bowling, fielding, captaincy — into one coherent contest where they must work together under real conditions, this project brings together components, hooks, routing, data fetching, and forms into one working application. The insight is that integration is its own skill: knowing each technique individually is necessary but not sufficient, exactly as a team of individually skilled players must combine to actually win a match.

Learning Objectives

  • Integrate components, hooks, React 19 features, routing, data fetching, forms, and accessibility into one application.
  • Build multi-page navigation with React Router (list, detail, create, edit) including nested layout and dynamic routes.
  • Manage server state with TanStack Query: cached reads, mutations, and cache invalidation.
  • Implement create/edit forms with validation (React Hook Form or React 19 Actions) and clear feedback.
  • Apply accessibility throughout: semantic markup, labelled forms, focus management, and keyboard support.
  • Handle loading, error, and empty states gracefully across the app.

Technical Requirements

  • Routes for a post list (/), a post detail (/posts/:id), a create page (/posts/new), and an edit page (/posts/:id/edit), within a shared layout.
  • Post list and detail data fetched and cached via TanStack Query (useQuery), with loading and error states.
  • Create and edit implemented with useMutation, invalidating the relevant queries so the UI stays consistent.
  • Forms built with React Hook Form (or React 19 Actions) including validation and accessible error messaging.
  • Accessible navigation (semantic nav, NavLink), labelled inputs, and focus management on route changes or modals.
  • Graceful empty/loading/error UI, and 404 handling for unknown routes.

Architecture & Design

The app uses React Router for navigation with a root layout (header/nav plus an Outlet) wrapping four pages: PostList, PostDetail, PostForm (for both create and edit), and a NotFound catch-all. TanStack Query manages all server state — a posts list query, a per-post detail query keyed by id, and mutations for create/update that invalidate those queries. Forms use React Hook Form with validation, and components are organised into pages, reusable UI (from Module 1's library), and hooks.

Data flows cleanly: route components read server data from the query cache via useQuery (never copied into local state), forms submit through useMutation which invalidates affected queries so reads refresh automatically, and client state (form values, UI toggles) stays local. Accessibility is woven in — semantic structure, labelled and error-associated inputs, focus moved to headings or forms on navigation — rather than bolted on.

Analogy🏏Cricket
🏏 Think of it like cricket: Just as a team has a clear structure — a batting order, a bowling rotation, fielding positions, and a captain coordinating them — the app has a clear structure: routed pages, a query cache coordinating server data, forms for input, and shared components, each with its role. The insight is that a well-organised whole, where each part has a defined responsibility and they cooperate through clear channels, is what makes both a cricket team and an application perform under real conditions.
jsx
// Structure: routing + query cache + forms, each concern in its place
// main.jsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createBrowserRouter, RouterProvider } from "react-router-dom";

const queryClient = new QueryClient();
const router = createBrowserRouter([
  { path: "/", element: <Layout />, children: [
    { index: true, element: <PostList /> },
    { path: "posts/new", element: <PostForm mode="create" /> },
    { path: "posts/:id", element: <PostDetail /> },
    { path: "posts/:id/edit", element: <PostForm mode="edit" /> },
    { path: "*", element: <NotFound /> },
  ]},
]);

createRoot(document.getElementById("root")).render(
  <QueryClientProvider client={queryClient}>
    <RouterProvider router={router} />
  </QueryClientProvider>
);

Phase 1 — Routing Skeleton and Layout

Set up React Router with a root Layout (semantic header and nav using NavLink, plus an Outlet) and the four routes: list, detail, create, edit, and a NotFound catch-all. Stub each page component so navigation works end to end before wiring data. This establishes the app's navigable skeleton and shared chrome.

Use Link/NavLink for navigation and useParams in the detail and edit pages to read the post id. Confirm the back button works, URLs are shareable, and the layout stays mounted across navigations. Add focus management so that navigating to a new page moves focus to its main heading, aiding keyboard and screen-reader users.

Analogy🏏Cricket
🏏 Think of it like cricket: Just as you first mark out the ground — boundary, pitch, creases — before any play begins, you first lay out the routes and layout before wiring data. The insight is that establishing the structure of the space comes first: the field must be marked before the match starts, exactly as the navigable skeleton must exist before features fill it in.
jsx
function Layout() {
  return (
    <>
      <header>
        <nav aria-label="Main">
          <NavLink to="/">Posts</NavLink>
          <NavLink to="/posts/new">Write</NavLink>
        </nav>
      </header>
      <main><Outlet /></main>
    </>
  );
}
function PostDetail() { const { id } = useParams(); /* fetch in Phase 2 */ return <h1>Post {id}</h1>; }
function NotFound() { return <h1>Page not found</h1>; }

Phase 2 — Data Fetching with TanStack Query

Wire the post list and detail to the server using useQuery: a ['posts'] query for the list and a ['post', id] query for the detail. Render loading, error, and empty states properly. The query cache becomes the single source of truth for server data — never copy it into useState — so multiple components stay consistent automatically.

Extract the queries into small custom hooks (usePosts, usePost(id)) to keep page components clean and reuse the fetching logic. Tune staleTime sensibly so navigating back to the list does not always refetch, and rely on background revalidation to keep data fresh. This phase makes the blog display real, cached data.

Analogy🏏Cricket
🏏 Think of it like cricket: Just as the scoreboard draws from one authoritative feed that every display shares, rather than each screen tallying independently, the pages draw post data from one query cache rather than each fetching and storing its own copy. The insight is that a single shared, authoritative data source keeps everything consistent: one feed for all scoreboards, exactly as one cache for all components reading posts.
jsx
// hooks/usePosts.js — encapsulate server-state access
import { useQuery } from "@tanstack/react-query";
export const usePosts = () =>
  useQuery({ queryKey: ["posts"], queryFn: () => api.getPosts(), staleTime: 30_000 });
export const usePost = (id) =>
  useQuery({ queryKey: ["post", id], queryFn: () => api.getPost(id), enabled: !!id });

function PostList() {
  const { data: posts, isLoading, isError } = usePosts();
  if (isLoading) return <p>Loading posts</p>;
  if (isError) return <p role="alert">Couldn't load posts.</p>;
  if (posts.length === 0) return <p>No posts yet. <Link to="/posts/new">Write one</Link></p>;
  return <ul>{posts.map(p => <li key={p.id}><Link to={`/posts/${p.id}`}>{p.title}</Link></li>)}</ul>;
}

Phase 3 — Forms, Mutations, and Accessibility

Build PostForm (handling both create and edit) with React Hook Form: registered, validated fields with accessible labels and error messages, and submission through a useMutation. On success, invalidate the ['posts'] list and the ['post', id] detail queries so the UI reflects the change, and navigate to the new or updated post with useNavigate.

For edit mode, pre-fill the form from the post detail query. Ensure full accessibility: labels tied to inputs, errors associated via aria-describedby and announced, focus moved to the first invalid field on a failed submit, and a disabled submit button with pending feedback while the mutation runs. Handle the mutation's error state with a clear, accessible message.

Analogy🏏Cricket
🏏 Think of it like cricket: Just as a player's recorded innings must be entered accurately into the official scorebook and every display updated to reflect it, a submitted post must be validated, saved via the mutation, and every relevant query updated to reflect it. The insight is that an input is not done when entered but when correctly recorded and propagated everywhere: the innings counts once it is in the book and on the boards, exactly as a post counts once saved and the caches are invalidated.
jsx
function PostForm({ mode }) {
  const { id } = useParams();
  const navigate = useNavigate();
  const qc = useQueryClient();
  const { data: existing } = usePost(mode === "edit" ? id : null);
  const { register, handleSubmit, formState: { errors, isSubmitting } } =
    useForm({ values: existing });            // pre-fill on edit

  const save = useMutation({
    mutationFn: (data) => mode === "edit" ? api.updatePost(id, data) : api.createPost(data),
    onSuccess: (saved) => {
      qc.invalidateQueries({ queryKey: ["posts"] });
      qc.invalidateQueries({ queryKey: ["post", saved.id] });
      navigate(`/posts/${saved.id}`);
    },
  });

  return (
    <form onSubmit={handleSubmit((d) => save.mutate(d))}>
      <label htmlFor="title">Title</label>
      <input id="title" aria-invalid={!!errors.title}
             {...register("title", { required: "Title is required" })} />
      {errors.title && <span role="alert">{errors.title.message}</span>}
      <button disabled={isSubmitting || save.isPending}>
        {save.isPending ? "Saving…" : "Save"}
      </button>
      {save.isError && <p role="alert">Failed to save. Try again.</p>}
    </form>
  );
}

Evaluation Rubric

  • Routing (20%): list/detail/create/edit routes within a shared layout, dynamic params, NavLink, and 404 handling all working with client-side navigation.
  • Data fetching (20%): server state managed via TanStack Query with proper loading/error/empty states; data read from the cache, not copied into local state.
  • Mutations & consistency (15%): create/edit via useMutation with correct query invalidation so the UI stays consistent after changes.
  • Forms (15%): validated create/edit forms (RHF or Actions) with pre-fill on edit and clear submission/pending/error feedback.
  • Accessibility (20%): semantic markup, labelled and error-associated inputs, focus management, and keyboard operability throughout.
  • Code quality & UX (10%): clean component/hook structure, reusable UI, and graceful handling of all states.

Extension Challenges: Add comments to each post using a React 19 Action with useOptimistic for instant feedback (combining Module 3); add search and filtering of the post list with a transition (useTransition) so typing stays responsive over a large list; persist a draft of the post form to localStorage via a useLocalStorage custom hook so unsaved work survives a reload; add Suspense boundaries with route-level data loading; and write a few tests (previewing Module 5) for the form validation and a query hook.

Submit your capstone project

Checking submission status…
Lesson 20 of 35
0% complete