React 19 stabilises a handful of features that have been in canary for a year: useTransition, useOptimistic, useFormStatus, server-side actions, and ref forwarding without forwardRef. They reshape how I write React.
Here's how I use each in production.
useTransition — The Async Default
Async UI work that doesn't block input:
function SearchBox() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState<Post[]>([]);
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value);
startTransition(async () => {
const data = await fetch(`/search?q=${e.target.value}`).then(r => r.json());
setResults(data);
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<Results items={results} />
</>
);
}
The input updates synchronously (no lag). The fetch happens in a transition, so React shows the new query immediately and updates results when ready. Without useTransition, the input would freeze during the fetch on slow connections.
useOptimistic — Lying About State
Show the result before the server confirms:
function LikeButton({ post }: { post: Post }) {
const [optimistic, addOptimistic] = useOptimistic(
post.likes,
(state, action: 'like' | 'unlike') => action === 'like' ? state + 1 : state - 1
);
async function handleLike() {
addOptimistic('like');
await fetch(`/posts/${post.id}/like`, { method: 'POST' });
}
return <button onClick={handleLike}>♥ {optimistic}</button>;
}
The count updates instantly. If the server rejects, React reverts on next render.
The catch: useOptimistic requires being inside a useTransition or a server action. Bare event handlers don't work.
useFormStatus — Form-Adjacent State
When a child component needs to know if its parent form is submitting:
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? 'Saving...' : 'Save'}</button>;
}
function PostForm() {
return (
<form action={savePost}>
<input name="title" />
<SubmitButton />
</form>
);
}
No prop drilling. The button reads its parent form's status via context.
Ref as a Prop
Drop forwardRef. Refs are now regular props:
// React 18
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => (
<input ref={ref} {...props} />
));
// React 19
function Input({ ref, ...props }: Props & { ref?: Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
Less ceremony. The TypeScript types are simpler.
Server Components in an Inertia World
This portfolio uses Inertia.js, not Next.js. So I don't have RSC. But the 'use server' actions still work via Inertia's form helpers:
import { useForm } from '@inertiajs/react';
function PostForm({ post }: { post: Post }) {
const { data, setData, post: submit, processing } = useForm({
title: post.title,
body: post.body,
});
return (
<form onSubmit={(e) => {
e.preventDefault();
submit(`/admin/posts/${post.id}`);
}}>
<input value={data.title} onChange={(e) => setData('title', e.target.value)} />
<button disabled={processing}>Save</button>
</form>
);
}
Inertia's processing is the equivalent of useFormStatus().pending. The mental model transfers.
What I Stopped Using
useEffectfor data fetching —useTransitioncovers most casesforwardRef— refs are props now- External state libraries for form state —
useFormStatus+ Inertia'suseFormcover 90%
Migration Risk
React 19 is mostly backwards-compatible. The breakages I hit:
- Removed
propTypesanddefaultPropson function components — use TypeScript types instead ReactDOM.renderremoved — usecreateRootstring ref={"foo"}removed — use callback refs
For an existing app, expect ~1 day of TypeScript fixes after the upgrade. The runtime behavior is essentially identical.
Al Amin Ahamed
Senior software engineer & AI practitioner. Laravel, PHP, WordPress plugins, WooCommerce extensions.
About me →More from the blog
← Older
Splitting Laravel into Three Vite Bundles for Smaller Pages
Newer →
Tailwind v4 Migration: From v3 Config to CSS-First Tokens
One email a month. No noise.
What I shipped, what I read, occasional deep dive. Unsubscribe anytime.
Check your inbox — confirmation link sent.