React 19 shipped in December 2024. Within two weeks, the ecosystem fragmentation was visible: packages marked themselves as “React 19 compatible,” blog posts documented breaking changes, and half the tutorials on the first page of Google pointed to APIs that no longer existed.

The frustration was legitimate. The changes were also, mostly, correct.

Here is what React 19 actually changed, what it broke, and whether any of it was worth it.

What Actually Shipped

Server Components Are Stable

React Server Components (RSCs) were experimental in React 18. In React 19 they’re stable and the framework story is cleaner. Components marked async or without 'use client' run on the server - no client-side JavaScript, full access to databases and file systems, no serialization overhead for the rendered output.

The mental model shift is real but worth it. A component like this:

// This runs on the server - no useEffect, no loading state
async function ProductPage({ id }: { id: string }) {
  const product = await db.products.findById(id);
  return <ProductView product={product} />;
}

No useState for loading, no useEffect to fetch, no API route to write. The component is just a function that returns UI. The database call happens at render time on the server.

The performance implications are significant. Pages that previously required three round trips (page load, then API call, then render) now complete in a single server render. First Contentful Paint improves measurably.

Actions

The useActionState and useFormStatus hooks formalize a pattern that every React app was implementing manually. Form submission with pending state, error handling, and optimistic updates used to require 40+ lines of useState, useReducer, and effect logic. Now:

async function submitForm(prevState: State, formData: FormData) {
  const result = await saveToDatabase(formData.get('email'));
  if (!result.ok) return { error: result.error };
  return { success: true };
}

function ContactForm() {
  const [state, action, isPending] = useActionState(submitForm, null);
  return (
    <form action={action}>
      <input name="email" type="email" />
      <button disabled={isPending}>Submit</button>
      {state?.error && <p>{state.error}</p>}
    </form>
  );
}

The isPending state is automatic. The error state is handled by return value. Server actions work here too - submitForm can be an async server function.

The React Compiler (Opt-In)

The React Compiler (formerly React Forget) is the part that generated the most discussion. It automatically memoizes components and values without useMemo, useCallback, or React.memo. Code that previously needed:

const expensiveValue = useMemo(() => compute(props.data), [props.data]);
const handleClick = useCallback(() => doSomething(id), [id]);

With the compiler enabled, you write:

const expensiveValue = compute(props.data);
const handleClick = () => doSomething(id);

And the compiler figures out the memoization. In practice: it works about 80% of the time without changes. The remaining 20% - components with impure renders, refs that change imperatively, patterns that violate React’s rules - require cleanup before the compiler can optimize them.

What Broke

The breakage was real. Significant libraries hadn’t updated:

  • react-router v6 required updates (v7 ships RSC support)
  • Many CSS-in-JS libraries broke: styled-components, Emotion, older versions of MUI
  • Third-party hook libraries that relied on internal React APIs changed
  • Testing patterns using act() required updates

The CSS-in-JS breakage was the most painful. The mechanism that server components use to stream HTML is incompatible with how many CSS-in-JS libraries inject styles. The community response was to migrate to CSS Modules, Tailwind, or the newer CSS-in-JS libraries (Panda CSS, vanilla-extract) that generate static CSS at build time instead of injecting at runtime.

The Migration Reality

For a mid-size application with 100,000 lines of React code:

Migration Task Effort
React 18 to 19 core upgrade 2-4 hours
Fix prop-types warnings (removed from React) 4-8 hours
Update CSS-in-JS to compatible library 1-3 weeks
Adopt Server Components for data fetching 2-4 weeks
Add Actions for forms 1 week
Enable React Compiler 1-2 weeks

The core upgrade is fast. The ecosystem cleanup is where time goes. Most teams will spend 2-6 weeks on a full migration depending on how much CSS-in-JS they use and how much they want to adopt RSCs versus keeping their existing data fetching patterns.

Was It Worth It?

For new projects: yes, unconditionally. Server Components and Actions are genuinely better abstractions. You write less code, ship faster pages, and the mental model is simpler once you internalize server vs. client component boundaries.

For existing applications: depends on what you’re using. If you’re heavily on styled-components or Emotion and your app is working fine, the migration cost is high for moderate benefit. If you have performance problems with data fetching or lots of manual memoization that’s hard to maintain, the new features solve real problems.

Bottom Line

React 19 was painful for the ecosystem because it required significant changes to patterns that had been stable for years. The pain was front-loaded and the bugs were real. But the features - Server Components, Actions, and the Compiler - represent a genuine evolution in how React applications should be built. The ecosystem is largely caught up now. For new projects, React 19 with Next.js 15 or Remix is the best version of React there has ever been.