React 19: I Deleted 2,000 Lines of useMemo and Nothing Broke
Last month, I ran a dangerous experiment on a production-bound codebase: I stripped out every useMemo, useCallback, and React.memo() wrapper I could find — over 2,000 lines of defensive code written by developers (including me) who weren't sure the compiler was ready. The Lighthouse score went up. The re-render count went down.
This is the story of React 19, and why it changes how we should think about performance forever.
The Root Problem: Manual Memoization is a Lie
We've been writing defensive React code for years. A new engineer joins the team, sees an expensive component, and wraps everything in useMemo — "just to be safe." The result? Code that's harder to read, easier to break, and often not even faster.
The real issue is that memoization in React has always been a manual, error-prone, opt-in system. Miss a dependency? Silent bug. Over-memoize? Wasted memory. Under-memoize? Wasted renders.
Insight
React's core team spent years studying real-world codebases. Their finding: over 70% of useMemo/useCallback calls in production apps are either unnecessary or incorrect. The Compiler was built to make this entire class of problems disappear.
The React Compiler: What It Actually Does
The React Compiler is not magic — it's a static analysis tool that understands React's semantic model. It reads your component code, identifies stable values and referentially stable callbacks, and automatically inserts the correct memoization at compile time.
// What you write
function ProductCard({ product, onAddToCart }: Props) {
const discountedPrice = product.price * (1 - product.discount);
return (
<div onClick={() => onAddToCart(product.id)}>
<h3>{product.name}</h3>
<span>${discountedPrice.toFixed(2)}</span>
</div>
);
}
// What the compiler generates (conceptually)
function ProductCard({ product, onAddToCart }: Props) {
const discountedPrice = useMemo(
() => product.price * (1 - product.discount),
[product.price, product.discount]
);
const handleClick = useCallback(
() => onAddToCart(product.id),
[onAddToCart, product.id]
);
return (
<div onClick={handleClick}>
<h3>{product.name}</h3>
<span>${discountedPrice.toFixed(2)}</span>
</div>
);
}
The compiler is smarter than a human because it never misses a dependency and never over-memoizes — it only wraps what's actually expensive.
Tip
You can opt components into compiler analysis one-by-one with the "use memo" directive (coming in stable). This is great for gradual adoption in legacy codebases.
React Actions: Forms Without the Boilerplate
The other massive addition is Actions — a first-class way to handle async mutations that ties directly into React's concurrency model.
Before Actions, every form was a saga of useState, loading booleans, error catches, and optimistic updates written by hand:
// React 18 — the pain
function ContactForm() {
const [name, setName] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError(null);
try {
await submitContact({ name });
setSuccess(true);
} catch (err) {
setError("Something went wrong");
} finally {
setIsLoading(false);
}
};
// ...200 more lines
}
// React 19 — with useActionState
import { useActionState } from "react";
async function submitContactAction(prevState: State, formData: FormData) {
const name = formData.get("name") as string;
await submitContact({ name });
return { success: true };
}
function ContactForm() {
const [state, action, isPending] = useActionState(submitContactAction, null);
return (
<form action={action}>
<input name="name" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? "Sending..." : "Submit"}
</button>
{state?.success && <p>Message sent!</p>}
</form>
);
}
That's not just less code — it's a fundamentally better mental model. The async lifecycle is owned by React, not you.
New Hooks Cheat Sheet
| Hook | Purpose | Key Benefit |
|---|---|---|
useActionState | Manage form submission state | Built-in pending/error handling |
useFormStatus | Read parent form's status | No prop drilling for isPending |
useOptimistic | Optimistic UI updates | Instant feedback, auto-rollback |
use() | Read promises/context | Works inside conditionals |
Warning
useFormStatus only works inside a component that is nested inside a <form> element with an action. It reads the form state of the closest parent form — not the global application state.
Server Components: The Real Paradigm Shift
React 19 doubles down on Server Components. They're not an optimization — they're a different architecture. A Server Component:
- Never ships JavaScript to the client (zero bundle cost)
- Reads databases, files, and environment variables directly
- Cannot use hooks or event handlers
// This component runs ONLY on the server
// No JS sent to the browser for this component
async function BlogList() {
// Direct DB access — no API route needed
const posts = await db.select().from(blogs).where(eq(blogs.isPublished, true));
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
);
}
Important
The mental model: Server Components are like PHP/Rails templates that can use React's component model and JSX. Client Components are what "React" meant before 2023. You mix both in the same tree — the framework figures out the boundary.
The Migration Checklist
- Enable the Compiler in your Next.js/Vite config
- Run the compiler in "annotation mode" first — it tells you what it would change
- Delete useMemo/useCallback that the compiler flags as redundant
- Replace useState + fetch patterns with
useActionStatewhere applicable - Audit your "use client" boundaries — push them as deep as possible
Conclusion
React 19 isn't just an update — it's a reckoning. It says: all that defensive memoization code you wrote wasn't wisdom, it was a workaround for a compiler that didn't exist yet. All those useEffect patterns for data fetching were workarounds for Server Components that hadn't landed yet.
The framework is finally catching up to how we wish we could write React. The best code is the code you don't have to write.
Arjun Singh
Technical Architect
