React 19 and the Actions Pattern

React 19 went stable in December 2024, and it is the most significant React release since Hooks. The headline features are Actions (a new pattern for handling form submissions and mutations), several new hooks, and Server Components becoming officially part of React rather than a Next.js-specific feature.
I have been using the release candidate for a few months, so I have had time to form opinions beyond the initial excitement.
Actions: The New Mutation Pattern
Before React 19, handling a form submission in React required wiring up an onSubmit handler, calling preventDefault, managing loading state, handling errors, and manually updating the UI after the mutation. It was a lot of boilerplate for something that should be simple.
Actions simplify this. A function that uses async transitions is treated as an Action. You can pass actions directly to form elements, and React manages the pending state for you.
function CreatePost() {
async function createPost(formData) {
'use server';
const title = formData.get('title');
const content = formData.get('content');
await db.posts.create({ data: { title, content } });
}
return (
<form action={createPost}>
<input name="title" placeholder="Title" />
<textarea name="content" />
<button type="submit">Create</button>
</form>
);
}
The 'use server' directive marks this as a server action that runs on the server. The form works with progressive enhancement: if JavaScript fails to load, the form still submits as a regular POST request.
useActionState
For more control over the action lifecycle, useActionState gives you the current state, a dispatch function, and a pending flag.
import { useActionState } from 'react';
function LoginForm() {
const [state, formAction, isPending] = useActionState(
async (previousState, formData) => {
const email = formData.get('email');
const password = formData.get('password');
try {
await login(email, password);
return { success: true, error: null };
} catch (e) {
return { success: false, error: e.message };
}
},
{ success: false, error: null }
);
return (
<form action={formAction}>
<input name="email" type="email" />
<input name="password" type="password" />
{state.error && <p className="error">{state.error}</p>}
<button disabled={isPending}>
{isPending ? 'Logging in...' : 'Log in'}
</button>
</form>
);
}
The hook manages the state transitions for you. When the action is in flight, isPending is true. When it completes, the state updates with the return value. Error handling is part of the state machine rather than a separate try/catch in an event handler.
useOptimistic
Optimistic updates have always been possible in React, but they required a lot of manual state management. The new useOptimistic hook makes the pattern first-class.
import { useOptimistic } from 'react';
function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);
async function handleSubmit(formData) {
const title = formData.get('title');
addOptimisticTodo({ id: Date.now(), title });
await addTodo(title);
}
return (
<div>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.title}
</li>
))}
</ul>
<form action={handleSubmit}>
<input name="title" />
<button>Add</button>
</form>
</div>
);
}
When the user submits, the new todo appears immediately (with reduced opacity to indicate it is pending). If the server action succeeds, the real data replaces the optimistic version. If it fails, the optimistic update is automatically rolled back. This pattern used to require 20+ lines of manual state management. Now it is a hook.
The use() Hook
React 19 introduces use(), a hook that can read the value of a Promise or Context. Unlike other hooks, use() can be called inside conditionals and loops.
import { use, Suspense } from 'react';
function UserProfile({ userPromise }) {
const user = use(userPromise);
return <h1>{user.name}</h1>;
}
// Usage:
<Suspense fallback={<p>Loading...</p>}>
<UserProfile userPromise={fetchUser(id)} />
</Suspense>
The use() hook integrates with Suspense to handle the loading state. When the promise is pending, the nearest Suspense boundary shows the fallback. When it resolves, the component renders with the data. This is a cleaner pattern than the useEffect + useState combination for data fetching.
Other Changes
A few smaller but welcome changes in React 19:
refis now a regular prop. No moreforwardRefwrapper. You just pass ref as a prop and it works.- Document metadata (title, meta tags) can be rendered directly in components and React hoists them to the document head.
- Stylesheet links can be declared in components with precedence ordering, and React handles deduplication and loading.
Is the Complexity Worth It
This is the honest question. React has gotten more complex with every major version. Hooks were simpler than classes in some ways but harder in others. Server Components add another mental model. Actions and useActionState add more APIs to learn.
For form-heavy applications with lots of mutations, I think the new patterns are a genuine improvement. The code is shorter, the state management is more robust, and progressive enhancement comes free. For simpler applications, you can still use onChange handlers and fetch calls. The new patterns do not replace the old ones.
My concern is that React is becoming a framework that requires deep expertise to use well. The gap between a beginner React developer and an expert React developer keeps widening. Whether that tradeoff is worth it depends on your team and your application. For me, the new features solve real problems I have had, so I am cautiously positive. But I understand why some developers are looking at simpler alternatives.