What You'll Build
You will build a small but complete feature that exercises the headline React 19 capabilities together: a comment thread with a submission form powered by an Action and useActionState, instant feedback via useOptimistic, a Suspense boundary for loading the initial comments, and a context providing the current user and theme. It is a focused assessment that proves you can combine the module's features into one coherent interface.
The feature lets a user view comments (loaded via Suspense), post a new comment (via an Action with optimistic display), and have the UI reflect the current user and theme from context. The goal is integration: Actions for the mutation, useOptimistic for responsiveness, Suspense for the read, and context for cross-cutting data — the React 19 toolkit working as a whole.
Prerequisites
- Completion of lessons 11–14, or equivalent familiarity with Actions/useActionState, Suspense/use, useOptimistic, and context.
- A React 19 + Vite project running locally.
- A mock async API (functions returning promises) for fetching and posting comments.
- Comfort with the rules of hooks and async functions.
- Understanding that optimistic state is temporary and must reconcile with the real result.
Setup & Project Structure
Create a mock async API with getComments() returning a promise of comments and postComment(text, user) returning a promise of the saved comment. Set up a context providing the current user and theme, and a CommentThread component that will combine the Suspense read, the Action submission, and the optimistic display.
Structure the feature so context wraps the thread, a Suspense boundary surrounds the comment list (which reads the comments promise via use), and the form uses useActionState with useOptimistic inside. Each React 19 feature gets a clear place in the structure, which is the point of the assessment.
// mockApi.js
export const getComments = () =>
new Promise(res => setTimeout(() => res([{ id: 1, text: "First!", user: "Sam" }]), 800));
export const postComment = (text, user) =>
new Promise(res => setTimeout(() => res({ id: Date.now(), text, user }), 600));
// context.js — current user + theme via context (React 19 provider syntax)
import { createContext, useContext } from "react";
export const AppContext = createContext(null);
export const useApp = () => useContext(AppContext);Step 1 — Provide Context and Suspend on the Read
Wrap the feature in an AppContext provider supplying the current user and theme. Inside, place a Suspense boundary around a CommentList component that reads the comments promise with the use hook, so React shows a fallback while the initial comments load — no manual loading state.
Create the comments promise once (outside render or via a stable source) so it is not recreated each render, then pass it to CommentList, which calls use(commentsPromise) and suspends until it resolves. The context makes the current user available to the form and list without prop drilling.
import { Suspense, use } from "react";
import { AppContext, useApp } from "./context";
import { getComments } from "./mockApi";
const commentsPromise = getComments(); // created once (stable), not per-render
function Feature() {
return (
<AppContext value={{ user: "You", theme: "dark" }}>
<Suspense fallback={<p>Loading comments…</p>}>
<CommentThread commentsPromise={commentsPromise} />
</Suspense>
</AppContext>
);
}
function CommentList({ commentsPromise }) {
const comments = use(commentsPromise); // suspends until resolved
return comments; // (rendered by CommentThread below)
}Step 2 — Action + useActionState for Submission
Build the submission form with useActionState: an async action reads the comment text from FormData, calls postComment with the current user from context, and returns updated state. Attach the returned formAction to the form and use the isPending flag (or useFormStatus in the button) to disable the control while submitting.
The action returns structured state — the new comment or an error — which useActionState stores. This replaces any manual loading/error state: React tracks the pending submission and stores the result, and the form reads cleanly from the returned state and pending flag.
import { useActionState } from "react";
import { postComment } from "./mockApi";
function CommentForm({ onPosted }) {
const { user } = useApp(); // current user from context
const [state, formAction, isPending] = useActionState(
async (prev, formData) => {
const text = formData.get("text")?.trim();
if (!text) return { error: "Comment cannot be empty" };
const saved = await postComment(text, user);
onPosted(saved);
return { error: null };
},
{}
);
return (
<form action={formAction}>
<input name="text" aria-invalid={!!state.error} />
<button disabled={isPending}>{isPending ? "Posting…" : "Post"}</button>
{state.error && <span role="alert">{state.error}</span>}
</form>
);
}Step 3 — useOptimistic for Instant Feedback
Wrap the comment list with useOptimistic so a new comment appears immediately when submitted, before the server confirms. Provide the current comments and an updater that appends an optimistic comment (marked pending); call the optimistic adder inside the action before awaiting postComment, so the UI updates instantly.
When the real postComment resolves, reconcile by appending the confirmed comment to the actual state, and React discards the optimistic one. Style pending comments (e.g. reduced opacity) to signal they are not yet confirmed, and handle the failure case so a rejected post does not leave a phantom comment.
import { useOptimistic, useState } from "react";
function CommentThread({ commentsPromise }) {
const initial = use(commentsPromise);
const [comments, setComments] = useState(initial);
const [optimistic, addOptimistic] = useOptimistic(
comments,
(cur, text) => [...cur, { id: "temp", text, pending: true }]
);
async function handlePost(saved) { setComments(cs => [...cs, saved]); } // reconcile
return (
<section>
{optimistic.map(c => (
<p key={c.id} style={{ opacity: c.pending ? 0.5 : 1 }}>{c.text}</p>
))}
<CommentForm
onPosted={handlePost}
onOptimistic={(text) => addOptimistic(text)} // call before awaiting in the action
/>
</section>
);
}Step 4 — Testing & Verification
Run the feature and confirm all four React 19 capabilities work together: the comment list loads behind a Suspense fallback (not a manual spinner), the form submits via an Action with a pending state, a new comment appears instantly via useOptimistic and then settles when confirmed, and the current user/theme come from context. Test the empty-comment error path and a simulated failure to ensure optimistic comments reconcile or revert correctly.
// App.jsx — mount and exercise the integrated feature
import { Feature } from "./Feature";
export default function App() { return <Feature />; }
// Manual test checklist:
// - On load: Suspense fallback shows, then comments appear (use + Suspense)
// - Submit empty: error from action state (useActionState)
// - Submit text: comment appears immediately, dimmed, then solid (useOptimistic)
// - Button disabled while posting (isPending / useFormStatus)
// - User/theme read from context with no prop drilling
// - Simulate postComment rejection -> optimistic comment reverts, error shownWarning: Create the comments promise once (e.g. outside render or from a stable cache), not inside the component body — a promise recreated every render makes use() suspend repeatedly and can loop. In real apps, a data library (TanStack Query, covered next module) manages this caching for you; here, keep the promise stable so Suspense resolves once rather than re-suspending on every render.
Extension Challenge: Replace the hand-managed comments promise with a real data-fetching approach and add a useTransition so switching between comment threads (e.g. different posts) keeps the UI responsive while the new thread loads. Then build a reusable SubmitButton using useFormStatus so any form in the app gets consistent pending behaviour, and add an error boundary (previewed for Module 6) around the Suspense to catch fetch failures gracefully.
- Suspense + the use hook handle the initial read with a fallback — no manual loading state.
- useActionState wires the submission action, tracking pending and storing structured result/error state.
- useOptimistic shows the new comment instantly, then reconciles with the confirmed server result.
- Context provides the current user and theme to the form and list without prop drilling.
- Create the data promise once (stable) so use() doesn't re-suspend every render; a data library handles this in production.
- Handle error and failure paths so optimistic updates reconcile or revert with clear feedback.