ShaharAmir
← Back to Blog
React3 min read

React 19: Actions Explained

Handle forms and async state with the new Actions API

S
Shahar Amir

React 19: Actions Explained

React 19 introduces Actions — a new way to handle forms and mutations. No more useState + useEffect boilerplate.

The Old Way

javascript
1234567891011121314151617181920
function Form() {
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);
async function handleSubmit(e) {
e.preventDefault();
setIsPending(true);
setError(null);
try {
await submitForm(new FormData(e.target));
} catch (err) {
setError(err.message);
} finally {
setIsPending(false);
}
}
return <form onSubmit={handleSubmit}>...</form>;
}

Too much code for something so common.

The New Way: Actions

javascript
12345678910111213
function Form() {
async function submitAction(formData) {
"use server";
await saveToDatabase(formData);
}
return (
<form action={submitAction}>
<input name="email" type="email" />
<button type="submit">Subscribe</button>
</form>
);
}

Pass an async function to action — React handles the rest.

useActionState Hook

Need loading states and errors? Use useActionState:

javascript
1234567891011121314151617181920212223
import { useActionState } from "react";
function Form() {
const [state, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const error = await subscribe(formData.get("email"));
if (error) return { error };
return { success: true };
},
null // initial state
);
return (
<form action={submitAction}>
<input name="email" type="email" disabled={isPending} />
<button disabled={isPending}>
{isPending ? "Subscribing..." : "Subscribe"}
</button>
{state?.error && <p className="error">{state.error}</p>}
{state?.success && <p className="success">Subscribed!</p>}
</form>
);
}

useOptimistic Hook

Show updates immediately before the server responds:

javascript
123456789101112131415161718192021222324252627282930
import { useOptimistic } from "react";
function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);
async function handleSubmit(formData) {
const newTodo = { text: formData.get("text"), id: Date.now() };
addOptimisticTodo(newTodo);
await addTodo(newTodo);
}
return (
<>
<form action={handleSubmit}>
<input name="text" />
<button>Add</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.text}
</li>
))}
</ul>
</>
);
}

useFormStatus Hook

Access form state from any child component:

javascript
1234567891011121314151617181920
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button disabled={pending}>
{pending ? "Submitting..." : "Submit"}
</button>
);
}
function Form() {
return (
<form action={submitAction}>
<input name="email" />
<SubmitButton /> {/* Knows when form is submitting */}
</form>
);
}

Server vs Client Actions

javascript
12345678910
// Server Action - runs on server
async function serverAction(formData) {
"use server";
await db.insert(formData);
}
// Client Action - runs on client
async function clientAction(formData) {
await fetch("/api/submit", { body: formData });
}

Why Actions?

  • Less boilerplate — no manual pending/error states
  • Progressive enhancement — forms work without JS
  • Optimistic updates — built-in support
  • Server integration — seamless with Server Components

Actions are the future of form handling in React.

#react-19#actions#forms#server

Stay Updated 📬

Get the latest tips and tutorials delivered to your inbox. No spam, unsubscribe anytime.