React 20 New Features and Migration Guide 2026
React 20 is the first major release since React 19 landed Server Components and Actions as experimental APIs. The headline: Server Components are now stable (no more experimental_ prefix), use() is the official primitive for async data in components, and several deprecated patterns from the class component era are finally removed. Here's what changed and how to migrate.
TL;DR
React 20 stabilizes Server Components, promotes use() to a first-class hook, adds native <ViewTransition> support, and removes legacy APIs (defaultProps on function components, string refs, legacy context). Most React 19 apps will upgrade with minimal changes — the breaking removals affect patterns deprecated since React 16-17.
Key Changes
| Feature | React 19 | React 20 |
|---|---|---|
| Server Components | Experimental | Stable |
use() hook | Experimental | Stable |
<ViewTransition> | Not available | Built-in |
defaultProps (function) | Deprecated warning | Removed |
| String refs | Deprecated warning | Removed |
| Legacy context API | Deprecated warning | Removed |
react-dom/test-utils | Deprecated | Removed |
| Ref as prop | Supported (React 19+) | Standard pattern |
Stable Server Components
The biggest change in React 20 is purely a stability classification. Server Components — async components that run on the server and send rendered HTML to the client — are no longer behind an experimental flag. The API surface is identical to React 19's implementation, but it's now covered by React's semantic versioning guarantee.
What this means in practice: frameworks can depend on RSC without shipping experimental React builds. Next.js App Router, TanStack Start, and Remix have all updated their RSC integrations to use the stable API.
If you were already using Server Components via Next.js App Router, nothing changes in your code. The 'use client' and 'use server' directives work exactly as before. The difference is downstream — library authors can now build RSC-native packages against a stable API. See best React 20 Server Component libraries for the ecosystem that's emerged.
The use() Hook
use() was introduced in React 19 as an experimental way to read promises and context inside components. React 20 promotes it to stable. The hook takes a promise or context and returns the resolved value, integrating with Suspense for loading states.
import { use, Suspense } from 'react';
async function fetchUser(id: string) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// Parent creates the promise, child reads it
export default function Page({ params }: { params: { id: string } }) {
const userPromise = fetchUser(params.id);
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
The key difference from useEffect + useState data fetching: use() works with Suspense boundaries. The component suspends while the promise is pending, and the nearest <Suspense> boundary shows the fallback. No loading state management, no race conditions, no effect cleanup.
use() also replaces useContext for reading context values, with one advantage: it works inside conditionals and loops, unlike useContext which must follow the rules of hooks.
function ThemeAwareButton({ showIcon }: { showIcon: boolean }) {
// use() can be called conditionally — useContext cannot
if (showIcon) {
const theme = use(ThemeContext);
return <button style={{ color: theme.primary }}>★ Click</button>;
}
return <button>Click</button>;
}
View Transitions
React 20 ships a built-in <ViewTransition> component that wraps the browser's View Transitions API. This enables animated transitions between route changes and state updates without third-party animation libraries.
import { ViewTransition } from 'react';
function ProductCard({ product }: { product: Product }) {
return (
<ViewTransition name={`product-${product.id}`}>
<Link href={`/products/${product.id}`}>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
</Link>
</ViewTransition>
);
}
The name prop establishes a shared element identity. When navigating from a product card to a product detail page, React automatically creates a morphing animation between the two elements with the same name. The transition uses CSS view-transition-name under the hood, so you can customize animations with CSS:
::view-transition-old(product-*) {
animation: fade-out 200ms ease-out;
}
::view-transition-new(product-*) {
animation: fade-in 200ms ease-in;
}
This works with React's concurrent rendering — transitions are batched with state updates and don't block interaction. Framework integration (Next.js, Remix) is automatic for route transitions.
Breaking Changes: What Was Removed
React 20 removes patterns that have been deprecated for multiple major versions. These are the changes most likely to require migration work.
defaultProps on Function Components
Function component defaultProps were deprecated in React 18.3 and removed in React 20. Use JavaScript default parameters instead.
// Before (breaks in React 20)
function Button({ variant, size }) { /* ... */ }
Button.defaultProps = { variant: 'primary', size: 'md' };
// After
function Button({ variant = 'primary', size = 'md' }) { /* ... */ }
This is the most common migration task. Search your codebase for .defaultProps and replace each one with default parameter values in the function signature.
String Refs
String refs (ref="myRef") were deprecated in React 16.3. In React 20, they throw a runtime error. Use useRef or callback refs.
// Before (breaks in React 20)
class MyComponent extends React.Component {
componentDidMount() {
this.refs.myInput.focus();
}
render() {
return <input ref="myInput" />;
}
}
// After
function MyComponent() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { inputRef.current?.focus(); }, []);
return <input ref={inputRef} />;
}
Legacy Context API
The contextTypes / childContextTypes / getChildContext API, deprecated since React 16.3, is removed. Use createContext and useContext (or use() in React 20).
react-dom/test-utils
act() moved to the react package in React 18. The react-dom/test-utils module is removed in React 20. Update your test imports:
// Before
import { act } from 'react-dom/test-utils';
// After
import { act } from 'react';
Migration Steps
Step 1: Update dependencies.
npm install react@20 react-dom@20 @types/react@20 @types/react-dom@20
Step 2: Run the React codemod. The React team provides an automated codemod for the most common migrations:
npx @react-codemod/cli react-20
This handles defaultProps → default parameters, string ref → useRef, and react-dom/test-utils → react import changes.
Step 3: Fix TypeScript errors. React 20's type definitions are stricter:
ReactNodeno longer includes{}(empty object)refis a regular prop (noforwardRefneeded — this was possible in React 19 but now the types enforce it)useRef(null)requires an explicit type parameter
Step 4: Test. Focus on components that used any removed API. The React DevTools Profiler's "Why did this render?" feature helps catch unexpected re-renders from the upgrade.
For projects using React 19's experimental RSC features, the migration is a version bump — no API changes required. For projects on React 18, consider upgrading to React 19 first and then to 20, as the React 18 → 19 migration has more breaking changes than 19 → 20.
What's Next
React 20's stability story makes it safe to adopt RSC and use() in production without worrying about API churn. The React compiler (originally "React Forget") continues to mature as an opt-in optimization pass, and React Native's new architecture is fully compatible with React 20.
The most impactful change isn't a new feature — it's the removal of legacy patterns. React 20 is the first version where modern React (hooks, concurrent features, server components) is the only React. That simplification benefits everyone building on the platform.