Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

Guided: Building Forms with React 19 Actions

In this lab, you'll build React 19 forms that use Actions instead of hand-written submit handlers. You'll return validation errors and results directly from a form Action, give users clear feedback while a submission is in progress, and keep the form responsive when a save fails. You'll come away with a cleaner, repeatable pattern for building forms in modern React.

Lab platform
Lab Info
Level
Intermediate
Last updated
Nov 06, 2026
Duration
45m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. Challenge

    Introduction

    Welcome to the Guided: Building Forms with React 19 Actions lab.


    Forms are where React components tend to collect the most bookkeeping: a flag for pending, an object for errors, a state hook for every field, and a submit handler that stitches them together. React 19 Actions move that work into the form itself, so your component describes what a submission produces instead of managing every intermediate state by hand.

    In this lab, you'll modernize a Globomanitcs feature-request board, a TypeScript form where employees propose improvements to company tools. Its submit handler has three problems you'll fix:

    • It tracks the request list, field values, errors, and a pending flag in separate useState hooks.
    • Its submit button can be clicked again while a save is still running.
    • A failed save silently clears every field.

    By the end of the lab, the board will:

    • submit through a form Action that returns errors and results as state
    • keep the user's input when a save fails
    • use a submit button that reports its own pending state
    • show new requests instantly with an optimistic update

    info> Feeling stuck? Check out the matching solution/stepN/ folder for the step you're on to see a working implementation.

    (Note Globomanitcs is fictional, and it has no affiliation with or endorsement by any real company.)

  2. Challenge

    Step 2: Define the form's Action state

    Here, you'll define the single state object that the form Action returns after every submission. By the end of this step, the board has one typed shape for everything a submission can produce, plus the value it starts from.

    The lab's codebase lives in the application directory, which already contains the board, a simulated save API, a validation helper, and scaffold files with TODO comments marking where you'll work.

    A form Action returns one value after each submission, and React hands that value back to your component as state. In this lab, that value is a BoardState object, and it holds everything the board shows after a submission:

    • requests: the feature requests that saved successfully
    • errors: the validation message for each invalid field
    • values: the values the user submitted
    • message: a save-failure message, or null when there isn't one

    Because every submission produces a complete BoardState, the board can render from one object instead of four separate hooks. The initialBoardState value is what the board shows before anyone submits.

  3. Challenge

    Step 3: Convert the form to an Action with useActionState

    With BoardState defined, the board has one shape for everything a submission can produce, and your Action's job is to build that shape. Here, you'll write submitFeatureRequest so it validates the submitted values, saves valid requests, and turns failures into state, then hand it to the board through useActionState. By the end of this step, submitting the form runs your Action and React tracks the pending state for you, with no onSubmit handler.

    To read the form, use the two arguments a form Action receives: the previous state and a FormData object that holds every named field in the form. In TypeScript, formData.get() returns FormDataEntryValue | null rather than a string, because a field can be missing and a file input returns a File, so convert each value before storing it.

    Hint: how to read one FormData field as a string
    const label = String(data.get('fieldName') ?? '');
    

    To validate, use the provided validateFeatureRequest helper. It returns an object with one message per invalid field, and an empty object means the input is valid. Return those errors as state, together with the submitted values, and clear any earlier message so an old save failure doesn't linger beside new field errors.

    To save, keep in mind that an Action should resolve with the next state even when the save fails. If it throws instead, the error escapes to the nearest error boundary rather than becoming a message the board can show. Wrap the save in try/catch and return a failure state from the catch block, keeping the user's values and the existing requests. In strict mode, TypeScript types the caught value as unknown, because JavaScript can throw any value, so check that it's an Error before reading its message.

    Hint: how to read a message from an unknown caught value
    const text = err instanceof Error ? err.message : 'Fallback message';
    

    To connect the board, use useActionState. It takes an Action and an initial state, and returns three values:

    • the current state, which starts as the initial state
    • a dispatch function that you pass to the form's action prop
    • an isPending flag that is true while the Action runs

    When a form's action prop is a function, React collects the FormData, calls your Action with the previous state, and stores the result, so you no longer need onSubmit or event.preventDefault(). The title, description, and priority inputs keep their own useState hooks until the next step, so their text stays in place after each submission for now.

    const [result, runAction, busy] = useActionState(someAction, startingValue);
    
  4. Challenge

    Step 4: Show inline errors and preserve input

    Your Action now returns errors, failure messages, and the user's values, but the board still shows errors only in a summary list and says nothing when a save fails. Here, you'll put each error beside its field, show the failure message, and switch the inputs so they take their starting value from the returned state. By the end of this step, a user can fix a mistake or retry a failed save without retyping the request.

    An error next to its field tells the user exactly what to fix, while a summary list makes them match each message to a field. Render each field's entry from state.errors directly beneath that field, in an element with an ID such as fieldname-error. A save failure isn't tied to one field, so render state.message above the form in an element with role="alert", which screen readers announce as soon as it appears. The description and priority error blocks are already in the form, commented out and marked TODO (Task 4.1), and they show the conditional-rendering pattern you'll use for the title.

    To preserve input, rely on what React does after a form Action completes without throwing: it resets the form, returning each uncontrolled field to its defaultValue. Your Action already returns what the user typed in state.values, so using those values as each field's defaultValue puts the input right back after a failed or invalid submission and clears it after a successful one. A <select> doesn't pick up a changed defaultValue after that reset, so also give it a key from the same value: when the key changes, React replaces the element and applies the new default. With this pattern, the field-value hooks and onChange handlers are no longer needed.

    <select key={saved.choice} defaultValue={saved.choice}>{/* options */}</select>
    
  5. Challenge

    Step 5: Build a reusable submit button with useFormStatus

    The board now validates, saves, recovers from failure, and preserves input through the Action state, and its header already uses isPending to show that a save is underway. Here, you'll complete a SubmitButton component that reads the status of the form it sits in, so it can disable itself and show a loading label without receiving any props. By the end of this step, double submissions are no longer possible.

    useFormStatus, imported from react-dom, returns an object whose pending property is true while the nearest parent <form> runs its Action. It only reads a form it's rendered inside, so you call it from a child component such as a button, not from the component that renders the <form>. That's the difference from isPending: isPending belongs to the component that called useActionState, while useFormStatus works inside any form, which makes the button reusable.

  6. Challenge

    Step 6: Show new requests optimistically with useOptimistic

    The form now gives clear feedback during every save, but a new request still appears in the list only after the 1.5-second delay. Here, you'll use useOptimistic to show each submitted request immediately with a (saving) marker, and let React remove it automatically if the save fails. By the end of this step, the board feels instant while still showing only what actually saved.

    useOptimistic takes the real state and an update function, and returns two values: the value to display, and a function that adds a temporary update. While an Action is pending, the displayed value includes those temporary updates. When the Action finishes, React discards them and shows the real state again, so a failed save rolls back without any cleanup code.

    const [shownValue, addTemporary] = useOptimistic(realValue, (current, incoming) => combine(current, incoming));
    

    To add an entry, call the add function inside an Action. React shows an optimistic update only while an Action is pending, and it warns if the add function runs outside one, so the call belongs inside the function you pass to useActionState. Wrap submitFeatureRequest in a small async function that adds the entry and then delegates to it, and give the wrapper's parameters explicit types, because strict mode can't infer them there. The wrapper calls addOptimisticRequest, which is declared below useActionState; this works because the wrapper runs only when the form is submitted, after the component has rendered. The project provides createPendingRequest(formData) in src/pendingRequest.ts, which builds the temporary entry with a temporary ID and the pending flag that makes the list show (saving). ### Run the finished board

    With every task complete, you can try the board in a browser.

    1. In Explorer, expand the .instructions folder.
    2. Right-click lab-layout-0.0.13.vsix, and select Install Extension VSIX.
      • You'll likely need to allow pop-ups in your browser to do this.
      • A Terminal and a Simple Browser tab will appear.
    3. In the Terminal that appears, enter cd ~/workspace/application/.
    4. Then run the following:
      npm run dev
      

      You'll see a message containing VITE in the Terminal. (Ignore the URLs there, as they won't work in-lab.)

    5. Go the Simple Browser VS Code tab to view the board. You'll likely need to click the Simple Browser's Reload button.

    Try these submissions:

    1. With the form empty, select Submit request. The title and description errors appear beneath their fields right away.
    2. Enter a title that contains the word fail, add a description, choose High, and select Submit request. The request appears in the list with a (saving) marker, the button shows Submitting... and stays disabled, and the header shows Saving.... After about 1.5 seconds, the entry disappears and an alert reads Save failed: the request service is unavailable., with all three fields still filled in.
    3. Change the title so it no longer contains fail, and select Submit request. The request appears immediately with (saving), the marker disappears when the save completes, and the form clears with the priority back to Medium.

    Congratulations on completing the lab.

    You rebuilt the feature-request board around a React 19 form Action. The Action now returns validation errors, saved requests, and failure messages as state, SubmitButton reports its own pending status, and new requests appear instantly before rolling back when a save fails, as you saw in the running board.

    Along the way, you practiced returning results from an Action instead of managing separate hooks, preserving input with defaultValue, scoping pending state with useFormStatus, and layering optimistic updates over real state. You can apply these patterns to any form where the result of a submission should drive what the user sees.

About the author

Pluralsight’s AI authoring technology is designed to accelerate the creation of hands-on, technical learning experiences. Serving as a first-pass content generator, it produces structured lab drafts aligned to learning objectives defined by Pluralsight’s Curriculum team. Each lab is then enhanced by our Content team, who configure the environments, refine instructions, and conduct rigorous technical and quality reviews. The result is a collaboration between artificial intelligence and human expertise, where AI supports scale and efficiency, and Pluralsight experts ensure accuracy, relevance, and instructional quality, helping learners build practical skills with confidence.

Real skill practice before real-world application

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Learn by doing

Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.

Follow your guide

All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.

Turn time into mastery

On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.

Get started with Pluralsight