Forms in React 19

React 19 has many new form-related features:

  • Server actions – Call async functions that run on the server in a client component
  • useActionState – Update state based on a form action
  • useFormStatus – Get a form’s status
  • useOptimistic – Optimistically update the UI before an async call completes

I’ve been experimenting with these.

Benefits

  1. Encourages web standards like <form> tags with an action, hidden inputs, and submit buttons. No more event.preventDefault!
  2. Graceful degradation. With React Server Components and useActionState, the form is interactive before client JS has executed. (But see concerns below).
  3. Nice DX for simple scenarios. Can easily declare initial form state, call a server action, and read the action’s response via state.
  4. useFormStatus provides the pending state of any parent component. So, I can create a reusable submit button that can “see” whether its parent form is pending. No custom state or props required!
  5. When I can’t use a <form> and useActionState (for reasons I outline below), I call useTransition (already in React 18). useTransition helps me update the state without blocking the UI, keeps my app responsive on slow devices, and gives me control over loading indicators via the isPending flag that it returns.
  6. Less custom form code.

Concerns

  1. Sometimes I have to choose between graceful degradation or good UX. Here’s why: If I use JS-reliant features like useOptimistic or useTransition, the app fails without JS. For instance, to change state and support graceful degradation, you must submit a form via a submit button. (This isn’t React’s fault, it’s how the web works). But, this means some common patterns can’t be implemented with graceful degradation. Example: In a todo app, I have a checkbox by each todo. I want to trigger a server action when I click the checkbox. I have two options: 1) I can require the user to click submit after checking the box (bad UX, but works without JS), or 2) I can call the server action inside useTransition instead (good UX, but requires JS).
  2. Form tag composition is constraining. I can only access a form’s state via useFormState *in a child component*. Sometimes, I need multiple forms on the page (for handling add vs delete, vs toggle). But, forms can’t be nested. This leads to instances where I can’t access the form’s pending state because it’s not “high” enough. Example: I need separate form state for each so I can show pending state for each item. I would like to style each when a state update for that is pending. But, to access form state, I need a form above each. And I can’t put a tag below. That’s invalid HTML. So, I’m forced to call useTransition, just so I can style the while the transition is in progress. I lose graceful degradation in this case.
  3. State decision fatigue. Now I need to choose between useState, useReducer, useActionState, useFormStatus, useTransition, useContext, useDeferredValue, useOptimistic, or some combination of these. Learning how to choose between these hooks and compose them effectively takes time.
  4. The <form> method defaults to POST. I understand why POST is the default – React’s server actions are called via POST, so it’s a handy default. But it would be more standards-friendly if it defaulted to GET, since that’s how the web works.

Summary

React 19 streamlines form code via new hooks for handling form state, actions, pending state, and optimistic updates. It also makes forms more robust by supporting graceful degradation in simple scenarios. The result? Less code and better UX.

But the new features aren’t perfect. Embracing web standards means we must accept the limitations imposed by those web standards. The good news is, these new features are optional, so we can use them when they add value, and skip them if they’re too constraining.

I’m excited for React 19!