React 19 went stable on December 5, 2024, and I'm just going to say it: this is the biggest shift in how we write React since Hooks dropped back in 2019.
I've been using React 19 in production for the past month, and there are 5 features that have completely changed how I build apps. Not "oh that's nice" changes. I'm talking about "I can't imagine going back" changes.
Let me show you what I mean.
1. Actions: The Death of Manual Form Handling
This is the big one. Actions fundamentally change how you handle async operations in React, and once you start using them, you'll wonder how you ever lived without them.
The Old Way (React 18)
Remember this nightmare?
function TodoForm() {
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: FormEvent) {
e.preventDefault();
setIsPending(true);
setError(null);
try {
const formData = new FormData(e.currentTarget);
await createTodo(formData);
// Reset form, show success, etc.
} catch (err) {
setError(err.message);
} finally {
setIsPending(false);
}
}
return (
<form onSubmit={handleSubmit}>
<input name="title" />
<button disabled={isPending}>
{isPending ? 'Creating...' : 'Create Todo'}
</button>
{error && <p className="error">{error}</p>}
</form>
);
}
Boilerplate city. Loading states, error handling, try-catch blocks everywhere. And if you forgot to set isPending back to false? Your button stays disabled forever.
The New Way (React 19 Actions)
function TodoForm() {
const [error, submitAction, isPending] = useActionState(
async (prevState, formData) => {
try {
await createTodo(formData);
return null; // Success
} catch (err) {
return err.message; // Error
}
},
null // Initial state
);
return (
<form action={submitAction}>
<input name="title" />
<button disabled={isPending}>
{isPending ? 'Creating...' : 'Create Todo'}
</button>
{error && <p className="error">{error}</p>}
</form>
);
}
Look at that. No manual isPending state. No manual error handling. No preventDefault(). It just... works.
Actions handle pending states, errors, and sequential requests automatically. This is huge.
2. useOptimistic: Make Your UI Feel Instant
Ever built a "like" button that feels laggy? useOptimistic fixes that, and it's honestly magical.
The Problem
// Old way: UI waits for server
async function handleLike() {
setIsLiking(true);
await likePost(postId); // User waits here 😴
setLikes(likes + 1);
setIsLiking(false);
}
The UI freezes while waiting for the server. Even if it's only 200ms, it feels slow.
The Solution
function LikeButton({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(currentLikes, amount) => currentLikes + amount
);
async function handleLike() {
addOptimisticLike(1); // UI updates INSTANTLY
try {
const newLikes = await likePost(postId);
setLikes(newLikes); // Sync with server
} catch {
// Automatically rolls back on error
}
}
return (
<button onClick={handleLike}>
❤️ {optimisticLikes}
</button>
);
}
The UI updates immediately. No waiting. And if the server request fails? It automatically rolls back. It's like magic, except it's just good engineering.
I've used this for likes, follows, bookmarks, shopping cart updates—anywhere you want that instant feedback. The UX improvement is night and day.
3. React Compiler: The End of Manual Memoization
This is the feature that made me audibly say "holy shit" when I first saw it work.
The Old Way
// You had to manually memoize EVERYTHING
const MemoizedChild = memo(ExpensiveChild);
function Parent({ data }) {
const processedData = useMemo(() => {
return data.map(item => ({ ...item, processed: true }));
}, [data]);
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <MemoizedChild data={processedData} onClick={handleClick} />;
}
Exhausting. And if you forgot one useMemo? Performance tank.
The New Way (React Compiler)
// Just write normal code
function Parent({ data }) {
const processedData = data.map(item => ({ ...item, processed: true }));
const handleClick = () => {
console.log('clicked');
};
return <ExpensiveChild data={processedData} onClick={handleClick} />;
}
The compiler automatically:
- Memoizes components
- Memoizes expensive computations
- Memoizes callbacks
- Only re-renders when necessary
No more useMemo, useCallback, or memo. The compiler does it all.
Real-World Impact
I removed 127 lines of memoization code from a production app. Performance stayed the same (actually improved slightly). Code became way more readable.
That's the dream, right there.
4. use(): Read Promises Directly in Render
This one's subtle but powerful.
The Old Pattern
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
if (!user) return <Loading />;
return <div>{user.name}</div>;
}
The New Pattern
function UserProfile({ userPromise }) {
const user = use(userPromise);
return <div>{user.name}</div>;
}
// Parent component
function App() {
const userPromise = fetchUser(userId);
return (
<Suspense fallback={<Loading />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
Why this is better:
- No
useEffectneeded - Works seamlessly with Suspense
- Easier to compose and test
- Better TypeScript inference
It's cleaner. It's more declarative. It just feels right.
5. Server Components (Now Production-Ready)
Server Components were experimental in React 18. In React 19, they're production-ready and integrated deeply with frameworks like Next.js.
What Are Server Components?
Components that run only on the server, never shipped to the client.
// app/dashboard/page.tsx (Server Component)
async function DashboardPage() {
// This runs on the SERVER
const user = await db.user.findUnique({ where: { id: userId } });
const posts = await db.post.findMany({ where: { authorId: userId } });
return (
<div>
<h1>Welcome, {user.name}</h1>
<PostList posts={posts} />
</div>
);
}
Benefits:
- ✅ Zero JavaScript sent to client (for this component)
- ✅ Direct database access (no API layer needed)
- ✅ Faster initial page loads
- ✅ Better SEO (fully rendered HTML)
When to Use Server vs Client Components
Server Components (default):
- Fetching data
- Accessing backend resources
- Keeping sensitive info on server
- Reducing client bundle size
Client Components ('use client'):
- Interactivity (onClick, onChange, etc.)
- Browser APIs (localStorage, window, etc.)
- React hooks (useState, useEffect, etc.)
- Third-party libraries that need the browser
The Upgrade Path: Should You Migrate?
If you're starting a new project: Use React 19. No question.
If you have an existing app:
Easy Wins (Do These First)
- Replace form handling with Actions - Huge DX improvement
- Add
useOptimisticto interactive features - Better UX - Let the compiler optimize - Enable it, remove manual memoization
Bigger Lifts (Plan These)
- Adopt Server Components - Requires framework support (Next.js 15+)
- Refactor data fetching to
use()- Nice-to-have, not urgent
What I'm Doing Differently
Since adopting React 19, my code looks different:
Before (React 18)
function TodoApp() {
const [todos, setTodos] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const addTodo = useCallback(async (title) => {
setIsLoading(true);
try {
const newTodo = await api.createTodo(title);
setTodos([...todos, newTodo]);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
}, [todos]);
// ... more boilerplate
}
After (React 19)
function TodoApp() {
const [todos, setTodos] = useState([]);
const [optimisticTodos, addOptimisticTodo] = useOptimistic(todos);
const [error, addTodo, isPending] = useActionState(
async (_, formData) => {
const title = formData.get('title');
addOptimisticTodo({ id: Date.now(), title, done: false });
try {
const newTodo = await api.createTodo(title);
setTodos([...todos, newTodo]);
return null;
} catch (err) {
return err.message;
}
},
null
);
// So much cleaner
}
The Bottom Line
React 19 isn't just "React with new features." It's a paradigm shift:
- Actions replace manual async handling
useOptimisticmakes UIs feel instant- React Compiler removes performance footguns
use()simplifies data fetching- Server Components reduce client bundle size
If you're still writing React like it's 2022, you're missing out.
Start with Actions and useOptimistic. Those two alone will make your apps feel 10x better.
Frequently Asked Questions
Is the React Compiler enabled by default in React 19? Not yet. It ships with React 19, but it's opt-in. You'll need to enable it in your build config. The React team is testing it at scale and plans to enable it by default in a future version.
Do I have to rewrite my entire app to use Partial Prerendering? Nope. If you're already using Suspense for loading states, you're most of the way there. The framework handles the complex parts.
Can I still use API Routes in React 19? Yes. While Server Actions are recommended for data mutations from React Server Components, API Routes are still great for REST/GraphQL endpoints, webhooks, or third-party integrations.
Does the React Compiler mean I should remove all my existing
useMemoanduseCallbackhooks? Leave them for now. The compiler is smart enough to respect existing manual memoization. For new code, you can skip them and let the compiler do its job.
What about React 18 projects? Should I upgrade immediately? Not necessarily. React 19 is stable, but if your React 18 app is working fine, there's no rush. Plan the upgrade, test it thoroughly, and migrate when you're ready. The benefits are real, but so is the testing overhead.