跳至主要內容

React 19 感覺比較不像一堆互不相干的 API,而更像 React 補上了以往應用程式碼要自己處理的幾個缺口。Forms 現在可以跑非同步 Actions,optimistic updates 有專用的 Hook,components 可以用 use 讀取 promises,而 refs、context 與 document metadata 等常見 pattern 也少了很多繁瑣寫法。

React 19.2 在這個基礎上加入了 ActivityuseEffectEvent,以及更好的效能工具。以下是我覺得最實用的功能,以及決定何時使用時想記住的注意事項。



1. Actions

Action 是在 Transition 裡執行的函式。它可以做非同步工作,同時由 React 管理周圍的 pending state、errors,以及最終的 state update。

Actions 最顯眼的用途是 <form> 上新的 action prop。不用再攔截 onSubmit、讀取表單、追蹤 loading flag,再手動重置表單——我可以直接把函式傳給它。


function UpdateName() {
  async function updateName(formData: FormData) {
    const name = String(formData.get("name") ?? "")

    await saveName(name)
  }

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

當 Action 成功時,React 會自動重置表單裡的 uncontrolled fields。同一個函式也可以指定給 button 的 formAction prop,讓不同 submit buttons 有不同行為。

Actions 不只限於 forms,但表單整合是它們砍掉最多例行 state management 的地方。



2. useActionStateuseFormStatus

useActionState 把 Action 接到由其前一次結果產生的 state。它回傳 current state、一個包裝過的 Action,以及 isPending 值。

Action 會在平常的參數之前先收到 previous state。對 form Action 來說,第二個參數就是提交的 FormData


import { useActionState } from "react"
import { useFormStatus } from "react-dom"

type FormState = {
  message: string
  ok: boolean
}

const initialState: FormState = {
  message: "",
  ok: false,
}

async function saveEmail(
  previousState: FormState,
  formData: FormData
): Promise<FormState> {
  const email = String(formData.get("email") ?? "")

  if (!email.includes("@")) {
    return { message: "Enter a valid email address.", 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, initialState)

  return (
    <form action={formAction}>
      <input name="email" type="email" />
      <SubmitButton />
      <p aria-live="polite">{state.message}</p>
    </form>
  )
}

useFormStatus 讀取其 parent form 的 status,所以必須從渲染在該 form 裡面 的 component 呼叫。若在建立 form 的同一個 component 裡呼叫,就觀察不到該 form 的 submission。

當 server 結果應該變成 UI state 時,我會用 useActionState;若 controls 只需要知道周圍 form 是否 pending,則用 useFormStatus



3. useOptimistic

等 network round trip 完成才更新介面,會讓本來很快的應用顯得緩慢。useOptimistic 讓 UI 在 Action 執行期間立刻顯示 預期結果,完成後再與真實 state 對齊。


import { useOptimistic, useState } from "react"

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 savedMessage = await saveMessage(text)
    setMessages((current) => [...current, savedMessage])
  }

  return (
    <>
      <ul>
        {optimisticMessages.map((message) => (
          <li key={message.id}>
            {message.text}
            {message.sending ? " (sending...)" : ""}
          </li>
        ))}
      </ul>

      <form action={sendMessage}>
        <input name="message" />
        <button type="submit">Send</button>
      </form>
    </>
  )
}

Optimistic value 是暫時的。Action 結束後,React 會回到 component 真實 state 或 props 提供的值。也就是說,source of truth 仍需要在 optimistic reducer 之外更新。

Optimistic updates 最適合預期結果很明顯、失敗可以解釋或還原的情況。對 destructive operations,或假裝成功會誤導用戶的情況,我會更謹慎。



4. 用 use 讀取 Resources

新的 use API 在 render 期間讀取 resource。目前這類 resource 通常是 PromiseContext

use 收到 pending Promise 時,component 會 suspend,React 顯示最近的 <Suspense> fallback。若 Promise reject,則由最近的 Error Boundary 處理 error。


import { Suspense, use } from "react"

type Profile = {
  name: string
  role: string
}

function ProfileCard({ profilePromise }: { profilePromise: Promise<Profile> }) {
  const profile = use(profilePromise)

  return (
    <article>
      <h2>{profile.name}</h2>
      <p>{profile.role}</p>
    </article>
  )
}

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 時呼叫。

重要的 Promise 注意點是 identity。在每次 Client Component render 時都建立新的 Promise,會導致反覆 suspension。Promise 應來自 framework、cache,或 Server Component,才能重用同一個 resource。

use 也可以有條件地讀取 Context:


function Heading({ muted }: { muted: boolean }) {
  if (muted) {
    return <h2 className="muted">Archived</h2>
  }

  const theme = use(ThemeContext)
  return <h2 className={theme}>Active</h2>
}

我把 use 想成 render 與可能尚未就緒的 resource 之間的橋樑,而不是取代每個 data-fetching library。



5. Refs 與 Context 少了繁瑣寫法

Function components 在 React 19 可以把 ref 當一般 prop 接收。新 components 不再需要只為了 expose element 而使用 forwardRef


import type { ComponentPropsWithRef } from "react"

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

function Search() {
  const inputRef = useRef<HTMLInputElement>(null)

  return <SearchInput ref={inputRef} placeholder="Search" />
}

既有的 forwardRef components 仍然可用,所以不必立刻改寫。React 計劃在遷移有足夠時間後,於未來版本 deprecate forwardRef

Ref callbacks 現在也可以回傳 cleanup functions:


function Canvas() {
  return (
    <canvas
      ref={(node) => {
        if (!node) return

        const observer = new ResizeObserver(() => draw(node))
        observer.observe(node)

        return () => observer.disconnect()
      }}
    />
  )
}

Context providers 也更短了。可以直接 render Context object,不必再用 .Provider


// Before
<ThemeContext.Provider value="dark">
  <App />
</ThemeContext.Provider>

// React 19
<ThemeContext value="dark">
  <App />
</ThemeContext>

這些改動不大,但拿掉了幾乎每個 React codebase 都常見的 wrappers。



6. Document Metadata 與 Resources

React 19 能理解 components 裡 render 的 <title><meta><link> tags。React 會把它們移到 document 的 <head>,也就是說 metadata 可以放在擁有它的 route 或 content 附近


function ProductPage({ product }: { product: Product }) {
  return (
    <>
      <title>{product.name} – Acme Store</title>
      <meta name="description" content={product.summary} />
      <link
        rel="canonical"
        href={`https://example.com/products/${product.slug}`}
      />

      <h1>{product.name}</h1>
      <p>{product.summary}</p>
    </>
  )
}

React 19 也會協調 stylesheets 與 scripts。Stylesheets 可以宣告 precedence,React 會等相關 CSS 就緒後才顯示 Suspense content。即使多個 components render 同一個 script,async scripts 也會被 deduplicate。

React DOM 暴露了 preconnectpreloadpreinitpreloadModulepreinitModule 等 resource hints,適用於應用在 React render 需要該 resource 的 element 之前就已知曉的情況。


import { preconnect, preload } from "react-dom"

function ProductImage({ src, alt }: { src: string; alt: string }) {
  preconnect("https://images.example.com")
  preload(src, { as: "image" })

  return <img src={src} alt={alt} />
}

Frameworks 已經處理了其中不少工作,所以在直接呼叫這些 API 之前,我會先查看 framework 的慣例。



7. 更好的 Custom Elements 與 Hydration Errors

React 19 完整支援 custom elements。在 server rendering 期間,字串與數字等 primitive props 會變成 attributes。在瀏覽器裡,當 custom element 暴露對應 properties 時,React 會把值 assign 為 properties。


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

這讓 web components 更容易使用,不必為每個非字串值寫 React-specific wrappers。

Hydration diagnostics 也更實用。不再是好幾條重疊的 warnings,React 19 會回報一條整合過的 error,並用 diff 顯示 server-rendered HTML 與 client output 的差異。

改進後的訊息並不會讓 hydration mismatches 變得無害。它只是讓真正原因——例如不同的日期、僅在瀏覽器執行的分支、變化中的外部資料,或無效的 HTML nesting——更快被找到。



8. React 19.2 的 Activity

Activity 讓 React 保持介面某一區塊 mounted,同時控制它是否可見,以及其 updates 應以多高的緊急程度處理。


import { Activity } from "react"

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

      <Activity mode={activeTab === "preview" ? "visible" : "hidden"}>
        <Preview />
      </Activity>
    </>
  )
}

React 19.2 支援兩種 modes:

  • visible 顯示 children、mount 它們的 effects,並正常處理 updates。
  • hidden 隱藏 children、unmount 它們的 effects,並把 updates 延後到 React 沒有可見工作為止。

與 conditional rendering 有用的差異是 preserved state。隱藏的 editor 可以保留 draft、selection 與 component state,同時 React 也能在背景準備很可能會用到的下一個畫面。

這與用 CSS 把 component 留在可見狀態不同。Hidden Activities 會清理 effects,所以 subscriptions 與其他外部 synchronization 必須能正確停止與重新啟動。



9. React 19.2 的 useEffectEvent

Effects 常常混進兩種不同想法:應對 dependency 作出反應的 synchronization,以及應讀取最新 props、卻不應重啟該 synchronization 的 event-like logic。

useEffectEvent 把 event-like 的部分分開。


import { useEffect, useEffectEvent } from "react"

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

  return <p>Room: {roomId}</p>
}

改變 theme 不再會重連 chat room,但當 connection event 觸發時,onConnected 仍會讀到最新的 theme。

Effect Events 不是用來隱藏缺失 dependencies 的方法。 它們應代表概念上由 Effect 觸發的邏輯,而且只能從同一個 component 或 custom Hook 裡的 Effects 呼叫。它們不應被放進 dependency arrays。

使用這個 API 也需要較新的 eslint-plugin-react-hooks,因為 linter 理解並會強制這些限制。



10. React 19.2 的效能與 Server 改進

React 19.2 在 Chrome DevTools performance profiles 加入了 React Performance Tracks。Scheduler tracks 顯示 update priorities,以及 work 何時被 scheduled、blocked、rendered 或 painted。Components tracks 顯示 components 何時 render,以及它們的 effects 何時 mount。

這提供比 React DevTools Profiler 更底層的視角。當緩慢的互動牽涉 scheduling、瀏覽器工作與 React rendering,而不是某個明顯昂貴的 component 時,我會用它。

還有兩項多數應用開發者會透過 framework 接觸到的新增:

  • cacheSignal 為 React Server Component 工作提供與 cache() entry 生命週期綁定的 AbortSignal,讓未使用的 requests 或其他工作可以被取消。
  • Partial pre-rendering APIs 讓 framework 可以預先 render 靜態 shell、儲存 postponed work,稍後再 resume 剩餘的 server render。

React 19.2 也透過批次附近的 Suspense boundary reveals 改善了 streaming SSR,並為 Node.js 加入 Web Streams 支援。React 團隊在 Node 環境仍建議使用 Node Streams APIs,因為它們更快,也更自然地配合 compression。



11. 升級到 React 19

我會把 React、React DOM,以及它們的 TypeScript definitions 一起升級,使用 framework 支援的最新 patched React 19 release。


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

官方 upgrade guide 包含常見遷移的 codemods。值得先檢查的 breaking changes 是已移除的 legacy APIs:

  • ReactDOM.renderReactDOM.hydratecreateRoothydrateRoot 取代。
  • unmountComponentAtNoderoot.unmount() 取代。
  • findDOMNode、string refs 與 this.refs 已移除。
  • Legacy Context APIs 與 function component propTypes 已移除。
  • Ref callback 的 TypeScript 規則更嚴格,因為 callbacks 現在可以回傳 cleanup functions。

我會把 升級與功能採用當成分開的步驟。先讓既有應用在 React 19 上運作,再在 Actions、useActivity 真正簡化某個 pattern 的地方引入它們。Major upgrade 若沒有與大規模 rewrite 混在一起,會更容易 debug。



重點

我在 React 19 看到的主題是 coordination。Actions 協調 async mutations 與 pending 及 optimistic UI。use 協調 rendering 與 resources。Document APIs 協調 metadata、styles 與 scripts。Activity 協調可見與背景工作,而 useEffectEvent 把 synchronization 與 event-like behavior 分開。

我預期最常使用的功能是 form Actions、useActionStateuseOptimistic、ref as a prop,以及 useEffectEventuseActivity 與 server rendering APIs 更依賴 framework 支援與應用架構,但它們指向一種 loading、mutations 與 rendering 被設計成一起運作的 React model。

完整細節我會回頭參考官方 React 19 release notesReact 19.2 release notes,以及 React 19 upgrade guide