跳到主要内容

React 19 补上了以往应用代码要自己处理的缺口:异步 form Actions、optimistic updates、用 use 读取 promises,以及 refs、context 与 document metadata 周围更少的繁琐写法。

React 19.2 加入 Activity、useEffectEvent,以及更好的性能工具。把升级与功能采用当成分开的步骤。

React Compiler 是一项分开的、stable、optional build-time optimizer。它从 HIR 到 cache 的 pipeline 与当前 adoption guidance,见 React Compiler Internals。



1. Actions

Action 是在 Transition 里执行的函数。它可以做异步工作,同时由 React 管理周围的 pending state、errors,以及最终的 state update。


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

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

  • 最显眼的用途是 <form> 上的 action prop。把函数传进去,而不是拦截 onSubmit、读取表单、追踪 loading flag,再手动重置。
  • 当 Action 成功时,React 会自动重置 uncontrolled fields。同一个函数也可以指定给 button 的 formAction,让不同 submit buttons 有不同行为。
  • Actions 不只限于 forms,但表单整合是它们砍掉最多例行 state management 的地方。

2. useActionState 与 useFormStatus

useActionState 把 Action 接到由其前一次结果产生的 state。它返回 current state、一个包装过的 Action,以及 isPending 值。


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>
  )
}

  • Action 会在平常的参数之前先收到 previous state。对 form Action 来说,第二个参数就是提交的 FormData。
  • useActionState 适合 server result 应该变成 UI state 的时候。useFormStatus 适合只需要知道周围 form 是否 pending 的 controls。
  • Failure: 在创建 form 的同一个 component 里调用 useFormStatus。它读的是 parent form,所以必须从渲染在该 form 里面 的 child 调用。

3. useOptimistic

useOptimistic 让 UI 在 Action 跑的时候立刻显示 预期结果,完成后再与真实 state 对账。


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>
  )
}

  • Optimistic value 是 暂时的。Action 结束后,React 会回到真实 state 或 props 提供的值。
  • Source of truth 仍然要在 optimistic reducer 外面更新。
  • Failure: 把 optimistic UI 当成已提交的成功。Destructive operations,以及假装成功可能误导用户的任何事,都该更谨慎。

4. 用 use 读取 Resources

use 在 rendering 期间读取 resource——今天是 Promise 或 Context。Pending Promise 会 suspend 到最近的 <Suspense> fallback。Rejection 会走到最近的 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>
  )
}

  • 与 Hooks 不同,use 可以放在 conditions 与 loops 里。它仍然必须在 React 正在 render 一个 component 或 custom Hook 时跑。
  • use 也可以有条件地读取 Context——在 muted heading 上跳过 use(ThemeContext),在 active 的那个上调用。
  • use 是通往可能尚未就绪的 resource 的桥,不是每一个 data-fetching library 的替代品。
  • Failure: 在每次 Client Component render 时创建新的 Promise。那会反复 suspend。Promise 应该来自 framework、cache,或 Server Component,好让同一个 resource 能被复用。

5. Refs 与 Context 少了繁琐写法

Function components 在 React 19 里可以把 ref 当成普通 prop 接收。新 components 不再需要只为了暴露一个 element 而写 forwardRef。


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()
  }}
/>

  • 既有的 forwardRef components 仍然能用。React 计划在迁移有时间发生之后再 deprecate forwardRef。
  • Ref callbacks 可以返回 cleanup functions——上面的 ResizeObserver disconnect。
  • Context object 可以直接渲染:<ThemeContext value="dark">,而不是 <ThemeContext.Provider value="dark">。
  • Failure: 把更严格的 ref-callback TypeScript 当成意外。Callbacks 现在可以返回 cleanup,所以类型会拒绝以前被忽略的意外 returns。

6. Document Metadata 与 Resources

React 19 理解渲染在 components 里的 <title>、<meta> 与 <link> tags。React 会把它们移到 document 的 <head>,所以 metadata 可以住在拥有它的 route 或内容旁边。


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> 与 <link rel="canonical"> 可以住在拥有它们的 route 旁边。React 会把它们 hoist 进 <head>。
  • Stylesheets 可以声明 precedence。React 会等相关 CSS,再揭示 Suspense 内容。即使多个 components 渲染同一份 script,async scripts 也会被 deduplicated。
  • React DOM 暴露 preconnect、preload、preinit、preloadModule 与 preinitModule,给在需要它们的 element 之前就已知的 resources。
  • Frameworks 已经处理了其中大部分。在直接调用这些 APIs 之前,先看 framework 的 conventions。

7. 更好的 Custom Elements 与 Hydration Errors

React 19 对 custom elements 有完整支持。Server rendering 时,strings 与 numbers 这类 primitive props 会变成 attributes。在 browser 里,当 custom element 暴露匹配的 properties 时,React 会把值赋成 properties。


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

  • Web components 更容易消费,不必为每个非 string 值写 React-specific wrappers。
  • Hydration diagnostics 被收拢:一个 error,附带 diff,显示 server-rendered HTML 与 client output 差在哪里。
  • Failure: 把更好的 message 当成修复。Mismatches 仍然来自 clocks、random values、browser-only branches、变化中的外部数据,或无效的 HTML nesting。

8. React 19.2 的 Activity

Activity 让 React 保持一段 interface mounted,同时控制它是否可见,以及它的 updates 该以多紧急的程度被处理。


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 显示 children、mount 它们的 effects,并正常处理 updates。
  • hidden 隐藏 children、unmount 它们的 effects,并把它们的 updates 延后到 React 没有可见工作剩下。
  • 与 conditional rendering 真正有用的差别是 preserved state。Hidden editor 可以保住 draft、selection 与 component state,同时 React 可以在背景准备很可能出现的下一个画面。
  • Failure: 把这当成 CSS display: none。Hidden Activities 会清理 effects,所以 subscriptions 必须能正确停止与重启。

9. React 19.2 的 useEffectEvent

Effects 常常混进两个想法:该对 dependency 做出反应的 synchronization,以及该读到最新 props、却不该重启那段 synchronization 的 event-like logic。useEffectEvent 把 event-like 那部分拆出来。


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])
}

  • 改 theme 不再会重连 chat room,但 connection event 触发时,onConnected 仍会读到最新的 theme。
  • Effect Events 只能从同一个 component 或 custom Hook 里的 Effects 调用。它们不应出现在 dependency arrays 里。
  • 使用这个 API 需要较新的 eslint-plugin-react-hooks;linter 理解并强制这些限制。
  • Failure: 用 Effect Events 掩盖缺失的 dependencies。它们应代表概念上由 Effect 触发的 logic。

10. React 19.2 的性能与 Server 改进

React 19.2 把 React Performance Tracks 加进 Chrome DevTools performance profiles。Scheduler tracks 显示 update priorities,以及 work 何时被 scheduled、blocked、rendered 或 painted。Components tracks 显示 components 何时 render,以及它们的 effects 何时 mount。

  • 这是比 React DevTools Profiler 更低阶的视图。适合缓慢交互牵涉 scheduling、browser 工作与 React rendering,而不是某一个明显昂贵的 component 的时候。
  • cacheSignal 给 React Server Component 工作一个绑在 cache() entry 生命周期上的 AbortSignal,好取消不再使用的 requests。
  • Partial pre-rendering APIs 让 framework 预先 render 一份 static shell、存下 postponed work,稍后再 resume 剩下的 server render。
  • Streaming SSR 会把邻近的 Suspense boundary reveals batch 起来。Node.js 得到 Web Streams 支持;React team 仍建议在 Node 里用 Node Streams APIs,因为它们更快,也更自然地配合 compression。
  • 大多数应用开发者会通过 framework 碰到 cacheSignal 与 partial pre-rendering。

11. 升级到 React 19

一起升级 React、React DOM,以及它们的 TypeScript definitions,使用 framework 支持的最新 patched React 19 release。


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

  • 官方 upgrade guide 包含常见 migrations 的 codemods。
  • ReactDOM.render / ReactDOM.hydrate → createRoot / hydrateRoot。unmountComponentAtNode → root.unmount()。
  • findDOMNode、string refs 与 this.refs 被移除。Legacy Context APIs 与 function component propTypes 被移除。
  • Ref callback TypeScript 规则更严格,因为 callbacks 现在可以返回 cleanup functions。
  • Failure: 把升级跟一次大型 rewrite 混在一起。先让既有应用在 React 19 上能跑,再在 Actions、use 或 Activity 真正简化某个 pattern 的地方引入它们。

要点

Actions 协调 async mutations 与 pending 及 optimistic UI。use 协调 rendering 与 resources。Document APIs 协调 metadata、styles 与 scripts。Activity 协调可见与背景工作。useEffectEvent 把 synchronization 与 event-like behavior 分开。

最常保留的功能是 form Actions、useActionState、useOptimistic、ref as a prop,以及 useEffectEvent。use、Activity 与 server rendering APIs 更依赖 framework 支持与架构。

官方说明见 React 19 release notes、React 19.2 release notes,以及 React 19 upgrade guide。


Recap Q&A

阅读下一篇笔记
TypeScript Beyond Strict