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

React 19 Features Assessment

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.

Analogy🏏Cricket
🏏 Think of it like cricket: Just as a team first drills its fundamental skills — a clean cover drive, a tidy pickup-and-throw, a reliable catch — before combining them into match play, you first build clean, reusable components before composing them into features. The insight is that mastering the fundamentals in isolation makes the combined performance solid: a team with grooved basics plays fluent cricket, exactly as an app of well-built components composes into a fluent UI.

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.

Analogy🏏Cricket
🏏 Think of it like cricket: before a tour you set up the training camp and give each specialist their own net — one lane for openers, one for the spinner, one for the death bowler — so each rehearses a focused, self-contained skill before you bring them together for a full practice match. Just as you scaffold a React project with Vite and a components folder holding Button, Input, Card, Toggle, and Modal as separate files, a camp organises separate stations for each role, each drilling one prop-driven job. Just as each component stays focused and driven by the props passed in, each net drill runs to a clear brief — 'defend the yorker', 'rotate strike' — nothing overloaded. And just as you finally compose them in App to see them work together, the camp ends with a full simulation where every rehearsed piece slots into one XI. The payoff: assembling a working whole is easy because every reusable part was built and tested in isolation first.

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.

bash
// 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.

Analogy🏏Cricket
🏏 Think of it like cricket: Just as the broadcast shows a holding graphic while the opening footage loads and announces the match context (teams, venue) to all viewers at once, the feature shows a Suspense fallback while comments load and broadcasts the user/theme via context. The insight is that you set the stage — context for everyone, a placeholder while content loads — before the interaction begins, exactly as a broadcast establishes context and fills the wait before play.
jsx
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.

Analogy🏏Cricket
🏏 Think of it like cricket: Just as calling for a review follows one defined protocol that tracks its own pending status and resolves with a verdict, the submission uses one Action that tracks pending and resolves with a result. The insight is that the standardised procedure handles the lifecycle for you: the review machinery manages the wait and the decision, exactly as useActionState manages the pending submission and stores its outcome.
jsx
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.

Analogy🏏Cricket
🏏 Think of it like cricket: Just as the broadcast flashes the boundary the instant the ball clears the rope, then confirms it with the official signal, the thread shows the new comment instantly, then confirms it when the server responds. The insight is that you present the expected outcome immediately and reconcile with reality after: the six appears at once but defers to the umpire, exactly as the optimistic comment appears at once but defers to the server's confirmation.
jsx
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.

Analogy🏏Cricket
🏏 Think of it like cricket: the final selection trial where you field the full XI and check every player does their exact job under match conditions. Just as you compose all five components in App and verify each behaviour, a captain runs a scenario and confirms each role fires correctly: the buttons render variants and fire onClick like bowlers delivering their set variations on cue; the controlled Input updating parent state on every keystroke is the batter feeding the scorer every single run in real time; the Toggle flipping and reporting its state is the third-umpire light switching out and back and signalling the result. Just as the Modal opens, closes on the backdrop or button but not when its own content is clicked, a DRS review triggers on a genuine appeal, resolves cleanly, but isn't set off by incidental noise near the stumps. And just as stateless components re-render purely from props while stateful ones manage their own, pure specialists execute exactly the brief while others track their own tally. The payoff: verified, trustworthy behaviour before the real match.
jsx
// 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 shown

Warning: 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.
Lesson 15 of 35
0% complete