# React 20 Just Changed Everything — Here's What MERN Devs Need to Know

* * *

There's a version of React where modern patterns aren't just the "recommended" way — they're the *only* way. React 20 is that version, and if you're still carrying around `defaultProps`, string refs, or the old Context API, the upgrade is going to be a bit of a reckoning. But it's also one of the cleanest, most confident leaps the framework has made in years.

Here's what changed, what broke, and how to think about it if you're building MERN apps.

* * *

## The `use()` Hook Is Finally Stable

The `use()` hook has been floating around in experimental land for a while, but React 20 makes it official. The short version: it lets you unwrap a Promise or a Context value — and unlike `useContext`, you can call it *conditionally*.

This changes how you handle async data in a pretty meaningful way:

```jsx
// Before — useContext had to be called unconditionally at the top level
function UserCard({ isAdmin }) {
  const user = useContext(UserContext); // always runs, even if we bail early

  if (!isAdmin) return null;
  return <div>{user.name}</div>;
}

// After — use() is fine inside conditions
function UserCard({ isAdmin }) {
  if (!isAdmin) return null;

  const user = use(UserContext); // works here, no rules broken
  return <div>{user.name}</div>;
}
```

It also works with Promises, which pairs beautifully with Suspense:

```jsx
async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

function UserProfile({ userPromise }) {
  const user = use(userPromise); // suspends until resolved, then renders
  return <h2>Welcome back, {user.name}</h2>;
}

// In a parent component:
<Suspense fallback={<Spinner />}>
  <UserProfile userPromise={fetchUser(42)} />
</Suspense>
```

If you're building MERN apps where your Express API feeds data to React components, this pattern becomes a natural fit — pass the promise down, let `use()` handle the suspension. It's cleaner than juggling `useEffect` + `useState` for every data fetch.

![](https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/5e3418b2-83a2-4326-9afb-3cedf9397132.png align="center")

* * *

## Server Components Are No Longer Experimental

This is the big one. React Server Components (RSC) are now stable in React core — not "available if you're using Next.js", not "opt-in experimental". Actually stable.

What this means in practice: framework authors can now build RSC-native packages without pinning to experimental React builds. If you've been avoiding RSC-based architecture because the ground felt shaky, the ground has finally settled.

For MERN developers this matters because you've been weighing whether to commit to RSC at all. Here's the mental model in code:

```jsx
// Server Component — runs on the server only, never ships JS to the browser
// No "use client" directive — this file never reaches the client bundle

import { db } from "@/lib/db"; // direct MongoDB/Mongoose call, no API roundtrip

export async function ProductList() {
  const products = await db.collection("products").find({}).toArray();

  return (
    <ul>
      {products.map((p) => (
        <li key={p._id}>{p.name} — ${p.price}</li>
      ))}
    </ul>
  );
}
```

```jsx
// Client Component — needs interactivity, so it ships to the browser
"use client";

export function AddToCartButton({ productId }) {
  const [added, setAdded] = useState(false);

  async function handleAdd() {
    await fetch(`/api/cart`, {
      method: "POST",
      body: JSON.stringify({ productId }),
    });
    setAdded(true);
  }

  return (
    <button onClick={handleAdd}>
      {added ? "✓ Added to Cart" : "Add to Cart"}
    </button>
  );
}
```

Your Express backend doesn't disappear — it shifts. Server components can talk to MongoDB directly (in an RSC-aware framework), while your Express API becomes the layer consumed by client components that need mutation endpoints. The net result is less fetch boilerplate overall.

* * *

## View Transitions Are Built In

Smooth route transitions used to mean reaching for Framer Motion or writing a pile of CSS animation hacks. React 20 ships a native `<ViewTransition>` component that hooks directly into the browser's View Transitions API.

```jsx
import { ViewTransition } from "react";
import { useNavigate } from "react-router-dom";

function ProductCard({ product }) {
  const navigate = useNavigate();

  return (
    <ViewTransition>
      <div
        className="card"
        onClick={() => navigate(`/products/${product.id}`)}
        style={{ viewTransitionName: `product-${product.id}` }}
      >
        <img src={product.image} alt={product.name} />
        <h3>{product.name}</h3>
        <p>${product.price}</p>
      </div>
    </ViewTransition>
  );
}

// On the detail page, give the hero image the same viewTransitionName
function ProductDetail({ product }) {
  return (
    <ViewTransition>
      <img
        src={product.image}
        alt={product.name}
        style={{ viewTransitionName: `product-${product.id}` }}
      />
      <h1>{product.name}</h1>
    </ViewTransition>
  );
}
```

When a user clicks from the list to the detail page, the card's image morphs smoothly into the hero — the browser interpolates the transition, React handles the timing. No third-party library required for this kind of thing anymore.

Framer Motion still has its place for complex sequence animations, but for the common "shared element transition between routes" case, you no longer need it.

* * *

## What Got Removed (Read This Part Carefully)

React 20 is the first version that doesn't try to stay backward-compatible with pre-hooks patterns. The things that had been deprecated for years are now gone for real.

`defaultProps` **on function components:**

```jsx
// ❌ Breaks in React 20
function Greeting({ name }) {
  return <h1>Hello, {name}</h1>;
}
Greeting.defaultProps = { name: "World" };

// ✅ Use default parameters instead
function Greeting({ name = "World" }) {
  return <h1>Hello, {name}</h1>;
}
```

**String refs:**

```jsx
// ❌ String refs are gone
class OldInput extends React.Component {
  handleFocus() {
    this.refs.myInput.focus(); // broken
  }
  render() {
    return <input ref="myInput" />;
  }
}

// ✅ useRef or callback refs
function ModernInput() {
  const inputRef = useRef(null);
  return <input ref={inputRef} />;
}
```

**Legacy Context API:**

```jsx
// ❌ childContextTypes / getChildContext is gone
// ✅ Migrate to createContext if you haven't already

const ThemeContext = createContext("light");

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ThemedButton />
    </ThemeContext.Provider>
  );
}
```

`react-dom/test-utils`**:**

The module is removed outright. If you're importing from it directly, switch to `@testing-library/react` — which wraps the same functionality and is what most people should've been using anyway.

* * *

## Should You Upgrade Right Now?

If your app is fully hooks-based and you haven't touched the legacy APIs in a while, the upgrade is probably smooth. Run your test suite, address the deprecation warnings React was already throwing, and you're likely done.

If you have older class components or inherited code, do an audit before upgrading:

*   Search for `defaultProps` attached to function components (class components still support it)
    
*   Search for `this.refs` anywhere
    
*   Check for `getChildContext` or `childContextTypes`
    
*   Search for direct imports from `react-dom/test-utils`
    

The React team ships codemods for some of these transforms — worth running before a manual pass. And if you've been holding off on a codebase modernization, this is as good a forcing function as you're going to get.

* * *

## The Bottom Line

React 20 isn't trying to add ten new things. It's cleaning house — shipping what was overdue for stability (`use()`, RSC) and retiring what was overdue for removal (the legacy patterns). The View Transitions integration is a bonus that'll save you from reaching for animation libraries in a lot of common use cases.

If you've been waiting for RSC to "settle down" before committing to it in your MERN architecture — it's settled. Time to revisit that decision.

What part of React 20 are you most interested in? Drop it in the comments — happy to dig into RSC patterns for Express-backed MERN apps in a follow-up.
