All posts

React 19: What Actually Changed for Developers

Subhan Farrakh · · 2 min read · Updated May 19, 2026

Stop Writing useEffect for Data Mutations

The single most impactful change in React 19 for day-to-day code is Actions. If you've ever written a component that looks like this:

tsx
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

const handleSubmit = async (e) => {
  e.preventDefault();
  setLoading(true);
  try {
    await submitForm(formData);
  } catch (err) {
    setError(err.message);
  } finally {
    setLoading(false);
  }
};

React 19 replaces this entire pattern. Actions are async functions that React tracks automatically:

tsx
const [error, submitAction, isPending] = useActionState(
  async (prevState, formData) => {
    const result = await submitForm(formData);
    if (!result.ok) return result.error;
    return null;
  },
  null
);

The isPending state is managed by React. The transition is batched. The optimistic update pattern is built in via useOptimistic.

The React Compiler (Forget)

React 19 ships with an optional compiler (previously called "React Forget") that automatically memoizes your components. In practice, this means you can stop writing useMemo and useCallback defensively.

Before:

tsx
const expensiveValue = useMemo(() => compute(data), [data]);
const stableHandler = useCallback(() => doSomething(id), [id]);

After (with compiler):

tsx
const expensiveValue = compute(data);
const stableHandler = () => doSomething(id);

The compiler statically analyzes your component tree and inserts memoization where it's beneficial. Not everywhere — that would be wasteful — but specifically where re-renders would be expensive.

The important caveat: the compiler only works on code that follows the Rules of React (no mutations during render, stable hook call order, etc.). If your codebase has workarounds for React's rules, the compiler won't touch those components and will tell you why.

use() — The New Primitive for Promises

The new use() hook lets you read a Promise inside a component and integrates with Suspense:

tsx
function UserProfile({ userPromise }) {
  // This suspends the component until userPromise resolves
  const user = use(userPromise);
  return <h1>{user.name}</h1>;
}

Unlike useEffect, use() can be called conditionally and inside loops — it breaks the hook rules constraint for this specific case because it's not a hook in the traditional sense.

What You Can Stop Doing

With React 19, you can retire several common patterns:

  • forwardRef() — ref is now a regular prop
  • Manual loading state for mutationsuseActionState handles it
  • Defensive useMemo everywhere — the compiler handles it
  • Context value memoization<Context value={...}> is now valid syntax
tsx
// Before
const MyContext = React.createContext();
<MyContext.Provider value={value}>

// After
<MyContext value={value}>

The Real Upgrade Path

React 19 is backward compatible. Existing code doesn't break. The upgrade story is:

  1. Update react and react-dom to 19
  2. Fix any ReactDOM.render calls (use createRoot)
  3. Opt in to the compiler via your bundler plugin
  4. Gradually adopt Actions as you touch existing mutation code

The wins are real and the migration is incremental — that's the best kind of upgrade.