What's New in React 19 — The Changes That Matter
React 19 key changes worth knowing: Actions, useActionState, use(), ref improvements, and native metadata support.

React 19 has been out for a while now, but plenty of projects are still on 18. The upgrade path isn't always obvious, and the "what's actually different" question doesn't get a straight answer from most release notes. So here's a breakdown focused on what changes your day-to-day code — not internal architecture shifts, not compiler theory, just the stuff you'll feel when writing components.
Actions — Form Handling Gets a Real Upgrade
This is the biggest change. How you handle form submissions and data mutations is fundamentally different now.
The old way required a lot of boilerplate:
// React 18 approach
function LoginForm() {
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);
async function handleSubmit(e) {
e.preventDefault();
setIsPending(true);
setError(null);
try {
await login(formData);
} catch (err) {
setError(err.message);
} finally {
setIsPending(false);
}
}
return <form onSubmit={handleSubmit}>...</form>;
}
Loading state, error state, try-catch... the same pattern over and over. React 19 cleans this up with the action prop:
// React 19 approach
function LoginForm() {
async function loginAction(formData: FormData) {
"use server"; // Runs on the server (Next.js, etc.)
await login(formData);
}
return <form action={loginAction}>...</form>;
}
Pass an async function to <form action> and React manages the pending state automatically. Less code, same result.
useActionState — The Companion Hook for Forms
This hook pairs with Actions to manage form state: results, errors, loading.
import { useActionState } from "react";
function ContactForm() {
const [state, submitAction, isPending] = useActionState(
async (prevState, formData: FormData) => {
const result = await sendMessage(formData);
if (!result.success) return { error: result.message };
return { success: true };
},
null // initial state
);
return (
<form action={submitAction}>
<input name="message" />
<button disabled={isPending}>
{isPending ? "Sending..." : "Send"}
</button>
{state?.error && <p className="text-red-500">{state.error}</p>}
{state?.success && <p className="text-green-500">Sent!</p>}
</form>
);
}
isPending is managed for you — no more useState for loading flags. Error handling flows through the return value instead of try-catch blocks.
useOptimistic — Built-in Optimistic Updates
You know the pattern: user clicks "Like," you update the UI immediately without waiting for the server. Previously you had to wire this up yourself. Now there's a hook for it.
import { useOptimistic } from "react";
function LikeButton({ count, onLike }) {
const [optimisticCount, addOptimistic] = useOptimistic(
count,
(current, increment: number) => current + increment
);
return (
<form action={async () => {
addOptimistic(1); // Update UI immediately
await onLike(); // Server request runs in background
}}>
<button>Like {optimisticCount}</button>
</form>
);
}
If the server request fails, it automatically rolls back to the previous state. No manual rollback logic needed.
use() — Reading Promises and Context Directly
The new use() API lets you read a Promise directly inside a component.
import { use } from "react";
function UserProfile({ userPromise }) {
const user = use(userPromise); // Suspense handles loading
return <h1>{user.name}</h1>;
}
This replaces the useEffect + useState pattern for async data loading. Combined with Suspense, loading states are handled automatically.
Unlike other hooks, use() can be called conditionally — inside if statements. That's because use() is technically an API, not a hook, so it's exempt from the rules of hooks.
It also works with Context:
const theme = use(ThemeContext); // replaces useContext(ThemeContext)
useContext isn't going away, but use() is handy when you need conditional reads.
Refs Got Simpler
Until React 18, passing a ref to a function component meant wrapping it with forwardRef. The boilerplate was annoying enough that people sometimes skipped ref forwarding entirely.
// React 18 — forwardRef required
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <input ref={ref} {...props} />;
});
In React 19, ref is just a regular prop:
// React 19 — just a prop
function Input({ ref, ...props }: InputProps & { ref?: Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
No wrapper needed. Cleaner code, better type inference. forwardRef still works but will be deprecated eventually.
Native Metadata Support
Render <title>, <meta>, and <link> tags directly in your components and React automatically hoists them to <head>.
function BlogPost({ post }) {
return (
<>
<title>{post.title}</title>
<meta name="description" content={post.description} />
<article>{post.content}</article>
</>
);
}
Previously you needed react-helmet or Next.js's Head component. With native support, that's one less dependency.
Stylesheet loading order management (<link rel="stylesheet" precedence="high">) was also added, preventing CSS order conflicts when loading styles per component.
Other Practical Changes
Ref callback cleanup — Ref callbacks can now return a cleanup function. Makes it easier to run teardown logic when a DOM element unmounts.
Better error reporting — onCaughtError and onUncaughtError callbacks give you finer control over error boundary behavior.
<Context> as a provider — You can use <Context> directly instead of <Context.Provider>. Small change, one less level of nesting.
Should You Upgrade?
For new projects, just start with React 19. Actions, useActionState, and the ref improvements genuinely make development smoother.
For existing projects, it depends. React 19 has some breaking changes — runtime propTypes checking is removed, ReactDOM.render is fully gone, and some internal APIs changed. Check that your core UI libraries support React 19 before planning a migration.
If there's no rush, waiting for the ecosystem to stabilize is fine. React 18 is perfectly capable for production apps. But the pull of those new APIs will only get stronger as libraries adopt them and tutorials start assuming React 19 as the baseline.