JavaScript 7 min read 3,090 views

React 19 in 2026: The Compiler, Actions and What Changed

React Compiler reached stable 1.0, so manual memoization is largely over. A practical guide to the compiler, Actions, the 19.2 additions, and what the compiler still cannot fix.

React development

The most consequential thing in React right now is not a new hook. It is that the React Compiler reached stable 1.0, which means the memoization work you have been doing by hand for years is largely obsolete.

This guide covers what that actually changes, plus Actions and the React 19.2 additions. Versions at the time of writing: React 19.2 with React Compiler 1.0.

The compiler, and what it removes

React re-renders a component when its state or props change, and re-runs everything inside. The traditional fix was to wrap things by hand:

// The old way — and there is a lot of it in most codebases
const visibleItems = useMemo(
  () => items.filter(i => i.status === filter),
  [items, filter]
)

const handleSelect = useCallback(
  (id) => setSelected(id),
  []
)

const Row = memo(function Row({ item, onSelect }) {
  return <li onClick={() => onSelect(item.id)}>{item.name}</li>
})

The compiler does this automatically, at build time, by analysing what actually depends on what:

// With the compiler enabled — same performance, no ceremony
const visibleItems = items.filter(i => i.status === filter)

function handleSelect(id) {
  setSelected(id)
}

function Row({ item, onSelect }) {
  return <li onClick={() => onSelect(item.id)}>{item.name}</li>
}

It also does something you cannot do manually: memoize conditionally, and at a finer granularity than whole values. Hooks cannot be called conditionally, so hand-written memoization is always coarser than optimal.

Enabling it

npm i -D babel-plugin-react-compiler eslint-plugin-react-hooks
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [
    react({
      babel: { plugins: [['babel-plugin-react-compiler', {}]] },
    }),
  ],
})

Run the lint rules before you enable it. The compiler only optimises components it can prove follow the rules of React, and the linter tells you which ones do not — mutating props, reading refs during render, conditional hooks. Those components are skipped silently rather than breaking, so without the linter you get partial coverage and no explanation.

Should you delete your existing useMemo calls?

Not urgently, and not all at once.

Existing useMemo and useCallback calls keep working — the compiler does not conflict with them. So there is no forced migration.

The practical approach: stop writing new ones, and remove old ones when you are already editing that file. A mass deletion across the codebase is a large diff with real regression risk and no user-visible benefit, which is a poor trade.

The exception worth keeping: useMemo around something genuinely expensive that is not about render identity — a heavy computation you want cached across renders for its own sake. That is a different concern from memoizing for referential stability.

Actions

Actions handle the pattern every form has: pending state, errors, and optimistic updates. Previously that was four useState calls and a try/catch.

import { useActionState } from 'react'

function UpdateProfile({ user }) {
  const [state, formAction, isPending] = useActionState(
    async (previousState, formData) => {
      try {
        await saveProfile({ name: formData.get('name') })
        return { ok: true, error: null }
      } catch (e) {
        return { ok: false, error: e.message }
      }
    },
    { ok: false, error: null }
  )

  return (
    <form action={formAction}>
      <input name="name" defaultValue={user.name} />
      <button disabled={isPending}>
        {isPending ? 'Saving…' : 'Save'}
      </button>
      {state.error && <p role="alert">{state.error}</p>}
    </form>
  )
}

isPending comes free, and because this is a real form action, submission works before hydration completes.

Optimistic updates

import { useOptimistic } from 'react'

function Comments({ comments, addComment }) {
  const [optimistic, addOptimistic] = useOptimistic(
    comments,
    (state, newComment) => [...state, { ...newComment, pending: true }]
  )

  async function action(formData) {
    const text = formData.get('text')
    addOptimistic({ id: 'temp', text })   // shows immediately
    await addComment(text)                 // reverts automatically on failure
  }

  return (
    <>
      {optimistic.map(c => (
        <p key={c.id} style={{ opacity: c.pending ? 0.5 : 1 }}>{c.text}</p>
      ))}
      <form action={action}>
        <input name="text" />
        <button>Post</button>
      </form>
    </>
  )
}

The automatic revert is the valuable part. Hand-rolled optimistic updates usually get the happy path right and the rollback wrong.

What React 19.2 added

FeatureWhat it is for
<Activity>Hide a subtree while preserving its state — tabs, wizards, back navigation
useEffectEventRead the latest value in an effect without adding it to the dependency array
cacheSignalCancel work when a cached request is no longer needed
Performance TracksReact-specific timings in browser devtools
Partial pre-renderingServe a static shell, stream the rest
Batched Suspense revealsFewer layout jumps during server rendering

useEffectEvent is the one that solves a genuinely common annoyance — the effect that should not re-run, but has to list a value it reads:

import { useEffectEvent } from 'react'

function ChatRoom({ roomId, theme }) {
  // Reads the current theme without making the effect depend on it
  const onConnected = useEffectEvent(() => {
    showNotification('Connected', theme)
  })

  useEffect(() => {
    const conn = createConnection(roomId)
    conn.on('connected', onConnected)
    conn.connect()
    return () => conn.disconnect()
  }, [roomId])   // theme is not here — and does not need to be
}

Before this, changing the theme reconnected the chat room. The usual workarounds were a ref or lying to the dependency array; both were worse.

What the compiler cannot fix

Worth being clear, because "automatic optimization" invites the wrong expectation:

  • Rendering too much at once. Ten thousand rows are slow whether or not they are memoized. Virtualise the list.
  • Genuinely expensive computation. The compiler avoids repeating work; it does not make the work cheaper.
  • Network waterfalls. Sequential requests are an architecture problem, not a rendering one.
  • Oversized bundles. Nothing about memoization reduces what you ship.
  • Components that break the rules of React. These are skipped entirely — which is exactly why you run the linter.

The compiler removes a class of tedious work. It does not remove the need to understand why something is slow.

A sensible order

  1. Add the ESLint plugin and fix what it reports. Useful on its own.
  2. Enable the compiler and verify in React DevTools that components show as optimised.
  3. Stop writing new useMemo and useCallback.
  4. Adopt Actions for forms as you touch them.
  5. Remove old memoization opportunistically, never as a sweep.

Frequently asked questions

Is React Compiler production ready?

Yes. It reached stable 1.0 in October 2025 after an extended beta and heavy use in production at Meta. It works with React 19 and React Native.

Do I still need useMemo and useCallback in React 19?

Rarely, once the compiler is enabled — it memoizes automatically, and more precisely than hooks allow, because it can memoize conditionally. Keep useMemo only for genuinely expensive computation you want cached for its own sake.

Should I remove all my existing useMemo calls?

Not in one sweep. They still work and do not conflict with the compiler. Stop adding new ones and remove old ones when you are editing that file anyway — a mass deletion is a large diff with regression risk and no visible benefit.

Why is the compiler skipping some of my components?

It only optimises components it can prove follow the rules of React. Mutating props, reading refs during render or calling hooks conditionally will cause it to skip a component silently. The ESLint plugin tells you which ones and why.

What is useActionState?

A hook that wraps an async form handler and gives you the result, a form action and a pending flag in one call, replacing the usual cluster of loading and error state. Because it attaches to a real form action, it also works before hydration.

What is useEffectEvent for?

Reading the latest value of something inside an effect without adding it to the dependency array. It solves the case where an effect re-runs for a value it merely reads — such as reconnecting a socket because a theme changed.

What is the Activity component?

A way to hide part of the tree while preserving its state, so a tab or wizard step keeps its scroll position and form input when you return to it, rather than being unmounted and rebuilt.

Does the compiler make my app faster automatically?

It removes unnecessary re-renders, which helps when that is your bottleneck. It does nothing for oversized lists, expensive computation, network waterfalls or large bundles — diagnose the actual cause before expecting it to help.

Share this article:
ES
Written by

Edrees Salih

Full-stack software engineer with 9 years of experience. Passionate about building scalable solutions and sharing knowledge with the developer community.

View Profile

Comments (0)

Leave a Comment

Your email will not be published.

No comments yet. Be the first to share your thoughts!

Related Articles

Related Articles

Need Help With Your Project?

Book a free 30-minute consultation to discuss your technical challenges and explore solutions together.