Skip to content

React 19 fills in gaps that application code used to handle manually: asynchronous form Actions, optimistic updates, reading promises with use, and less ceremony around refs, context, and document metadata.

React 19.2 adds Activity, useEffectEvent, and better performance tooling. Treat upgrade and feature adoption as separate steps.

React Compiler is a separate, stable, optional build-time optimizer. See React Compiler Internals for its HIR-to-cache pipeline and current adoption guidance.



1. Actions

An Action is a function that runs inside a Transition. It can perform asynchronous work while React manages the pending state, errors, and the final state update around it.


tsx
function UpdateName() {
  async function updateName(formData: FormData) {
    await saveName(String(formData.get("name") ?? ""))
  }

  return (
    <form action={updateName}>
      <input name="name" />
      <SubmitButton />
    </form>
  )
}

  • The most visible use is the action prop on <form>. Pass a function instead of intercepting onSubmit, reading the form, tracking a loading flag, and resetting by hand.
  • When the Action succeeds, React automatically resets uncontrolled fields. The same function can be assigned to a button's formAction when submit buttons need different behavior.
  • Actions are not limited to forms, but the form integration is where they remove the most routine state management.

2. useActionState and useFormStatus

useActionState connects an Action to state produced by its previous result. It returns the current state, a wrapped Action, and an isPending value.


tsx
type FormState = { message: string; ok: boolean }

async function saveEmail(
  previousState: FormState,
  formData: FormData
): Promise<FormState> {
  const email = String(formData.get("email") ?? "")
  if (!email.includes("@")) return { message: "Enter a valid email.", ok: false }
  await subscribe(email)
  return { message: "You are subscribed.", ok: true }
}

function SubmitButton() {
  const { pending } = useFormStatus()
  return <button type="submit" disabled={pending}>{pending ? "Subscribing..." : "Subscribe"}</button>
}

function NewsletterForm() {
  const [state, formAction] = useActionState(saveEmail, { message: "", ok: false })
  return (
    <form action={formAction}>
      <input name="email" type="email" />
      <SubmitButton />
      <p aria-live="polite">{state.message}</p>
    </form>
  )
}

  • The Action receives the previous state before its usual arguments. For a form Action, the second argument is the submitted FormData.
  • useActionState fits when the server result should become UI state. useFormStatus fits controls that only need to know whether the surrounding form is pending.
  • Failure: calling useFormStatus in the same component that creates the form. It reads the parent form, so it must be called from a child rendered inside that form.

3. useOptimistic

useOptimistic lets the UI show the expected result immediately while an Action is running, then reconcile with the real state when it completes.


tsx
type Message = { id: string; text: string; sending?: boolean }

function MessageThread({ initialMessages }: { initialMessages: Message[] }) {
  const [messages, setMessages] = useState(initialMessages)
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (current, text: string) => [...current, { id: crypto.randomUUID(), text, sending: true }]
  )

  async function sendMessage(formData: FormData) {
    const text = String(formData.get("message") ?? "")
    addOptimisticMessage(text)
    const saved = await saveMessage(text)
    setMessages((current) => [...current, saved])
  }

  return (
    <form action={sendMessage}>
      {optimisticMessages.map((m) => (
        <p key={m.id}>{m.text}{m.sending ? " (sending...)" : ""}</p>
      ))}
      <input name="message" />
    </form>
  )
}

  • The optimistic value is temporary. When the Action finishes, React returns to the value supplied by real state or props.
  • The source of truth still has to be updated outside the optimistic reducer.
  • Failure: treating optimistic UI as committed success. Destructive operations, and anything where pretending success could mislead the user, warrant more caution.

4. Reading Resources with use

use reads a resource during rendering — today a Promise or a Context. A pending Promise suspends to the nearest <Suspense> fallback. A rejection goes to the nearest Error Boundary.


tsx
function ProfileCard({ profilePromise }: { profilePromise: Promise<Profile> }) {
  const profile = use(profilePromise)
  return <h2>{profile.name}</h2>
}

function ProfilePage({ profilePromise }: { profilePromise: Promise<Profile> }) {
  return (
    <Suspense fallback={<p>Loading profile...</p>}>
      <ProfileCard profilePromise={profilePromise} />
    </Suspense>
  )
}

  • Unlike Hooks, use can sit inside conditions and loops. It still has to run while React is rendering a component or custom Hook.
  • use can also read Context conditionally — skip use(ThemeContext) on a muted heading, call it on the active one.
  • use is a bridge to a resource that may not be ready, not a replacement for every data-fetching library.
  • Failure: creating a new Promise on every Client Component render. That can suspend repeatedly. The Promise should come from a framework, a cache, or a Server Component so the same resource can be reused.

5. Less Ceremony for Refs and Context

Function components can receive ref as a normal prop in React 19. New components no longer need forwardRef just to expose an element.


tsx
function SearchInput(props: ComponentPropsWithRef<"input">) {
  return <input type="search" {...props} />
}

<canvas
  ref={(node) => {
    if (!node) return
    const observer = new ResizeObserver(() => draw(node))
    observer.observe(node)
    return () => observer.disconnect()
  }}
/>

  • Existing forwardRef components still work. React plans to deprecate forwardRef after migrations have had time to happen.
  • Ref callbacks may return cleanup functions — the ResizeObserver disconnect above.
  • A Context object can be rendered directly: <ThemeContext value="dark"> instead of <ThemeContext.Provider value="dark">.
  • Failure: treating the stricter ref-callback TypeScript as a surprise. Callbacks can now return cleanup, so the types reject accidental returns that used to be ignored.

6. Document Metadata and Resources

React 19 understands <title>, <meta>, and <link> tags rendered inside components. React moves them to the document's <head>, so metadata can live near the route or content that owns it.


tsx
function ProductPage({ product }: { product: Product }) {
  return (
    <>
      <title>{product.name} – Acme Store</title>
      <meta name="description" content={product.summary} />
      <h1>{product.name}</h1>
    </>
  )
}

  • <title>, <meta>, and <link rel="canonical"> can live next to the route that owns them. React hoists them into <head>.
  • Stylesheets can declare a precedence. React waits for the relevant CSS before revealing Suspense content. Async scripts are deduplicated even if several components render the same script.
  • React DOM exposes preconnect, preload, preinit, preloadModule, and preinitModule for resources known before the element that needs them.
  • Frameworks already handle much of this. Check the framework's conventions before calling these APIs directly.

7. Better Custom Elements and Hydration Errors

React 19 has full support for custom elements. During server rendering, primitive props such as strings and numbers become attributes. In the browser, React assigns values as properties when the custom element exposes matching properties.


tsx
function Checkout() {
  return (
    <payment-card
      customer-id="customer_123"
      options={{ appearance: "compact" }}
    />
  )
}

  • Web components are easier to consume without writing React-specific wrappers for every non-string value.
  • Hydration diagnostics are consolidated: one error with a diff showing how the server-rendered HTML differs from the client output.
  • Failure: treating the better message as a repair. Mismatches still come from clocks, random values, browser-only branches, changing external data, or invalid HTML nesting.

8. Activity in React 19.2

Activity lets React keep a section of the interface mounted while controlling whether it is visible and how urgently its updates should be processed.


tsx
function Workspace({ activeTab }: { activeTab: "editor" | "preview" }) {
  return (
    <>
      <Activity mode={activeTab === "editor" ? "visible" : "hidden"}>
        <Editor />
      </Activity>
      <Activity mode={activeTab === "preview" ? "visible" : "hidden"}>
        <Preview />
      </Activity>
    </>
  )
}

  • visible shows the children, mounts their effects, and processes updates normally.
  • hidden hides the children, unmounts their effects, and defers their updates until React has no visible work left.
  • The useful difference from conditional rendering is preserved state. A hidden editor can keep its draft, selection, and component state, while React can prepare a likely next screen in the background.
  • Failure: treating this as CSS display: none. Hidden Activities clean up effects, so subscriptions must be able to stop and restart correctly.

9. useEffectEvent in React 19.2

Effects often mix two ideas: synchronization that should react to a dependency, and event-like logic that should read the latest props without restarting that synchronization. useEffectEvent separates the event-like part.


tsx
function ChatRoom({ roomId, theme }: { roomId: string; theme: "light" | "dark" }) {
  const onConnected = useEffectEvent(() => {
    showNotification("Connected", theme)
  })

  useEffect(() => {
    const connection = createConnection(roomId)
    connection.on("connected", () => onConnected())
    connection.connect()
    return () => connection.disconnect()
  }, [roomId])
}

  • Changing theme no longer reconnects the chat room, but onConnected still reads the latest theme when the connection event fires.
  • Effect Events can only be called from Effects in the same component or custom Hook. They should not appear in dependency arrays.
  • Using this API requires a recent eslint-plugin-react-hooks; the linter understands and enforces those restrictions.
  • Failure: using Effect Events to hide missing dependencies. They should represent logic that is conceptually triggered by an Effect.

10. Performance and Server Improvements in React 19.2

React 19.2 adds React Performance Tracks to Chrome DevTools performance profiles. The Scheduler tracks show update priorities and when work was scheduled, blocked, rendered, or painted. The Components tracks show when components render and when their effects mount.

  • This is a lower-level view than the React DevTools Profiler. It fits when a slow interaction involves scheduling, browser work, and React rendering rather than one obviously expensive component.
  • cacheSignal gives React Server Component work an AbortSignal tied to the lifetime of a cache() entry, so unused requests can be cancelled.
  • Partial pre-rendering APIs let a framework pre-render a static shell, store postponed work, and resume the remaining server render later.
  • Streaming SSR batches nearby Suspense boundary reveals. Node.js gets Web Streams support; the React team still recommends Node Streams APIs in Node because they are faster and work naturally with compression.
  • Most application developers will encounter cacheSignal and partial pre-rendering through a framework.

11. Upgrading to React 19

Upgrade React, React DOM, and their TypeScript definitions together, using the latest patched React 19 release supported by the framework.


bash
npm install react@^19 react-dom@^19
npm install --save-dev @types/react@^19 @types/react-dom@^19

  • The official upgrade guide includes codemods for common migrations.
  • ReactDOM.render / ReactDOM.hydrate → createRoot / hydrateRoot. unmountComponentAtNode → root.unmount().
  • findDOMNode, string refs, and this.refs are removed. Legacy Context APIs and function component propTypes are removed.
  • Ref callback TypeScript rules are stricter because callbacks can now return cleanup functions.
  • Failure: mixing the upgrade with a large rewrite. First make the existing application work on React 19, then introduce Actions, use, or Activity where they simplify a real pattern.

Takeaway

Actions coordinate async mutations with pending and optimistic UI. use coordinates rendering with resources. Document APIs coordinate metadata, styles, and scripts. Activity coordinates visible and background work. useEffectEvent separates synchronization from event-like behavior.

The features that earn a keep most often are form Actions, useActionState, useOptimistic, ref as a prop, and useEffectEvent. use, Activity, and the server rendering APIs depend more on framework support and architecture.

Official notes: React 19 release notes, React 19.2 release notes, and React 19 upgrade guide.


Recap Q&A

Read the next note
TypeScript Beyond Strict