
React 19 is the most important update since the introduction of Hooks. It doesn’t bring radical new concepts — it brings the definitive solution to a problem we solved a thousand times in different ways: handling forms and mutations.
`useActionState`: forms without manual useState
import { useActionState } from "react"; // [!code ++]
async function updateProfileAction(prevState: State, formData: FormData) {
try {
await updateProfile({
name: formData.get("name") as string,
bio: formData.get("bio") as string,
});
return { success: true, error: null };
} catch {
return { success: false, error: "Error saving profile" };
}
}
function ProfileForm() {
const [state, action, isPending] = useActionState(
// [!code highlight]
updateProfileAction,
{ success: false, error: null }
);
return (
{state.error && {state.error}
}
{state.success && Saved!
}
);
}
`useOptimistic`: instant UI with automatic rollback
The optimistic update pattern (updating the UI before the server confirms) was tedious. Now:
import { useOptimistic, useActionState } from "react";
function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
// [!code highlight]
initialTodos,
(state, newTodo: Todo) => [...state, newTodo]
);
async function addTodoAction(_: State, formData: FormData) {
const title = formData.get("title") as string;
// Immediate UI update
addOptimisticTodo({ id: crypto.randomUUID(), title, done: false }); // [!code highlight]
// Real mutation (the hook reverts if it fails)
await createTodo(title);
return { error: null };
}
const [state, action, isPending] = useActionState(addTodoAction, {
error: null,
});
return (
{optimisticTodos.map(todo => (
-
{todo.title}
))}
>
);
}</code>
`use()`: consuming Promises and context conditionally
import { use, Suspense } from "react";
async function fetchUser(id: string): Promise {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
function UserProfile({ userPromise }: { userPromise: Promise }) {
const user = use(userPromise); // [!code highlight] — can be used inside conditionals
return {user.name}
;
}
// The Suspense boundary caches and resolves the promise
function App() {
const userPromise = fetchUser("123"); // created outside the component
return (
<Suspense fallback={Loading user…
}>
);
}
Server Actions in practice
React 19 formalizes Server Actions (functions marked with `"use server"` that run on the server):
"use server";
import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";
export async function deletePost(id: string) {
await db.post.delete({ where: { id } });
revalidatePath("/posts"); // invalidates server cache // [!code highlight]
}
import { deletePost } from "./actions";
export function PostCard({ post }: { post: Post }) {
return (
{post.title}
);
}
Summary of new APIs
| API | Replaces | When to use |
| ---------------- | ------------------------------------------- | ----------------------------------------- |
| `useActionState` | `useState` + `useReducer` for forms | Any mutation with UI feedback |
| `useOptimistic` | Manual rollback logic | Updates that improve perceived performance|
| `use(promise)` | `useEffect` + `useState` for data fetching | Components reading promises in render |
| `use(context)` | `useContext` | When you need to read it conditionally |
| `ref` as prop | `forwardRef` | Always — removes the unnecessary wrapper |
Leave a comment