React 19 實戰:併發特性、Actions 與新 Hooks

前端工程

React 19 為什麼值得升級

React 19 把過去幾年實驗性的併發特性正式收斂為穩定 API,並補齊了「表單 / 資料變更」這一長期短板。核心變化可以概括為一句話:用 Action 包裹「寫操作」,用併發原語包裹「讀操作」,讓 UI 在等待中不卡頓

能力 React 18 React 19
Server Components 實驗(Next.js 等框架) 成為文件級穩定特性
表單提交 手寫 useState + onSubmit useActionState / <form action>
讀取 Promise / Context use 不適用 use() 可在渲染中讀 Promise/Context
ref 只能 useRef + forwardRef 可直接作為 Props 傳遞
樂觀更新 手寫狀態切換 useOptimistic
併發過渡 useTransition useTransition + useDeferredValue 增強

Actions:把「寫操作」變成一等公民

Action 是一個「可以被 React 等待」的函式。配合 <form action>useActionState,React 會自動管理 pending 狀態並停用提交按鈕。

"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: "訂閱失敗,請重試" };
  return { ok: true, msg: "訂閱成功!" };
}

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 ? "提交中…" : "訂閱"}</button>
      <p>{state.msg}</p>
    </form>
  );
}

useFormStatus 則讓它能在子元件裡拿到父表單的狀態,無需層層透傳:

import { useFormStatus } from "react-dom";

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

設定類資料(如介面回傳的表單預設值)可先用 JSON 格式化 工具整理,再回填到 initialState,避免手寫大物件出錯。


use:在渲染中讀 Promise 與 Context

use() 是 React 19 引入的「渲染期讀取」原語:它能直接 await 一個 Promise,也能讀 Context,並會在 Suspense 邊界處掛起。

import { use } from "react";

function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
  const comments = use(commentsPromise); // 掛起直到 resolve
  return (
    <ul>
      {comments.map((c) => (
        <li key={c.id}>{c.body}</li>
      ))}
    </ul>
  );
}

注意:use() 必須在元件或 Hook 的渲染階段呼叫,不能放在 try/catch 之外的條件分支裡——它不是一個普通函式,而是告訴 React「這裡可能要等」。


併發渲染:讓等待不卡 UI

useTransition:把「慢更新」標記為非緊急

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); // 輸入框立刻回應
    startTransition(() => {
      setList(filterExpensive(value)); // 重計算包在 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:自動「延遲」一個值

當輸入與渲染解耦時,useDeferredValue 更省心:

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

兩者的差別:useTransition 包裹「一段狀態更新」,useDeferredValue 派生「一個被延遲的值」,通常和 useMemo 配合。


useOptimistic:樂觀更新開箱即用

import { useOptimistic, useRef } from "react";

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

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

  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 ref={input} name="text" />
      {messages.map((m) => (
        <p key={m.id}>{m.text}{m.sending ? "(發送中)" : ""}</p>
      ))}
    </form>
  );
}

涉及主題色、按鈕對比度時,可借助 顏色轉換顏色對比度 工具校驗 UI 可存取性(WCAG)。


ref 作為 Props 與文件元資料

ref 不再是特例

function MyInput({ ref }: { ref: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} />;
}
// 使用:<MyInput ref={myRef} />  —— 不再需要 forwardRef

文件元資料原生支援

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

function Page() {
  return (
    <>
      <Title>頁面標題</Title>
      <Meta name="description" content="頁面描述" />
    </>
  );
}

從 React 18 平滑遷移

  1. 升級依賴npm i react@19 react-dom@19,並升級 @types/react
  2. 替換 forwardRef:直接把 ref 寫進 Props 即可,舊寫法在 19 中已棄用但暫相容。
  3. 落地 Actions:把「提交後 setState + loading」的表單遷移到 useActionState
  4. use 收口非同步:Server Components 下把資料 Promise 傳給子元件,用 use() 讀取。
  5. 接入併發:列表/搜尋類互動用 useDeferredValueuseTransition 提升回應感。

常見問題 FAQ

Q1:Actions 只能在 Server Component 裡用嗎?

不是。用戶端也能用 useActionState 包裹普通非同步函式;Server Action 只是其中一種「函式直接跑在服務端」的形式。

Q2:use() 和 await 有什麼區別?

await 是 JS 執行期語意,use() 是 React 的渲染語意:它會在 Suspense 邊界掛起,並能被併發排程打斷重渲染。

Q3:useTransition 和 useDeferredValue 選哪個?

要「延遲一整段更新」用 useTransition;要「基於某個快速變化的值派生慢結果」用 useDeferredValue + useMemo

Q4:React 19 還支援 IE 嗎?

不支援,且已徹底移除舊版遺留行為。現代瀏覽器環境是前提。

Q5:樂觀更新失敗怎麼回滾?

useOptimistic 會在傳入的 Action 拋錯時自動回滾到真實狀態;你只需保證 setMessages 的「真實資料來源」在失敗時不變即可。


工具推薦

在 React 19 開發中,以下 工具庫 能幫上忙:


React 19 的精髓不是「又多了幾個 Hook」,而是把「寫操作(Action)」與「讀操作(併發/use)」用統一心智模型管起來:UI 在等待資料時不卡頓,在提交變更時狀態自管。掌握 Action + 併發原語,你就握住了現代 React 的鑰匙。

本站提供瀏覽器本地工具,免註冊即可試用 →

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