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 serverasync function serverAction(formData) { "use server"; await db.insert(formData);}
// Client Action - runs on clientasync 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.