Skip to content

React 19 in Production: useTransition, useOptimistic, and the End of forwardRef

A

Al Amin Ahamed

Senior Engineer

9 min read
𝕏 in

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

  • useEffect for data fetchinguseTransition covers most cases
  • forwardRef — refs are props now
  • External state libraries for form stateuseFormStatus + Inertia's useForm cover 90%

Migration Risk

React 19 is mostly backwards-compatible. The breakages I hit:

  • Removed propTypes and defaultProps on function components — use TypeScript types instead
  • ReactDOM.render removed — use createRoot
  • string 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.

Share 𝕏 in
A

Al Amin Ahamed

Senior software engineer & AI practitioner. Laravel, PHP, WordPress plugins, WooCommerce extensions.

About me →

One email a month. No noise.

What I shipped, what I read, occasional deep dive. Unsubscribe anytime.