React 19 Features
use() hook, Actions, useFormStatus, useOptimistic, and the React Compiler
What's New in React 19
React 19 is a major release that introduces powerful new primitives for handling async operations, forms, and optimistic updates. These features simplify patterns that previously required complex boilerplate or external libraries.
π Key React 19 Features
- use() β Read promises and context in render
- Actions β Async transitions with automatic pending states
- useFormStatus() β Track form submission state
- useOptimistic() β Optimistic UI updates
- useActionState() β Manage action state and errors
- React Compiler β Automatic memoization (no more manual useMemo/useCallback)
- ref as prop β No more forwardRef needed
The use() Hook
use() lets you read the value of a Promise or Context directly during render. Unlike other hooks, use() can be called inside conditionals and loops.
import { use, Suspense } from 'react';
// Reading a Promise with use()
function UserProfile({ userPromise }) {
// Suspends until the promise resolves
const user = use(userPromise);
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
// Parent passes the promise (created outside render)
function App() {
const userPromise = fetchUser(1); // returns a Promise
return (
<Suspense fallback={<p>Loading user...</p>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// use() with Context β replaces useContext()
function ThemedButton() {
const theme = use(ThemeContext); // can be inside if/loops!
return <button style={{ background: theme.primary }}>Click me</button>;
}
Actions and useTransition
Actions are async functions passed to startTransition. React 19 automatically handles pending states, errors, and optimistic updates for you.
import { useState, useTransition } from 'react';
function UpdateProfile() {
const [name, setName] = useState('');
const [isPending, startTransition] = useTransition();
const [error, setError] = useState(null);
async function handleSubmit() {
setError(null);
startTransition(async () => {
// This async function is an "Action"
const result = await updateProfile({ name });
if (result.error) {
setError(result.error);
return;
}
// React automatically manages isPending
// No need for manual loading state!
});
}
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<input value={name} onChange={e => setName(e.target.value)} />
<button disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
{error && <p className="text-red-500">{error}</p>}
</form>
);
}
useActionState
useActionState manages the full lifecycle of a form action β state, pending status, and error handling in one hook:
import { useActionState } from 'react';
async function addTodo(previousState, formData) {
const title = formData.get('title');
if (!title) {
return { error: 'Title is required', todos: previousState.todos };
}
const newTodo = await saveTodo({ title });
return {
error: null,
todos: [...previousState.todos, newTodo],
};
}
function TodoForm() {
const [state, formAction, isPending] = useActionState(addTodo, {
error: null,
todos: [],
});
return (
<div>
<form action={formAction}>
<input name="title" placeholder="New todo..." />
<button disabled={isPending}>
{isPending ? 'Adding...' : 'Add Todo'}
</button>
{state.error && <p className="text-red-500">{state.error}</p>}
</form>
<ul>
{state.todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
}
useFormStatus
useFormStatus gives child components access to the parent form's submission state. Must be called from a component inside a form:
import { useFormStatus } from 'react-dom';
// This component must be a child of a <form>
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button disabled={pending} className="btn">
{pending ? (
<span className="flex items-center gap-2">
<Spinner /> Submitting...
</span>
) : (
'Submit'
)}
</button>
);
}
// Usage
function ContactForm() {
async function handleSubmit(formData) {
await sendMessage(formData);
}
return (
<form action={handleSubmit}>
<input name="email" type="email" required />
<textarea name="message" required />
{/* SubmitButton reads the form's pending state */}
<SubmitButton />
</form>
);
}
useOptimistic
useOptimistic lets you show an optimistic (predicted) state while an async action is in progress, then automatically reverts if it fails:
import { useOptimistic, useTransition } from 'react';
function MessageList({ messages, sendMessage }) {
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
// Merge function: how to add the optimistic value
(currentMessages, newMessage) => [
...currentMessages,
{ ...newMessage, sending: true },
]
);
const [, startTransition] = useTransition();
async function handleSend(formData) {
const text = formData.get('message');
const optimisticMsg = { id: Date.now(), text, sending: true };
startTransition(async () => {
addOptimistic(optimisticMsg);
await sendMessage(text); // If this fails, optimistic update reverts
});
}
return (
<div>
{optimisticMessages.map(msg => (
<div key={msg.id} className={msg.sending ? 'opacity-60' : ''}>
{msg.text}
{msg.sending && <span className="text-xs"> (sending...)</span>}
</div>
))}
<form action={handleSend}>
<input name="message" />
<button>Send</button>
</form>
</div>
);
}
ref as a Prop (No More forwardRef)
In React 19, function components can accept ref as a regular prop β no more wrapping in forwardRef:
// React 18 β needed forwardRef
const Input = forwardRef(function Input(props, ref) {
return <input ref={ref} {...props} />;
});
// React 19 β ref is just a prop!
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// Usage is the same
function Form() {
const inputRef = useRef(null);
return <Input ref={inputRef} placeholder="Type here" />;
}
The React Compiler (React Forget)
The React Compiler automatically optimizes your components by adding memoization where needed. It eliminates the need to manually use useMemo, useCallback, and React.memo.
// Before React Compiler β manual optimization
function ProductList({ products, onSelect }) {
const sorted = useMemo(
() => [...products].sort((a, b) => a.price - b.price),
[products]
);
const handleClick = useCallback(
(id) => onSelect(id),
[onSelect]
);
return sorted.map(p => (
<Product key={p.id} product={p} onClick={handleClick} />
));
}
// With React Compiler β just write natural code!
function ProductList({ products, onSelect }) {
const sorted = [...products].sort((a, b) => a.price - b.price);
return sorted.map(p => (
<Product key={p.id} product={p} onClick={() => onSelect(p.id)} />
));
}
// The compiler figures out what needs memoization automatically!
β Migration Tips
- β’ React 19 is backward compatible β existing code continues to work
- β’ Start by replacing
forwardRefwith ref-as-prop - β’ Adopt
useActionStatefor form handling in new code - β’ Use
useOptimisticto replace manual optimistic update patterns - β’ The React Compiler is opt-in β enable it per file or folder
- β’
use()works great with Suspense for data fetching