React 20 Is Here: Stable Server Actions and Partial Prerendering Change Everything
Hello HaWkers, React 20 is finally here and it's not just another incremental update. This version represents the biggest architectural change since Hooks. Server Actions are now stable, Partial Prerendering eliminates the white screen, and the React Compiler handles automatic memoization.
If you thought you already knew React, get ready to relearn a few things. Let's explore everything that changed.
Server Actions: The Star of React 20
After months in Canary, Server Actions have finally graduated to stable.
What Are Server Actions
Server Actions allow you to execute code on the server directly from React components, without creating separate API routes.
// Before: Needed an API route + fetch on the client
// pages/api/submit.js
export default async function handler(req, res) {
const data = JSON.parse(req.body);
await saveToDatabase(data);
res.json({ success: true });
}
// component.jsx
async function handleSubmit(formData) {
const res = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(formData)
});
return res.json();
}// After: Server Action directly in the component
// actions.js
'use server'
export async function submitForm(formData) {
const name = formData.get('name');
const email = formData.get('email');
await saveToDatabase({ name, email });
return { success: true };
}
// component.jsx
import { submitForm } from './actions';
function ContactForm() {
return (
<form action={submitForm}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit">Submit</button>
</form>
);
}
Practical Benefits
1. Less Boilerplate:
// API routes eliminated
// No manual fetch()
// No manual loading state management
// TypeScript end-to-end2. Progressive Enhancement:
// Form works even without JavaScript!
<form action={serverAction}>
{/* If JS fails, form still submits */}
</form>3. Integration With useActionState:
'use client'
import { useActionState } from 'react';
import { submitForm } from './actions';
function Form() {
const [state, formAction, isPending] = useActionState(
submitForm,
{ message: '' }
);
return (
<form action={formAction}>
<input name="email" disabled={isPending} />
<button disabled={isPending}>
{isPending ? 'Submitting...' : 'Submit'}
</button>
{state.message && <p>{state.message}</p>}
</form>
);
}
Partial Prerendering: End of the White Screen
PPR (Partial Prerendering) solves one of React's oldest problems: the initial loading time.
The Old Problem
User clicks the link
↓
White screen (loading JS)
↓
White screen (hydrating)
↓
Content appears (finally!)
Total time: 2-5 seconds on slow connectionsHow PPR Works
User clicks the link
↓
Static content appears IMMEDIATELY
↓
Dynamic parts stream in as they load
↓
Page fully interactive
Time to first content: < 100msPractical Implementation
// page.jsx - Partial Prerendering in action
import { Suspense } from 'react';
export default function ProductPage({ params }) {
return (
<main>
{/* Static part - rendered at build time */}
<Header />
<ProductDetails id={params.id} />
{/* Dynamic part - streaming */}
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={params.id} />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations userId={getCurrentUser()} />
</Suspense>
{/* Static again */}
<Footer />
</main>
);
}
Performance Metrics
Before PPR:
TTFB: 800ms
FCP: 2.1s
LCP: 3.2s
TTI: 4.5sAfter PPR:
TTFB: 50ms
FCP: 150ms
LCP: 800ms
TTI: 1.2sCompanies that adopted PPR report massive improvements in Core Web Vitals.
React Compiler: Automatic Memoization
Remember all that complexity with useMemo, useCallback, and React.memo? The React Compiler handles it automatically.
Before: Manual Memoization
// The hell of manual memoization
const MemoizedList = React.memo(function List({ items, onItemClick }) {
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
const handleClick = useCallback(
(id) => {
onItemClick(id);
},
[onItemClick]
);
return (
<ul>
{sortedItems.map(item => (
<ListItem
key={item.id}
item={item}
onClick={handleClick}
/>
))}
</ul>
);
});
const ListItem = React.memo(function ListItem({ item, onClick }) {
return (
<li onClick={() => onClick(item.id)}>
{item.name}
</li>
);
});
After: Clean Code
// React Compiler handles the optimization
function List({ items, onItemClick }) {
const sortedItems = items.sort((a, b) =>
a.name.localeCompare(b.name)
);
return (
<ul>
{sortedItems.map(item => (
<ListItem
key={item.id}
item={item}
onClick={() => onItemClick(item.id)}
/>
))}
</ul>
);
}
function ListItem({ item, onClick }) {
return (
<li onClick={onClick}>
{item.name}
</li>
);
}
// Same performance, 60% less codeEnabling the Compiler
// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
// Options
}]
]
};
// Or in Next.js 15+
// next.config.js
module.exports = {
experimental: {
reactCompiler: true
}
};
Asset Loading API
New API for fine-grained control over resource loading.
Smart Prioritization
import { preload, prefetch, preinit } from 'react-dom';
function App() {
// Critical preload - loads immediately
preload('/critical-image.jpg', { as: 'image' });
// Prefetch - loads when idle
prefetch('/next-page-data.json', { as: 'fetch' });
// Preinit - initializes script
preinit('/analytics.js', { as: 'script' });
return <MainContent />;
}Conditional Loading
function ProductGallery({ products }) {
return (
<div>
{products.map((product, index) => {
// First 4 images: high priority
if (index < 4) {
preload(product.image, {
as: 'image',
fetchPriority: 'high'
});
}
return <ProductCard key={product.id} product={product} />;
})}
</div>
);
}
Improved Hooks
React 20 brings significant improvements to existing hooks.
Expanded useDeferredValue
function SearchResults({ query }) {
// Defer non-urgent updates
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.7 : 1 }}>
<Results query={deferredQuery} />
</div>
);
}useOptimistic For Optimistic UI
function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);
async function handleAdd(formData) {
const newTodo = { text: formData.get('text') };
// UI updates immediately
addOptimisticTodo(newTodo);
// Server processes in background
await addTodo(newTodo);
}
return (
<form action={handleAdd}>
<input name="text" />
<ul>
{optimisticTodos.map(todo => (
<li
key={todo.id}
style={{ opacity: todo.pending ? 0.5 : 1 }}
>
{todo.text}
</li>
))}
</ul>
</form>
);
}
Migration From React 19 To 20
The migration is relatively smooth, but there are points to watch out for.
Breaking Changes
// 1. forwardRef is no longer needed
// Before
const Input = forwardRef((props, ref) => (
<input ref={ref} {...props} />
));
// After
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// 2. Context as direct provider
// Before
<ThemeContext.Provider value={theme}>
// After
<ThemeContext value={theme}>Migration Checklist
# 1. Update dependencies
npm install react@20 react-dom@20
# 2. Update TypeScript types
npm install @types/react@20
# 3. Run codemods
npx @react-codemod/update-react-imports
# 4. Check deprecations
npx react-upgrade-check
Mature React Server Components
RSC have moved from experimental to production standard.
Recommended Architecture
app/
├── layout.jsx # Server Component
├── page.jsx # Server Component
├── components/
│ ├── Header.jsx # Server Component
│ ├── Footer.jsx # Server Component
│ └── client/
│ ├── Form.jsx # 'use client'
│ └── Modal.jsx # 'use client'
└── actions/
└── submit.js # 'use server'Golden Rule
// Default: Server Components
// - No directive needed
// - Can access database directly
// - Don't add JS to the client bundle
// Client Components: only when necessary
// - Interactivity (onClick, onChange)
// - Browser hooks (useState, useEffect)
// - Browser APIs (localStorage, etc)
'use client' // Only when you really need it
Performance: Real Numbers
Comparisons from projects migrated to React 20.
Bundle Size
React 19: 142 KB (gzipped)
React 20: 128 KB (gzipped)
Reduction: 10%Core Web Vitals (Average)
Metric | React 19 | React 20 | Improvement
-----------|----------|----------|------------
LCP | 2.1s | 0.9s | -57%
FID | 95ms | 42ms | -56%
CLS | 0.12 | 0.05 | -58%
INP | 180ms | 75ms | -58%Memory Usage
Average application:
React 19: 45MB heap
React 20: 32MB heap
Reduction: 29%What's Coming Next
React 20 is just the beginning. The roadmap indicates:
React 21 (Preview)
- Activity API (replaces StrictMode)
- Built-in animation support
- Suspense improvements
- Offscreen renderingEcosystem
- Next.js 16 with PPR as default
- Remix full adoption of RSC
- React Native with new architecture
Conclusion
React 20 is not just an update — it's a reimagination of how we build web applications. Server Actions eliminate boilerplate, PPR ends white screens, and the Compiler removes the complexity of manual optimization.
For developers, it means simpler code and better performance. For users, it means faster and more responsive applications.
If you haven't started migrating yet, now is the time. The future of React is server-first, streaming-enabled, and automatically optimized.
If you want to keep up with the latest ecosystem news, I recommend checking out another article: TypeScript 7 Native in Go: 10x Faster where we explore another major change that is transforming frontend development.

