跳至主要內容

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