React forms have traditionally been built by connecting input fields to state, handling onChange, validating values, and manually managing submission.
That approach is still useful, especially for forms that need detailed client-side control.
Modern React also provides newer patterns for handling form submissions and asynchronous actions. These patterns can reduce the amount of manual state management required for common form workflows.
Some of the important modern concepts include:
- Form Actions
useActionStateuseFormStatus- Pending states
- Form errors
- Optimistic updates
These features are particularly useful when a form submits data asynchronously, such as creating a post, updating a profile, adding an item to a shopping cart, or sending a message to a server.
The goal is not to replace every traditional form technique. Instead, modern React gives you additional tools for handling asynchronous form workflows in a more declarative way.
Form Actions
A form action is a function that React can use to handle a form submission.
Instead of always writing:
<form onSubmit={handleSubmit}>
you can use an action:
<form action={handleSubmit}>
For example:
function ContactForm() {
async function handleSubmit(formData) {
const name = formData.get("name");
console.log(name);
}
return (
<form action={handleSubmit}>
<input
name="name"
placeholder="Your name"
/>
<button type="submit">
Send
</button>
</form>
);
}
When the form is submitted, React invokes the action with a FormData object.
What Is FormData?
FormData is a browser API for representing form field data.
For example:
const name = formData.get("name");
If the user entered:
Alex
then:
formData.get("name")
returns:
"Alex"
You can retrieve other fields in the same way:
const email = formData.get("email");
const message = formData.get("message");
Form Action Example
async function handleSubmit(formData) {
const data = {
name: formData.get("name"),
email: formData.get("email"),
message: formData.get("message")
};
console.log(data);
}
Then:
<form action={handleSubmit}>
<input name="name" />
<input name="email" type="email" />
<textarea name="message" />
<button type="submit">
Send Message
</button>
</form>
Notice that the inputs do not need individual state variables simply to collect the submitted values.
Why Use Form Actions?
Form Actions can be useful when:
- The primary purpose of the form is submission
- You do not need to react to every keystroke
- The submission is asynchronous
- You want React to manage submission state
- You want to work naturally with
FormData
They can make simple submission workflows easier to express.
Action Functions Can Be Asynchronous
An action can be asynchronous:
async function createPost(formData) {
const title = formData.get("title");
await savePost(title);
console.log("Post created");
}
The action can perform tasks such as:
- Calling an API
- Saving data
- Updating a database through a server-side mechanism
- Performing validation
- Returning an error result
The exact server behavior depends on the React framework or application architecture being used.
Image Placement: Form Actions
Place this image immediately after the Form Actions section.
Create a modern 16:9 educational infographic titled “React Form Actions”. Show a clean form on the left with Name, Email, and Message fields. Show the form submission flowing into a form action function and then into an API/server operation. Use a modern Web4Learners developer aesthetic with rounded cards, subtle gradients, clean typography, minimal labels, and polished contemporary UI. Avoid excessive text and old-fashioned visual styles.
useActionState
useActionState is a React Hook that allows you to manage the result of an Action while also keeping track of its associated state.
It is especially useful for forms that need to display the result of a submission.
You import it from React:
import { useActionState } from "react";
A basic example:
const [state, formAction, isPending] =
useActionState(action, initialState);
The returned values are:
state: the latest result returned by the actionformAction: a function that can be passed to the form’sactionisPending: whether the action is currently pending
Basic Example
import { useActionState } from "react";
async function loginAction(previousState, formData) {
const email = formData.get("email");
const password = formData.get("password");
if (!email) {
return {
error: "Email is required"
};
}
if (!password) {
return {
error: "Password is required"
};
}
return {
success: true
};
}
function LoginForm() {
const [state, formAction, isPending] =
useActionState(loginAction, {
error: null,
success: false
});
return (
<form action={formAction}>
<input
name="email"
type="email"
placeholder="Email"
/>
<input
name="password"
type="password"
placeholder="Password"
/>
<button
type="submit"
disabled={isPending}
>
{isPending ? "Logging in..." : "Login"}
</button>
{state.error && (
<p>{state.error}</p>
)}
{state.success && (
<p>Login successful.</p>
)}
</form>
);
}
The action receives two arguments:
async function loginAction(previousState, formData) {
The first is the previous action state.
The second is the submitted FormData.
Understanding the Flow
The process looks like this:
User submits form
↓
formAction
↓
Action function
↓
Validation / API request
↓
Return result
↓
useActionState updates state
↓
UI displays result
This is useful because the result of the submission becomes part of the React component’s state.
Returning Different Results
An action can return different objects depending on what happened.
For example:
return {
error: "Invalid email"
};
Or:
return {
success: true
};
You can also return field-specific errors:
return {
errors: {
email: "Invalid email",
password: "Password is too short"
}
};
The component can then render the appropriate messages.
useActionState Is Not Only for Errors
You can return useful information from the action.
For example:
return {
success: true,
message: "Profile updated successfully"
};
Then:
{state.message && (
<p>{state.message}</p>
)}
This creates a clear relationship between the result of the action and the UI.
useFormStatus
useFormStatus is a React Hook that provides information about the status of the nearest parent form submission.
It is imported from:
import { useFormStatus } from "react-dom";
A common use is creating a reusable submit button.
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
>
{pending ? "Submitting..." : "Submit"}
</button>
);
}
Then:
function Form() {
return (
<form action={handleSubmit}>
<input name="email" />
<SubmitButton />
</form>
);
}
Why Must useFormStatus Be Inside the Form?
useFormStatus provides status for a parent form.
Therefore, the component calling it needs to be rendered somewhere inside that form.
This works:
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button disabled={pending}>
{pending ? "Saving..." : "Save"}
</button>
);
}
function ProfileForm() {
return (
<form action={saveProfile}>
<input name="name" />
<SubmitButton />
</form>
);
}
This is a useful pattern because the submit button does not need to receive a separate pending prop.
Reusable Submit Button
You can create a reusable component:
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
>
{pending ? "Please wait..." : "Submit"}
</button>
);
}
Then reuse it in multiple forms.
Pending States
A pending state tells users that an operation is currently being processed.
For example:
Submitting...
Saving...
Updating...
Deleting...
Sending...
Without pending feedback, users may wonder whether their click worked.
Modern React provides tools for representing pending form actions.
With useActionState:
const [
state,
formAction,
isPending
] = useActionState(
action,
initialState
);
You can use:
<button disabled={isPending}>
{isPending ? "Saving..." : "Save"}
</button>
With useFormStatus:
const { pending } = useFormStatus();
You can use:
<button disabled={pending}>
{pending ? "Saving..." : "Save"}
</button>
Why Disable the Button?
Suppose a user clicks:
Save
The request takes two seconds.
If the button remains active, the user may click it several times.
That could potentially create multiple submissions.
A pending state allows you to disable the button:
<button disabled={pending}>
This provides both better feedback and a safer interaction pattern.
Pending Text
A simple pending state:
{pending ? "Submitting..." : "Submit"}
You can also display additional UI:
{pending && (
<p>Saving your changes...</p>
)}
Pending State Is Temporary
The lifecycle can be thought of as:
Idle
↓
Submitting
↓
Success / Error
↓
Idle
The exact state management depends on the action implementation, but this general pattern appears frequently in real applications.
Form Errors
Forms can fail for many reasons.
For example:
- Required field is missing
- Email is invalid
- Password is too short
- Username already exists
- Server rejects the request
- Network request fails
- User is not authorized
Modern form Actions can return errors that the component can render.
Returning an Error
async function signupAction(previousState, formData) {
const email = formData.get("email");
if (!email) {
return {
error: "Email is required"
};
}
return {
success: true
};
}
Then:
{state.error && (
<p>{state.error}</p>
)}
Field-Level Errors
For a larger form, returning an object of field errors is useful.
return {
errors: {
name: "Name is required",
email: "Enter a valid email",
password: "Password must be at least 8 characters"
}
};
Then:
{state.errors?.name && (
<p>{state.errors.name}</p>
)}
and:
{state.errors?.email && (
<p>{state.errors.email}</p>
)}
General Form Error
Not every error belongs to one specific field.
For example:
return {
formError: "Unable to save your profile. Please try again."
};
Then:
{state.formError && (
<p>{state.formError}</p>
)}
This is useful for server or application-level failures.
Image Placement: Form Errors and Pending States
Place this image after the Form Errors section.
Create a modern 16:9 educational infographic showing a React form in three states: normal, pending, and error. Show a submit button changing from “Submit” to “Submitting…”, then show a clean field-level error message. Use a modern Web4Learners visual style with rounded cards, subtle gradients, clean developer typography, minimal text, and polished UI. Avoid red dominating the design; use red only as a small visual indicator for the error state.
A Complete useActionState Example
Here is a practical registration form using useActionState.
import { useActionState } from "react";
const initialState = {
errors: {},
success: false
};
async function registerAction(previousState, formData) {
const name = formData.get("name")?.trim();
const email = formData.get("email")?.trim();
const password = formData.get("password");
const errors = {};
if (!name) {
errors.name = "Name is required";
}
if (!email) {
errors.email = "Email is required";
} else if (!email.includes("@")) {
errors.email = "Enter a valid email address";
}
if (!password) {
errors.password = "Password is required";
} else if (password.length < 8) {
errors.password =
"Password must be at least 8 characters";
}
if (Object.keys(errors).length > 0) {
return {
errors,
success: false
};
}
// Submit data to your backend here.
return {
errors: {},
success: true
};
}
function RegistrationForm() {
const [state, formAction, isPending] =
useActionState(
registerAction,
initialState
);
return (
<form action={formAction}>
<div>
<label htmlFor="name">
Name
</label>
<input
id="name"
name="name"
type="text"
aria-invalid={Boolean(state.errors.name)}
/>
{state.errors.name && (
<p>{state.errors.name}</p>
)}
</div>
<div>
<label htmlFor="email">
Email
</label>
<input
id="email"
name="email"
type="email"
aria-invalid={Boolean(state.errors.email)}
/>
{state.errors.email && (
<p>{state.errors.email}</p>
)}
</div>
<div>
<label htmlFor="password">
Password
</label>
<input
id="password"
name="password"
type="password"
aria-invalid={Boolean(
state.errors.password
)}
/>
{state.errors.password && (
<p>{state.errors.password}</p>
)}
</div>
<button
type="submit"
disabled={isPending}
>
{isPending
? "Creating Account..."
: "Create Account"}
</button>
{state.success && (
<p>
Your account was created successfully.
</p>
)}
</form>
);
}
This example demonstrates a complete action-based form workflow:
Form
↓
Action
↓
Validate
↓
Return errors
↓
Display errors
or:
Form
↓
Action
↓
Successful submission
↓
Display success
Optimistic Form Updates
An optimistic update updates the interface immediately, before the server confirms that the operation has completed.
The idea is simple:
Assume the action will succeed and update the UI immediately, while the actual request runs in the background.
This can make applications feel much faster.
For example, imagine a user likes a tutorial.
Without an optimistic update:
User clicks Like
↓
Wait for server
↓
Server responds
↓
Like count changes
With an optimistic update:
User clicks Like
↓
Like count changes immediately
↓
Request sent
↓
Server confirms
If the request fails, the UI can be corrected.
useOptimistic
React provides the useOptimistic Hook for optimistic UI updates.
Import it from React:
import { useOptimistic } from "react";
A basic pattern looks like:
const [optimisticState, addOptimistic] =
useOptimistic(state, updateFunction);
The first value is the optimistic version of your state.
The second value is a function that lets you apply an optimistic update.
Example: Like Button
import { useOptimistic } from "react";
function LikeButton({ likes, saveLike }) {
const [optimisticLikes, addOptimisticLike] =
useOptimistic(
likes,
(currentLikes) => currentLikes + 1
);
async function handleLike() {
addOptimisticLike();
await saveLike();
}
return (
<button onClick={handleLike}>
❤️ {optimisticLikes}
</button>
);
}
The displayed number can increase immediately while the request is being processed.
Optimistic Todo Example
Imagine a todo application.
The user adds:
Learn React
Instead of waiting for the server response, you can show the new todo immediately.
Conceptually:
Current todos
+
New todo
↓
Optimistic UI
↓
Server request
If the server succeeds, the real data replaces or confirms the optimistic result.
If the request fails, you can display an error and restore the appropriate state.
Optimistic Forms
Optimistic updates can be particularly useful for small actions such as:
- Likes
- Favorites
- Following
- Adding comments
- Updating toggles
- Adding items to a cart
- Changing preferences
For example:
function FavoriteButton({
isFavorite,
saveFavorite
}) {
const [optimisticFavorite, setOptimisticFavorite] =
useOptimistic(isFavorite);
async function handleClick() {
setOptimisticFavorite(!optimisticFavorite);
await saveFavorite(!optimisticFavorite);
}
return (
<button onClick={handleClick}>
{optimisticFavorite
? "Remove Favorite"
: "Add Favorite"}
</button>
);
}
The UI responds immediately.
Why Optimistic Updates Feel Fast
Users do not have to wait for a network request before seeing feedback.
This is especially useful when:
- The expected operation is highly likely to succeed
- The UI change is easy to reverse
- The network request takes noticeable time
- Immediate feedback improves the experience
Optimistic Updates Need Error Handling
Optimistic UI should not blindly assume that every request succeeds.
For example, a request may fail because:
- The network is unavailable
- The user lost authorization
- The server rejected the request
- The data became invalid
- A conflict occurred
When that happens, the UI needs a strategy for correcting the optimistic state.
Image Placement: Optimistic Updates
Place this image after the Optimistic Forms section.
Create a modern 16:9 educational infographic titled “Optimistic UI in React”. Show a user clicking a Like button, immediately changing the UI, while a background API request is shown progressing toward server confirmation. Show two small outcomes: success keeps the update, failure rolls it back. Use a modern Web4Learners developer aesthetic with clean typography, rounded cards, subtle gradients, minimal text, and a polished contemporary interface.
Combining useActionState and useFormStatus
These Hooks solve related but different problems.
useActionState is useful for managing the result of an action.
useFormStatus is useful for reading the status of the surrounding form.
For example:
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
async function saveAction(previousState, formData) {
const name = formData.get("name");
if (!name) {
return {
error: "Name is required"
};
}
return {
success: true
};
}
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
>
{pending ? "Saving..." : "Save"}
</button>
);
}
function ProfileForm() {
const [state, formAction] =
useActionState(
saveAction,
{
error: null,
success: false
}
);
return (
<form action={formAction}>
<input
name="name"
placeholder="Name"
/>
{state.error && (
<p>{state.error}</p>
)}
{state.success && (
<p>Profile saved.</p>
)}
<SubmitButton />
</form>
);
}
This creates a clean separation:
useActionState
↓
Action result
↓
Errors / success
useFormStatus
↓
Submission status
↓
Pending UI
When to Use Each Hook
Use useActionState When You Need
- The result of a form action
- Validation errors
- Server errors
- Success messages
- Returned action data
- State associated with an action
Example:
const [state, formAction, isPending] =
useActionState(action, initialState);
Use useFormStatus When You Need
- Pending status inside a form
- A reusable submit button
- Loading indicators
- Disabled form controls
- Submission metadata provided by the surrounding form
Example:
const { pending } = useFormStatus();
Use useOptimistic When You Need
- Immediate UI feedback
- Optimistic updates while an action is running
- Temporary UI state before confirmation
Example:
const [optimisticValue, setOptimisticValue] =
useOptimistic(value);
These Hooks can be used independently or together depending on the application’s requirements.
Modern Form Architecture
A modern React form can follow a structure like:
Form
|
↓
Form Action
|
┌──────────┼──────────┐
↓ ↓ ↓
Validation API Database
|
↓
Action Result
|
┌────┴────┐
↓ ↓
Error Success
|
↓
Error UI
At the same time, the form can expose a pending state:
Form
↓
useFormStatus
↓
Pending UI
And for operations where immediate feedback is useful:
User Action
↓
Optimistic Update
↓
Server Request
↓
Confirm or Correct
This provides a flexible foundation for modern form experiences.
Traditional Forms vs Modern Form Actions
Both approaches are useful.
| Traditional Approach | Modern Action Approach |
|---|---|
onSubmit handler | Form action |
| Manually read state | Can receive FormData |
| Often manually track submission | React action APIs can expose pending state |
| Manually manage result state | useActionState can manage action results |
useState often used for status | useFormStatus can read form status |
| Custom optimistic logic | useOptimistic provides a React pattern |
This does not mean traditional controlled forms are obsolete.
For example, a search field that needs to filter results while the user types may still benefit from controlled state.
A form whose primary purpose is submitting data can often benefit from an action-based approach.
Choose the pattern based on what the form needs to do.
Controlled Inputs with Form Actions
Form Actions do not mean controlled inputs cannot be used.
You can still use state when the interface needs to respond to the current input.
For example:
import { useState } from "react";
function SearchForm() {
const [query, setQuery] = useState("");
return (
<form action={searchAction}>
<input
name="query"
value={query}
onChange={(event) =>
setQuery(event.target.value)
}
/>
<button type="submit">
Search
</button>
</form>
);
}
The input is controlled, while the form submission uses an action.
This is an important point:
Modern form Actions and controlled components are not mutually exclusive.
Use controlled state when you need real-time React control.
Use form Actions when an action-based submission workflow is more convenient.
Common Mistakes with Modern React Forms
Forgetting the name Attribute
If you want to retrieve an input through FormData, it needs a name.
This:
<input name="email" />
can be retrieved with:
formData.get("email")
Without a useful name, the field will not provide the expected entry.
Calling formData.get() with the Wrong Name
If your input is:
<input name="username" />
use:
formData.get("username");
not:
formData.get("user");
The names must match.
Using useFormStatus Outside the Form
useFormStatus is intended to read the status of a parent form.
A common pattern is therefore to create a child component:
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button disabled={pending}>
Submit
</button>
);
}
and render it inside:
<form action={action}>
<SubmitButton />
</form>
Confusing Action State with Form Status
These concepts are related but different.
Action state describes the result or state associated with an action.
Form status describes the current status of the surrounding form submission.
Do not treat them as interchangeable.
Showing Success Before the Action Succeeds
Success UI should normally be based on a successful result.
Pending means:
The operation is still running.
Success means:
The operation completed successfully.
Keep these states separate.
Forgetting Error Handling
An asynchronous action can fail.
Always consider:
Success
Error
Pending
rather than designing only for the successful path.
Overusing Optimistic Updates
Optimistic UI is not appropriate for every operation.
If an operation has serious consequences or is likely to fail, you may want to wait for server confirmation before showing the final result.
Use optimistic updates where temporary UI assumptions are safe and reversible.
Best Practices for Modern React Forms
Keep Actions Focused
An action should have a clear responsibility.
For example:
async function updateProfile(formData) {
// Validate and update profile
}
Avoid turning one action into an unrelated collection of operations.
Validate Submitted Data
Do not assume that form data is valid simply because the browser or React UI checked it.
Validate data again where the actual operation is performed, especially when data crosses a trust boundary.
Provide Pending Feedback
Users should know when their submission is being processed.
For example:
{pending ? "Saving..." : "Save"}
Display Useful Errors
Instead of a generic:
Something went wrong.
when possible, provide a message that helps the user understand what to do next.
Keep Field Errors Close to Fields
For example:
<input name="email" />
{state.errors?.email && (
<p>{state.errors.email}</p>
)}
This makes the form easier to understand.
Use Optimistic Updates Carefully
Optimistic updates work best when the expected result is predictable and the UI can recover cleanly if the request fails.
Separate Pending, Error, and Success States
A useful mental model is:
Idle
↓
Pending
↓
Success
or:
Idle
↓
Pending
↓
Error
Keeping these states separate makes the UI easier to reason about.
Web4Learners Example
Imagine Web4Learners has a Contact Us form.
The user enters:
Name
Email
Message
The form could use an Action:
async function contactAction(previousState, formData) {
const name = formData.get("name");
const email = formData.get("email");
const message = formData.get("message");
if (!name) {
return {
error: "Please enter your name."
};
}
if (!email) {
return {
error: "Please enter your email."
};
}
if (!message) {
return {
error: "Please enter a message."
};
}
// Send the message to your backend.
return {
success: true
};
}
Then:
const [state, formAction, isPending] =
useActionState(
contactAction,
{
error: null,
success: false
}
);
The UI can display:
Idle
before submission.
Then:
Sending...
while the action is running.
Then:
Message sent successfully.
if it succeeds.
Or:
Please enter your email.
if validation fails.
This creates a clear user experience without requiring separate state variables for every submission status.
Practice Exercise
Build a React Web4Learners Feedback Form.
The form should contain:
- Name
- Rating
- Feedback message
- Submit button
Use a form Action to process the submission.
Use useActionState to manage:
- Validation errors
- Success state
- Returned messages
Create a separate SubmitButton component that uses:
useFormStatus()
to display:
Submit Feedback
and:
Submitting...
while the form is pending.
Add validation rules:
- Name is required
- Email is required
- Rating must be selected
- Feedback must contain at least 10 characters
Return field-specific errors.
After a successful submission, display:
Thank you for your feedback!
As an additional challenge, create a favorite or like button using useOptimistic so that the interface updates immediately while the request is being processed.
Modern React Forms Cheat Sheet
| Concept | Purpose |
|---|---|
| Form Action | Handles form submission through an action function |
FormData | Provides submitted form values |
useActionState | Manages state returned by an action |
useFormStatus | Reads the status of a parent form |
pending | Indicates that form submission is in progress |
useOptimistic | Provides a pattern for immediate optimistic UI updates |
| Field error | Error associated with a specific input |
| Form error | Error affecting the overall submission |
| Success state | Indicates that an action completed successfully |
formData.get() | Retrieves a submitted field value |
name | Identifies form fields in FormData |
action | Function invoked when the form is submitted |
Key Takeaways
- Modern React provides action-based patterns for handling form submissions.
- A form can use an
actionfunction to process submittedFormData. - Form fields need appropriate
nameattributes when their values are retrieved fromFormData. useActionStateconnects an action’s returned result to React state.- An action can return validation errors, server errors, success information, or other useful data.
useFormStatusprovides information about the submission status of a parent form.- A reusable submit button can use
useFormStatusto display pending feedback. - Pending states help users understand that an operation is still being processed.
useActionStateanduseFormStatussolve different problems and can be used together.- Controlled components are still useful when the UI needs to respond to input changes in real time.
- Form Actions do not eliminate the need for validation.
- Client-side validation improves the experience, while validation at the appropriate server or application boundary remains important.
- Error states should clearly communicate what went wrong and how the user can fix it.
- Optimistic updates allow the interface to respond before a server operation finishes.
useOptimisticprovides a React pattern for implementing optimistic UI.- Optimistic updates should be used carefully when an operation can fail or has important consequences.
- Good modern forms clearly communicate idle, pending, success, and error states.
- The best form architecture depends on the behavior your form needs, not simply on using the newest API.