What's New in React 19
React 19 (stable Q1 2026) ships the most developer-impactful features since React 16 introduced hooks. The theme is Actions: a unified pattern for handling async mutations that eliminates the verbose useState + useEffect + loading/error state boilerplate that every React developer has written hundreds of times.
| Feature | Before (React 18) | After (React 19) |
|---|---|---|
| Form submissions | useState + useEffect + manual error handling | Actions with built-in pending/error states |
| Optimistic UI | Manual state + rollback logic | useOptimistic hook |
| Form status | Prop drilling or Context | useFormStatus hook |
| Memoization | useMemo + useCallback everywhere | React Compiler (auto-memoization) |
| Refs on function components | forwardRef wrapper | ref as regular prop |
1. Actions β The End of Form Boilerplate
React 18 β The Old Way
function UpdateNameForm() {
const [name, setName] = useState('');
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setIsPending(true);
setError(null);
try {
await updateUserName(name);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setIsPending(false);
}
}
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
{error && <p className="error">{error}</p>}
<button disabled={isPending}>{isPending ? 'Saving...' : 'Save'}</button>
</form>
);
}
React 19 β Actions
import { useActionState } from 'react';
async function updateNameAction(prevState: unknown, formData: FormData) {
const name = formData.get('name') as string;
if (!name || name.length < 2) {
return { error: 'Name must be at least 2 characters' };
}
await updateUserName(name);
return { success: true };
}
function UpdateNameForm() {
const [state, submitAction, isPending] = useActionState(updateNameAction, null);
return (
<form action={submitAction}>
<input name="name" />
{state?.error && <p className="error">{state.error}</p>}
{state?.success && <p className="success">Name updated!</p>}
<SubmitButton />
</form>
);
}
2. useFormStatus β Smart Submit Buttons
import { useFormStatus } from 'react-dom';
function SubmitButton({ label = 'Submit' }: { label?: string }) {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending} aria-busy={pending}>
{pending ? '<span className="spinner" /> Saving...' : label}
</button>
);
}
// Reuse across any form β automatically picks up the right pending state
function CreatePostForm() {
const [state, action] = useActionState(createPostAction, null);
return (
<form action={action}>
<input name="title" placeholder="Post title" />
<textarea name="body" placeholder="Content..." />
<SubmitButton label="Publish Post" />
</form>
);
}
3. useOptimistic β Instant UI Feedback
import { useOptimistic, useActionState } from 'react';
type Message = { id: string; text: string; status: 'sent' | 'pending' };
function ChatThread({ initialMessages }: { initialMessages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic<Message[], string>(
initialMessages,
(currentMessages, newText) => [
...currentMessages,
{ id: Date.now().toString(), text: newText, status: 'pending' },
]
);
async function sendMessageAction(_: unknown, formData: FormData) {
const text = formData.get('text') as string;
addOptimisticMessage(text); // Update UI immediately
await sendMessage(text); // Then actually send
}
const [, action] = useActionState(sendMessageAction, null);
return (
<>
<div className="messages">
{optimisticMessages.map(msg => (
<div key={msg.id} style={{ opacity: msg.status === 'pending' ? 0.5 : 1 }}>
{msg.text}
</div>
))}
</div>
<form action={action}>
<input name="text" autoFocus />
<SubmitButton label="Send" />
</form>
</>
);
}
4. React Compiler β Automatic Memoization
// Before Compiler β manual memoization
const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) {
const sortedItems = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
const handleSelect = useCallback((id: string) => onSelect(id), [onSelect]);
return sortedItems.map(item => <Item key={item.id} item={item} onSelect={handleSelect} />);
});
// After Compiler β write normal code
function ExpensiveList({ items, onSelect }) {
const sortedItems = [...items].sort((a, b) => a.name.localeCompare(b.name));
return sortedItems.map(item => (
<Item key={item.id} item={item} onSelect={() => onSelect(item.id)} />
));
}
Important: React Compiler requires components to follow the Rules of React (pure renders, no mutation of props/state). Components that violate the rules are automatically skipped by the compiler.
5. Refs as Regular Props
// React 18 β required forwardRef wrapper
const Input = React.forwardRef<HTMLInputElement, { label: string }>(
function Input({ label }, ref) {
return <input ref={ref} aria-label={label} />;
}
);
// React 19 β ref is just a prop
function Input({ label, ref }: { label: string; ref: React.Ref<HTMLInputElement> }) {
return <input ref={ref} aria-label={label} />;
}
const inputRef = useRef<HTMLInputElement>(null);
<Input label="Username" ref={inputRef} />