React 19 is the most significant update to the framework since the introduction of Hooks. These aren’t cosmetic changes: the mental model for how we manage state, forms, and rendering changes in a structural way. This guide covers everything you need to know with real-world examples.
The Problem React 19 Solves
Before diving into the new features, it’s worth understanding the pain points that existed:
- Forms: handling
isPending, errors, optimism, and rollback required dozens of lines of manual state - Excessive memoization:
useMemoanduseCallbackwere used by default “just in case,” adding noise without measurable benefit - Refs and forwardRef: the API was verbose and error-prone in library components
React 19 tackles all three problems directly.
Actions: The New Model for Forms and Mutations
The central concept of React 19 is Actions: functions that handle state transitions asynchronously. The React runtime automatically manages pending state, errors, and synchronization.
useActionState
Replaces the usual useState + isLoading + error pattern:
import { useActionState } from 'react';
async function createUser(prevState: FormState, formData: FormData) {
try {
const user = await api.createUser({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
return { success: true, user, error: null };
} catch (err) {
return { success: false, user: null, error: 'Failed to create user' };
}
}
export function CreateUserForm() {
const [state, dispatch, isPending] = useActionState(createUser, {
success: false,
user: null,
error: null,
});
return (
<form action={dispatch}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
{state.error && <p className="error">{state.error}</p>}
{state.success && <p className="success">User {state.user?.name} created</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create user'}
</button>
</form>
);
}
useFormStatus
Lets child components read the parent form state without prop drilling:
import { useFormStatus } from 'react-dom';
function SubmitButton({ label }: { label: string }) {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending} className={pending ? 'opacity-50' : ''}>
{pending ? 'Processing...' : label}
</button>
);
}
// Use it inside any <form>
function LoginForm() {
return (
<form action={loginAction}>
<input name="email" type="email" />
<input name="password" type="password" />
<SubmitButton label="Sign in" />
</form>
);
}
useOptimistic
Updates the UI immediately while the request is in flight, and reverts if it fails:
import { useOptimistic } from 'react';
type Message = { id: number; text: string; sending?: boolean };
export function MessageList({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(state, newMessage: Message) => [...state, { ...newMessage, sending: true }]
);
async function sendMessage(formData: FormData) {
const text = formData.get('message') as string;
const tempMessage = { id: Date.now(), text, sending: true };
addOptimistic(tempMessage); // UI updates immediately
await api.sendMessage(text); // Real request in the background
}
return (
<div>
{optimisticMessages.map(msg => (
<div key={msg.id} className={msg.sending ? 'opacity-60' : ''}>
{msg.text} {msg.sending && '⏳'}
</div>
))}
<form action={sendMessage}>
<input name="message" />
<button type="submit">Send</button>
</form>
</div>
);
}
React Compiler: The End of Manual useMemo
The React Compiler (formerly “React Forget”) statically analyzes your code and applies memoization automatically where it makes sense. You no longer need to reason about when to use useMemo or useCallback.
// Before React Compiler — you had to memoize manually
const ExpensiveComponent = memo(({ items, onSelect }: Props) => {
const filtered = useMemo(
() => items.filter(i => i.active),
[items]
);
const handleSelect = useCallback((id: number) => {
onSelect(id);
}, [onSelect]);
return <List items={filtered} onSelect={handleSelect} />;
});
// With React Compiler — write normal code, the compiler handles it
function ExpensiveComponent({ items, onSelect }: Props) {
const filtered = items.filter(i => i.active); // Compiler memoizes if necessary
return <List items={filtered} onSelect={onSelect} />;
}
To enable it in a Vite project:
npm install --save-dev babel-plugin-react-compiler
// vite.config.ts
export default defineConfig({
plugins: [
react({
babel: {
plugins: [['babel-plugin-react-compiler', {}]],
},
}),
],
});
The use() Hook
Allows reading a Promise or Context directly inside a component, even inside conditional blocks:
import { use, Suspense } from 'react';
// Reading a Promise
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // Suspends until resolved
return <div>{user.name}</div>;
}
// Use it with Suspense
function App() {
const userPromise = fetchUser(userId);
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// Reading Context conditionally (impossible with useContext)
function ThemeButton({ showLabel }: { showLabel: boolean }) {
if (showLabel) {
const theme = use(ThemeContext); // ✅ Inside an if — now valid
return <button style={{ color: theme.primary }}>Label</button>;
}
return <button>No label</button>;
}
Ref Improvements: The End of forwardRef
Refs can now be passed as a prop directly, eliminating the forwardRef boilerplate:
// Before — you needed forwardRef
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<input ref={ref} {...props} />
));
// React 19 — ref is a normal prop
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
// Identical usage in both cases
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return <Input ref={inputRef} placeholder="Name" />;
}
Document Metadata from Components
You can manage <title>, <meta> and <link> directly in components without external libraries:
function ArticlePage({ article }: { article: Article }) {
return (
<>
<title>{article.title} — My Blog</title>
<meta name="description" content={article.excerpt} />
<meta property="og:title" content={article.title} />
<link rel="canonical" href={`https://example.com/blog/${article.slug}`} />
<article>
<h1>{article.title}</h1>
<p>{article.content}</p>
</article>
</>
);
}
React automatically hoists these tags to <head>, without duplicates.
Migration Guide from React 18
Most React 18 code works without changes. What you should review:
| Old pattern | React 19 equivalent |
|---|---|
ReactDOM.render() | createRoot().render() (already React 18) |
forwardRef + useImperativeHandle | Normal props with ref |
Defensive useMemo / useCallback | Remove them, the Compiler handles it |
| Form management libraries | useActionState for simple cases |
Conclusion
React 19 significantly reduces boilerplate code in the areas where it hurts most: forms, memoization, and component composition. The migration is incremental and backward compatible, making it easy to adopt in existing projects without a massive rewrite.
The biggest change isn’t technical but mental: you can go back to writing straightforward React code without constantly thinking about performance optimizations.