Home / Articles / React 19 Upgrade Triage: Which New APIs Replace Workarounds You Have

This article is published in English.

React 19 Upgrade Triage: Which New APIs Replace Workarounds You Have

A practical look at use, Server Actions, useOptimistic and the React Compiler, with the caveats that matter and a plan for which React 19 changes to adopt first.

1579 words

React 19 added more API surface than any release in years, and the flood of coverage makes it hard to see what matters for an existing codebase. For most teams, a few additions replace workarounds you already maintain, one changes how you think about data loading, and the rest can wait. Below are the ones that matter, with before-and-after code and the caveats that are easy to miss, so you can plan the upgrade by value.

use: reading Promises and Context during render

The use API is the most significant addition, and it behaves differently from every hook you know. Regular hooks must be called at the top level of a component, in the same order on every render. use is exempt from that rule: you can call it after an early return, inside a condition or inside a loop.

In the example below, the component returns a guest view when there is no userId, and only then reads the user:

// React 19 — use() can be called inside conditionals and loops
function UserProfile({ userId }) {
  if (!userId) return <GuestView />;

  // This is valid in React 19
  const user = use(fetchUser(userId));

  return <div>{user.name}</div>;
}

The real power is in what use accepts: a Promise or a Context. Given a Promise, React suspends the component until it settles. The comparison below shows how much disappears. The older version tracks the data and a loading flag in state, fetches in an effect and renders a spinner by hand; the React 19 version reads the value directly:

// Before React 19
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUser(userId).then(data => {
      setUser(data);
      setLoading(false);
    });
  }, [userId]);

  if (loading) return <Spinner />;
  return <div>{user.name}</div>;
}

// React 19
function UserProfile({ userId }) {
  const user = use(fetchUser(userId));
  return <div>{user.name}</div>;
}

The work moved rather than vanished: the nearest <Suspense> boundary shows the fallback while the Promise is pending, and the nearest error boundary handles rejection. The component describes only the success path.

The Promise has to be stable

use expects the same Promise across renders. If fetchUser(userId) creates a new one on every render, React suspends again each time, causing repeated requests or a component that never settles; React warns about uncached Promises created during render in Client Components. So treat the snippets as illustrations of the shape. The Promise should come from something that caches it: a Suspense-aware data library, a framework loader, or a Server Component that starts the request and passes the Promise down as a prop. Wrapping the call in useMemo is sometimes suggested, but React does not guarantee that memoized values are kept, so it is not a reliable cache.

Server Actions as a React feature

Next.js App Router users already know Server Actions. In React 19 they are part of React itself (the docs now call them Server Functions), available in any framework that supports Server Components.

The example defines an async function marked with 'use server', which inserts a user and revalidates the list, and passes it to a form's action prop:

// Server Action — runs on the server, called from the client
async function submitForm(formData) {
  'use server';

  const name = formData.get('name');
  await db.users.create({ name });
  revalidatePath('/users');
}

// Client component
function UserForm() {
  return (
    <form action={submitForm}>
      <input name="name" />
      <button type="submit">Add User</button>
    </form>
  );
}

The action prop of <form> now accepts a function, including an async one; React runs it in a transition on submit and passes it the FormData. Pending state, optimistic updates and errors come from companion APIs: useFormStatus or useActionState for pending and result state, useOptimistic for instant feedback, and error boundaries for failures. You write the mutation; React coordinates the rest.

One detail in the snippet needs adjusting in a real app. A function with an inline 'use server' directive can only be defined in a Server Component. If UserForm is a Client Component, move submitForm into its own file with 'use server' at the top and import it.

Where Server Components removed static UI from the client bundle, Server Actions remove the hand-written API route for mutations: less code, fewer round trips. Still treat each action as a public endpoint and validate input and authorization inside it.

useOptimistic: instant feedback without duplicate state

Rendering an outcome ahead of server confirmation once meant juggling extra state by hand. useOptimistic takes the current state and a reducer-like update function, and returns the state to render plus a function that applies an optimistic change. Here, adding a todo inserts a temporary item marked as pending, which renders slightly faded until the save finishes:

function TodoList({ todos }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (currentTodos, newTodo) => [...currentTodos, newTodo]
  );

  async function handleAdd(text) {
    addOptimisticTodo({ id: 'temp', text, pending: true });
    await saveTodo(text); // Server Action
  }

  return (
    <ul>
      {optimisticTodos.map(todo => (
        <li key={todo.id} style={{ opacity: todo.pending ? 0.7 : 1 }}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

When you call addOptimisticTodo, React shows the optimistic list immediately. When the surrounding action finishes, the optimistic value is dropped and React renders from the todos prop again, which by then should contain the saved item.

Previously you kept confirmed and pending copies of the data and cleaned up by hand when they diverged; now the hook owns that lifecycle. Two conditions matter. First, the optimistic update must happen inside an action or transition, for example a function passed to a form's action prop or wrapped in startTransition; calling it from a plain event handler makes React warn and the update may not display. Second, the parent must actually receive fresh todos after the save, typically through revalidation, or the new item will disappear when the optimistic state resets. Temporary ids such as 'temp' also collide if two items are added quickly, so generate a unique one. We cover these edge cases in five useOptimistic rollback failure modes.

The React Compiler: memoization by default

The React Compiler, formerly React Forget, arrived alongside React 19 as an opt-in build step, and it has the longest-lasting effect on everyday code. It inserts memoization at build time, so values and callbacks are reused when inputs are unchanged and children skip re-renders when props are the same. Manual useMemo, useCallback and React.memo become mostly unnecessary, as the comparison shows:

// Before: manual memoization required
const expensiveValue = useMemo(() => compute(a, b), [a, b]);
const stableCallback = useCallback(() => doSomething(id), [id]);
const MemoizedChild = React.memo(ChildComponent);

// After React Compiler: write normal code
const expensiveValue = compute(a, b);
const handleClick = () => doSomething(id);
// ChildComponent renders only when its props change - automatically

You still need to understand when and why components re-render, but the manual escape hatches matter far less. Our overview of patterns that trigger unnecessary re-renders is still useful background.

For new projects the compiler is a sound default. In existing code, roll it out carefully: it assumes components are pure and follow the Rules of React, so code that mutates during render can behave differently once compiled. Enable it gradually, run your tests, and use the ESLint plugin to find violations first. Check the current React documentation for its release status and supported React versions.

What to adopt, and in what order

Immediate value, low risk

  • useOptimistic wherever forms interact with server state.
  • Server Actions if you are on Next.js 14 or later and want to replace API routes for mutations.

Worth planning for

  • use once your data layer produces stable, cached Promises; it fits naturally with Suspense.
  • The React Compiler on new projects, or on well-tested parts of an existing app first.

Not urgent for most apps

  • The ref changes: ref can now be passed as a regular prop to function components, so forwardRef is no longer needed.
  • Context improvements, which are mostly developer-experience changes rather than new behavior.
  • Document metadata support, which lets components render <title> and <meta> tags that React places in the document head.

Key takeaways

  • React 19 is an evolution rather than a breaking rewrite. Its new APIs formalize patterns that production teams already build by hand: optimistic updates, server mutations and conditional data reading.
  • use moves loading and error handling to Suspense and error boundaries, but only works well with Promises that are cached outside the render.
  • Server Actions and useOptimistic pair naturally; the first handles the write, the second hides its latency.
  • The compiler makes memoization the default, provided your components follow the Rules of React.

The useful question is not whether to upgrade, but which of these primitives replaces a workaround you are maintaining right now. Start there.