Skip to content
How I use the new React 19 hooks in this portfolio's admin — useTransition for non-blocking input, useOptimistic for ins...

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

Al Amin Ahamed

Al Amin Ahamed

Senior Software Engineer

· Updated 15 hours ago 9 min read

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:

TSX
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:

TSX
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:

TSX
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:

TSX
// 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:

TSX
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
Al Amin Ahamed

Al Amin Ahamed

Senior software engineer & AI practitioner. 5+ years shipping Laravel platforms, WordPress plugins, WooCommerce extensions, and AI-driven products.

About me →

More from the blog

Need this kind of work shipped?

Available for freelance and consulting.

Laravel platforms, WordPress plugins, WooCommerce extensions, and AI integrations.