React 19 in Practice: Concurrent Features, Actions, and the New Hooks

前端工程

Why React 19 Is Worth the Upgrade

React 19 formalizes years of experimental concurrent features into stable APIs and closes the long-standing gap around "forms / data mutations." The core idea: wrap writes in Actions and reads in concurrent primitives, so the UI never freezes while waiting.

Capability React 18 React 19
Server Components experimental (via frameworks) stable, document-level feature
Form submission hand-rolled useState + onSubmit useActionState / <form action>
Reading Promise / Context use unavailable use() reads Promise/Context in render
ref only useRef + forwardRef passed directly as a prop
Optimistic update hand-rolled state toggling useOptimistic
Concurrent transition useTransition useTransition + enhanced useDeferredValue

Actions: First-Class Writes

An Action is a function React can "wait for." Combined with <form action> or useActionState, React manages the pending state and disables the submit button automatically.

"use client";
import { useActionState } from "react";

async function subscribe(prevState: { ok: boolean; msg: string }, formData: FormData) {
  const email = formData.get("email");
  const res = await fetch("/api/subscribe", {
    method: "POST",
    body: JSON.stringify({ email }),
  });
  if (!res.ok) return { ok: false, msg: "Subscription failed, please retry" };
  return { ok: true, msg: "Subscribed!" };
}

export function SubscribeForm() {
  const [state, formAction, pending] = useActionState(subscribe, {
    ok: false,
    msg: "",
  });

  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <button disabled={pending}>{pending ? "Submitting…" : "Subscribe"}</button>
      <p>{state.msg}</p>
    </form>
  );
}

useFormStatus lets a child component read the parent form's status without prop drilling:

import { useFormStatus } from "react-dom";

function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? "Saving…" : "Save"}</button>;
}

Organize config data (e.g. form defaults returned by an API) with the JSON Formatter tool before feeding it into initialState to avoid hand-writing large objects.


use: Read Promises and Context in Render

use() is React 19's "read during render" primitive: it can directly await a Promise or read a Context, and it suspends at the nearest Suspense boundary.

import { use } from "react";

function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
  const comments = use(commentsPromise); // suspends until resolved
  return (
    <ul>
      {comments.map((c) => (
        <li key={c.id}>{c.body}</li>
      ))}
    </ul>
  );
}

Caveat: use() must be called during the render phase of a component or Hook, not inside conditional branches outside try/catch — it is not an ordinary function but a signal to React that "this may need to wait."


Concurrent Rendering: Stay Responsive While Waiting

useTransition: mark "slow updates" as non-urgent

import { useTransition, useState } from "react";

function Search() {
  const [query, setQuery] = useState("");
  const [list, setList] = useState<Item[]>([]);
  const [isPending, startTransition] = useTransition();

  function onChange(e: React.ChangeEvent<HTMLInputElement>) {
    const value = e.target.value;
    setQuery(value); // input responds instantly
    startTransition(() => {
      setList(filterExpensive(value)); // heavy compute wrapped in transition
    });
  }

  return (
    <div>
      <input value={query} onChange={onChange} />
      <ul style={{ opacity: isPending ? 0.6 : 1 }}>
        {list.map((i) => (
          <li key={i.id}>{i.name}</li>
        ))}
      </ul>
    </div>
  );
}

useDeferredValue: automatically "defer" a value

When input and rendering are decoupled, useDeferredValue is simpler:

const deferred = useDeferredValue(query);
const results = useMemo(() => filterExpensive(deferred), [deferred]);

Difference: useTransition wraps "a span of state updates"; useDeferredValue derives "a deferred value," usually with useMemo.


useOptimistic: Optimistic Updates Out of the Box

import { useOptimistic, useRef } from "react";

type Msg = { id: number; text: string; sending?: boolean };

function Chat() {
  const [messages, setMessages] = useOptimistic<Msg[]>([]);

  async function send(formData: FormData) {
    const text = formData.get("text") as string;
    setMessages((list) => [...list, { id: Date.now(), text, sending: true }]);
    await fetch("/api/chat", { method: "POST", body: JSON.stringify({ text }) });
  }

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

For theme colors and button contrast, verify UI accessibility (WCAG) with the Color Converter and Color Contrast tools.


ref as a Prop and Document Metadata

ref is no longer special

function MyInput({ ref }: { ref: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} />;
}
// usage: <MyInput ref={myRef} /> — no forwardRef needed

Native document metadata

import { Title, Meta } from "react-dom";

function Page() {
  return (
    <>
      <Title>Page Title</Title>
      <Meta name="description" content="Page description" />
    </>
  );
}

Smooth Migration from React 18

  1. Upgrade deps: npm i react@19 react-dom@19 and upgrade @types/react.
  2. Replace forwardRef: just put ref in props; the old form is deprecated but still works for now.
  3. Adopt Actions: migrate "submit then setState + loading" forms to useActionState.
  4. Use use for async: pass data Promises to children under Server Components and read with use().
  5. Add concurrency: use useDeferredValue or useTransition for list/search interactions.

FAQ

Q1: Can Actions only be used in Server Components?

No. Client components can also wrap ordinary async functions with useActionState; a Server Action is just one form where the function runs on the server.

Q2: What's the difference between use() and await?

await is a JS runtime concept; use() is a React render concept: it suspends at a Suspense boundary and can be interrupted and re-rendered by concurrent scheduling.

Q3: Which to pick, useTransition or useDeferredValue?

Use useTransition to "defer a whole span of updates"; use useDeferredValue + useMemo to "derive a slow result from a fast-changing value."

Q4: Does React 19 still support IE?

No, and legacy behaviors have been removed. A modern browser environment is required.

Q5: How do I roll back a failed optimistic update?

useOptimistic automatically rolls back to the real state when the Action throws; just ensure the "real data source" behind setMessages is unchanged on failure.


In React 19 development, these tools help:


The essence of React 19 is not "a few more Hooks," but a unified mental model that governs writes (Actions) and reads (concurrency/use): the UI stays responsive while waiting for data and self-manages state while submitting changes. Master Actions plus concurrent primitives, and you hold the key to modern React.

Try these browser-local tools — no sign-up required →

#React#React 19#并发渲染#前端#useTransition#Server Components#Actions