React 19 Beta: Live Notes From Day One
React 19 Beta: Live Notes From Day One#
React 19 beta just installed on a side project. Let's see what breaks.
(Nothing broke. Anticlimactic start.)
What follows is basically live notes from going through the new APIs one by one. Raw reactions rather than polished tutorial.
Having been burned before by new React APIs — Suspense for data fetching was "ready" for years before it actually was — the approach here is excited but cautious.
use() — The Hook That Breaks the Rules#
use() is a hook that can be called conditionally. That alone made me do a double take. Every hook tutorial since 2019 has drilled "don't call hooks conditionally" into our heads and now React itself ships one that explicitly allows it.
use() with Context — cleaner way to read context:
import { use, createContext } from "react";
const ThemeContext = createContext<"light" | "dark">("light");
// Before: useContext, always at top level
function OldButton({ children }: { children: React.ReactNode }) {
const theme = useContext(ThemeContext);
return <button className={theme}>{children}</button>;
}
// React 19: can be called inside a conditional
function NewButton({ children, className }: { children: React.ReactNode; className?: string }) {
if (className) {
// early return, no context needed
return <button className={className}>{children}</button>;
}
const theme = use(ThemeContext);
return <button className={theme}>{children}</button>;
}
OK so this is nice for avoiding unnecessary context reads, but honestly? I don't have many components where I conditionally need context. The bigger deal is...
use() with Promises — this is the one that matters:
import { use, Suspense } from "react";
// promise is created in a parent, passed as prop
function UserCard({ userPromise }: { userPromise: Promise<User> }) {
// this suspends the component until the promise resolves
const user = use(userPromise);
return (
<div className="card">
<img src={user.avatarUrl} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
function UserSection({ userId }: { userId: string }) {
// create promise here, pass it down
const userPromise = fetchUser(userId);
return (
<Suspense fallback={<CardSkeleton />}>
<UserCard userPromise={userPromise} />
</Suspense>
);
}
This is "render-as-you-fetch" made ergonomic. You create the promise at one level, pass it down, and the child suspends on it. The Suspense boundary shows the fallback. No useState, no useEffect, no loading boolean.
Important detail: the promise must be created outside the component that calls use(), or memoized. Calling use(fetchUser(id)) directly inside the render function creates a new promise every render. Infinite loop. Read the docs carefully on this one before wiring it up.
In a Next.js Server Component context, use() with promises is less relevant since async components handle it directly. But for client-side React? This is big.
Actions: React Gets an Opinion on Mutations#
React 19 formalizes "Actions" — async functions that handle state transitions. useTransition now accepts async functions:
"use client";
import { useState, useTransition } from "react";
async function addToCart(productId: string, quantity: number) {
const response = await fetch("/api/cart", {
method: "POST",
body: JSON.stringify({ productId, quantity }),
});
if (!response.ok) throw new Error("Failed to add to cart");
return response.json();
}
function AddToCartButton({ productId }: { productId: string }) {
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const handleClick = () => {
startTransition(async () => {
try {
setError(null);
await addToCart(productId, 1);
} catch (err) {
setError(err instanceof Error ? err.message : "Something went wrong");
}
});
};
return (
<div>
<button
onClick={handleClick}
disabled={isPending}
className="rounded-md bg-indigo-600 px-4 py-2 text-white disabled:opacity-50"
>
{isPending ? "Adding..." : "Add to Cart"}
</button>
{error && <p className="text-red-600 text-sm mt-1">{error}</p>}
</div>
);
}
And useActionState (replacing the earlier useFormState from React DOM) is purpose-built for form actions:
"use client";
import { useActionState } from "react";
async function submitContactForm(
prevState: { error: string | null; success: boolean },
formData: FormData
): Promise<{ error: string | null; success: boolean }> {
"use server";
const name = formData.get("name") as string;
const message = formData.get("message") as string;
if (!name || !message) {
return { error: "Name and message are required", success: false };
}
// ... send email, save to db, etc.
return { error: null, success: true };
}
function ContactForm() {
const [state, formAction, isPending] = useActionState(submitContactForm, {
error: null,
success: false,
});
if (state.success) {
return (
<div className="rounded-lg bg-green-50 p-4 text-green-800">
Your message was sent. I will get back to you soon.
</div>
);
}
return (
<form action={formAction} className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium mb-1">Name</label>
<input id="name" name="name" type="text" className="w-full rounded-md border px-3 py-2" />
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium mb-1">Message</label>
<textarea id="message" name="message" rows={4} className="w-full rounded-md border px-3 py-2" />
</div>
{state.error && (
<p className="text-red-600 text-sm">{state.error}</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? "Sending..." : "Send Message"}
</button>
</form>
);
}
If you've been using Next.js App Router with server actions, this all feels very familiar. React is essentially standardizing what Next.js has been doing. Which makes sense — same team, same design philosophy.
ref as a Prop (FINALLY)#
I'm going to be honest: this is the change I'm most excited about in terms of daily developer experience. forwardRef was always the kind of API where I'd look up the syntax every single time:
// React 18 — forwardRef wrapper, display name nonsense
const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
function TextInput({ label, ...props }, ref) {
return (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
</div>
);
}
);
// React 19 — ref is just a prop. that's it.
function TextInput({ label, ref, ...props }: TextInputProps & { ref?: React.Ref<HTMLInputElement> }) {
return (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
</div>
);
}
No wrapper function. No display name gymnastics. No ESLint rule about naming the inner function. ref is a prop. Destructure it. Pass it down. This is how it should have always worked. I'm going to aggressively remove every forwardRef in my codebase the moment I upgrade.
Document Metadata in Components#
React 19 lets you render <title>, <meta>, and <link> from inside components — they hoist to <head> automatically:
function ProductPage({ product }: { product: Product }) {
return (
<>
<title>{product.name} — My Store</title>
<meta name="description" content={product.description.substring(0, 160)} />
<meta property="og:title" content={product.name} />
<meta property="og:image" content={product.imageUrl} />
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCartButton productId={product.id} />
</article>
</>
);
}
Honestly? In a Next.js project you'd still use metadata export or generateMetadata. This adds very little if you're already on Next.js. Its real value is for client-side React apps and people not using a meta-framework. I'm not going to pretend this changes my life.
Stylesheet Ordering#
precedence prop on <link rel="stylesheet"> elements lets React manage insertion order:
function ThemeProvider({ theme }: { theme: "default" | "dark" }) {
return (
<>
<link rel="stylesheet" href="/styles/base.css" precedence="default" />
<link rel="stylesheet" href={`/styles/theme-${theme}.css`} precedence="high" />
</>
);
}
React deduplicates and inserts in precedence order. Solves a real problem in component libraries where stylesheets were inserted in mount order, leading to cascade bugs. Not something I deal with daily since I'm all-in on Tailwind, but I can see this being huge for component library authors.
Things I Don't Understand Yet#
I'm honestly not sure when I'd use use() with context over just calling useContext at the top level. The conditional calling is neat in theory, but in practice, how often do you conditionally need context? Maybe I'll find the use case. Right now it feels like a solution looking for a problem.
The interaction between useOptimistic and error handling also isn't clear to me yet. If the optimistic update happens and then the server action fails, React reverts the state — but what if the user has already interacted with the optimistic state? Like, they saw the item as "published" and navigated away? I need to think about this more.
First Impressions Score#
Ranking the React 19 features by how much they'll change my daily work:
- ref as a prop — Will touch every component library interaction. Immediate quality of life. 10/10.
- useActionState — Already using the Next.js version. Glad it's standardized. 9/10.
- use() with promises — Genuinely new ergonomics for client-side Suspense. I think this becomes a standard pattern in data libraries. 8/10.
- Actions / async useTransition — Clean formalization of a pattern I was doing manually. 7/10.
- Document metadata — Nice for non-Next.js apps. Irrelevant for me currently. 5/10.
- use() with context — I need to find the use case. 4/10.
- Stylesheet precedence — Important for library authors, not for me. 4/10.
Overall: React 19 is a coherent release. Everything fits together into a consistent story about async state management and server-first React. It's not a rewrite — it's React's team cashing in years of Suspense and Server Component design work and making the patterns first-class.
I've been burned before by getting excited about React features too early. But this one feels different. The APIs are smaller, more focused, and solve problems I actually have.
I'll write a proper deep-dive once 19 is stable and I've used it on a real project. For now? Cautiously very excited.
TL;DR: Install the beta, try ref as a prop on your component library wrappers, and play with use() + Suspense for client-side data fetching. Those are the features that'll change how you write React daily. The rest is important but less immediately impactful.