React 19 Was Worth the Wait
I built AssessAI on React 19 from day one. Not because I wanted to be on the bleeding edge — because the features solved real problems I was hitting on every previous project.
After shipping 200+ source files on it, here's what mattered and what didn't.
server components are the big deal
The mental model shift is simple: components render on the server by default. They can be async. They can fetch data directly. No useEffect. No loading states for initial data. No client-side waterfall.
// This is a server component. It runs on the server.
// No "use client" directive, no useState, no useEffect.
async function AssessmentList() {
const assessments = await db.query("SELECT * FROM assessments WHERE active = true");
return (
<ul>
{assessments.map((a) => (
<li key={a.id}>{a.title}</li>
))}
</ul>
);
}The HTML ships fully rendered. The JavaScript for this component? Zero bytes. The client never downloads the database query, the data fetching logic, or the rendering code. It just gets HTML.
On AssessAI this meant the assessment dashboard loads with data already in the DOM. No loading spinner. No layout shift. The user sees content immediately.
use() replaces half of useEffect
The use() hook reads promises and contexts. It works during render — not in an effect that fires after paint.
"use client";
import { use } from "react";
function QuestionDisplay({ questionPromise }: { questionPromise: Promise<Question> }) {
const question = use(questionPromise); // suspends until resolved
return <div>{question.text}</div>;
}This replaces the pattern of useEffect + useState + loading state + error state that we've been writing for years. The promise is created in a server component and passed down. The client component just reads it. Suspense handles the loading boundary.
The subtle win: the fetch starts on the server when the page renders, not when the client component mounts. You eliminate the client-side waterfall entirely.
actions simplified forms
React 19 Actions handle form submissions as a first-class concept. useActionState gives you pending state, error handling, and progressive enhancement in one hook:
"use client";
import { useActionState } from "react";
function SubmitResponse({ questionId }: { questionId: string }) {
const [state, submitAction, isPending] = useActionState(
async (_prev: any, formData: FormData) => {
const response = formData.get("response") as string;
const result = await saveResponse(questionId, response);
if (!result.ok) return { error: result.message };
return { success: true };
},
null
);
return (
<form action={submitAction}>
<textarea name="response" />
<button disabled={isPending}>
{isPending ? "Saving..." : "Submit"}
</button>
{state?.error && <p className="text-red-500">{state.error}</p>}
</form>
);
}No onSubmit. No e.preventDefault(). No manual setLoading(true) / setLoading(false). The form works without JavaScript (progressive enhancement) and upgrades to async when JS loads.
One gotcha that burned me: programmatic input (like from browser automation or testing tools) doesn't trigger React 19's form action handlers the way onChange used to work. This broke my Chrome-based E2E testing. I had to fall back to API-level testing for form flows.
what didn't change much
useOptimistic is nice but niche. I used it exactly once — for optimistic UI updates when candidates save responses. The API is clean but I wouldn't restructure code around it.
The new ref as prop (no more forwardRef) is a nice papercut fix. Not a feature. Just less boilerplate.
useFormStatus is useful inside submit buttons but I rarely need it outside that specific pattern.
the real shift
React 19 isn't a list of features. It's a paradigm change in where code runs. Server by default, client when needed. Data fetches start on the server, not in a useEffect. Forms submit natively before JS even loads.
The migration cost for existing apps is real — the server/client boundary requires rethinking component architecture. But for new projects, the developer experience is genuinely better. Less state management. Less loading boilerplate. Less JavaScript shipped to the browser.
I wouldn't go back.
More in Software Dev
Git Workflow for Solo Founders
Trunk-based dev, feature branches, conventional commits. What works when you're the only person pushing code.
My Code Review Checklist
What I look for when reviewing code: correctness, edge cases, naming, testing. Lessons from leading a team at Blinq.
Docker for Developers, Not Ops
Dev containers, multi-stage builds, compose for local dev. The Docker knowledge that actually matters when you're writing code, not managing infrastructure.