React 19 的革新:深入探討 useActionState Hook

作者: Calpa Liu
字數:2185
出版:2025年3月26日

React 19 引入了一系列創新功能,其中最引人注目的之一是 useActionState Hook。這個強大的新工具為前端開發者提供了更簡潔、更高效的方式來管理表單操作和狀態更新。本文將深入剖析 useActionState 的實用性,並通過實例演示如何在項目中有效地運用它。

useActionState 的基本概念

useActionState 是 React 19 中引入的一個新 Hook,旨在簡化基於表單操作的狀態更新。它實際上是前身 useFormState 的重命名和改進版本,設計目的更為明確,功能也更加完善。

這個 Hook 的基本語法如下:

const [state, formAction, isPending] = useActionState(fn, initialState, permalink?);

參數解析

  • fn: 表單提交或按鈕按下時要調用的函數。此函數首先接收表單的前一個狀態,然後是其他標準表單操作參數
  • initialState: 初始狀態值,可以是任何可序列化的值
  • permalink (可選): 一個包含唯一頁面 URL 的字符串,用於動態內容頁面與漸進式增強

返回值

useActionState 返回一個包含三個元素的數組:

  1. 當前狀態 (state): 首次渲染時,它與您提供的 initialState 匹配;操作調用後,它會匹配操作返回的值
  2. 操作函數 (formAction): 可傳遞給表單的 action 屬性或表單內按鈕的 formAction 屬性的新操作
  3. 等待狀態 (isPending): 一個布爾值,指示是否有待處理的轉換

為什麼選擇 useActionState?

傳統上,使用 useState 管理表單狀態通常需要多個狀態變數來處理不同方面,如用戶輸入、加載狀態和錯誤處理。這種方法會導致代碼冗長且難以維護。而 useActionState 的優勢在於:

  1. 減少樣板代碼:消除了使用多個 useState 來管理相關狀態的需求
  2. 內置加載狀態:通過 isPending 標誌自動跟踪異步操作的進度
  3. 面向操作的設計:將狀態轉換直接綁定到特定操作,提高清晰度
  4. 服務器端兼容性:與支持 React Server Components 的框架無縫集成

基本使用示例

讓我們從一個簡單的計數器示例開始,展示 useActionState 的基本使用方式:

import { useActionState } from "react";

async function increment(previousState, formData) {
  return previousState + 1;
}

function Counter() {
  const [count, formAction, isPending] = useActionState(increment, 0);

  return (
    <form>
      <p>計數:{count}</p>
      <button formAction={formAction} disabled={isPending}>
        {isPending ? "處理中..." : "增加"}
      </button>
    </form>
  );
}

在這個例子中,每次點擊按鈕,計數器都會增加 1。useActionState Hook 負責在表單提交時更新狀態。

表單處理與錯誤管理

useActionState 在處理表單提交和錯誤管理方面特別有用。下面是一個更複雜的表單處理示例:

import { useActionState } from "react";

async function submitUser(previousState, formData) {
  try {
    // 模擬 API 調用
    await new Promise((resolve) => setTimeout(resolve, 1000));

    const name = formData.get("name");
    const email = formData.get("email");

    if (!name || !email) {
      return { success: false, error: "請填寫所有欄位" };
    }

    // 處理成功結果
    return {
      success: true,
      data: { name, email },
      error: null,
    };
  } catch (error) {
    // 處理錯誤情況
    return {
      success: false,
      error: "提交失敗,請稍後再試",
    };
  }
}

function UserForm() {
  const [formState, formAction, isPending] = useActionState(submitUser, {
    success: false,
    data: null,
    error: null,
  });

  return (
    <form action={formAction}>
      <div>
        <label htmlFor="name">姓名:</label>
        <input type="text" id="name" name="name" required />
      </div>

      <div>
        <label htmlFor="email">電子郵件:</label>
        <input type="email" id="email" name="email" required />
      </div>

      <button type="submit" disabled={isPending}>
        {isPending ? "提交中..." : "提交"}
      </button>

      {formState.error && <div className="error">{formState.error}</div>}

      {formState.success && (
        <div className="success">用戶 {formState.data.name} 已成功添加!</div>
      )}
    </form>
  );
}

在此示例中,我們:

  • 處理表單提交並執行模擬 API 調用
  • 在提交期間禁用按鈕以防止重複提交
  • 根據操作結果顯示成功或錯誤消息
  • 使用單一狀態對象管理整個表單狀態

與 Server Components 集成

useActionState 的一個強大特性是它能與支持 React Server Components 的框架無縫集成,這使得表單在 JavaScript 完全加載之前就能具有交互性。

import { useActionState } from "react";
import { createUser } from "./actions.js"; // 服務器操作

function AddUserForm() {
  const [result, formAction, isPending] = useActionState(createUser, null);

  return (
    <form action={formAction}>
      <h2>添加新用戶</h2>

      <input type="text" name="name" placeholder="姓名" required />
      <input type="email" name="email" placeholder="電子郵件" required />
      <input type="number" name="age" placeholder="年齡" required />

      <button type="submit" disabled={isPending}>
        {isPending ? "添加中..." : "添加用戶"}
      </button>

      {result?.error && <div className="error">{result.error}</div>}
      {result?.success && <div className="success">用戶已成功添加!</div>}
    </form>
  );
}

在這個示例中,createUser 是一個服務器操作,可以在服務器上執行數據庫操作,使客戶端和服務器之間的通信更加無縫。

與 useState 的對比

為了更清楚地理解 useActionState 的優勢,讓我們比較使用傳統 useState 方法和 useActionState 方法處理表單的代碼:

使用 useState 的方法

import { useState } from "react";

function CommentForm() {
  const [comment, setComment] = useState("");
  const [isPending, setIsPending] = useState(false);
  const [error, setError] = useState(null);
  const [comments, setComments] = useState([]);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsPending(true);
    setError(null);

    try {
      // 模擬 API 調用
      await new Promise((resolve) => setTimeout(resolve, 1000));
      setComments([...comments, comment]);
      setComment("");
    } catch (err) {
      setError("提交評論失敗");
    } finally {
      setIsPending(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={comment}
        onChange={(e) => setComment(e.target.value)}
        disabled={isPending}
      />

      <button type="submit" disabled={isPending || !comment}>
        {isPending ? "提交中..." : "添加評論"}
      </button>

      {error && <div className="error">{error}</div>}
      <ul>
        {comments.map((c, i) => (
          <li key={i}>{c}</li>
        ))}
      </ul>
    </form>
  );
}

使用 useActionState 的方法

import { useActionState } from "react";

async function addComment(prevState, formData) {
  // 模擬 API 調用
  await new Promise((resolve) => setTimeout(resolve, 1000));

  const newComment = formData.get("comment");
  if (!newComment) {
    return { ...prevState, error: "評論不能為空" };
  }

  return {
    comments: [...prevState.comments, newComment],
    error: null,
  };
}

function CommentForm() {
  const [state, formAction, isPending] = useActionState(addComment, {
    comments: [],
    error: null,
  });

  return (
    <form action={formAction}>
      <textarea name="comment" disabled={isPending} />

      <button type="submit" disabled={isPending}>
        {isPending ? "提交中..." : "添加評論"}
      </button>

      {state.error && <div className="error">{state.error}</div>}
      <ul>
        {state.comments.map((c, i) => (
          <li key={i}>{c}</li>
        ))}
      </ul>
    </form>
  );
}

通過對比可以看出:

  1. 代碼量減少useActionState 版本明顯更簡潔,減少了許多樣板代碼
  2. 狀態整合:所有相關狀態被統一管理,而不是散布在多個 useState
  3. 自動處理加載狀態:不需要手動設置 setIsPending(true/false)
  4. 清晰的表單提交:使用表單的 action 屬性,而不是 onSubmit 事件處理器

進階使用技巧

檔案上傳表單

async function uploadFile(prevState, formData) {
  try {
    // 模擬文件上傳
    await new Promise((resolve) => setTimeout(resolve, 2000));
    const file = formData.get("file");

    if (!file || file.size === 0) {
      return { success: false, message: "請選擇一個文件" };
    }

    return { success: true, message: "文件上傳成功!", fileName: file.name };
  } catch {
    return { success: false, message: "上傳失敗。" };
  }
}

function UploadForm() {
  const [uploadStatus, uploadAction, isUploading] = useActionState(
    uploadFile,
    null
  );

  return (
    <form action={uploadAction}>
      <input type="file" name="file" />
      <button type="submit" disabled={isUploading}>
        {isUploading ? "上傳中..." : "上傳"}
      </button>

      {uploadStatus && (
        <p className={uploadStatus.success ? "success" : "error"}>
          {uploadStatus.message}
          {uploadStatus.fileName && <span>{uploadStatus.fileName}</span>}
        </p>
      )}
    </form>
  );
}

樂觀 UI 更新

結合 useOptimisticuseActionState 可以創建更加流暢的用戶體驗:

import { useActionState, useOptimistic } from "react";

function ChangeName({ currentName, onUpdateName }) {
  const [optimisticName, setOptimisticName] = useOptimistic(currentName);

  const submitAction = async (formData) => {
    const newName = formData.get("name");
    setOptimisticName(newName);
    const updatedName = await updateName(newName);
    onUpdateName(updatedName);
    return { success: true };
  };

  const [state, formAction, isPending] = useActionState(submitAction, null);

  return (
    <form action={formAction}>
      <p>您的名稱是:{optimisticName}</p>
      <p>
        <label>更改名稱:</label>
        <input type="text" name="name" disabled={isPending} />
      </p>
      <button type="submit" disabled={isPending}>
        {isPending ? "更新中..." : "更新名稱"}
      </button>
    </form>
  );
}

最佳實踐

使用 useActionState 時,請牢記以下最佳實踐:

  1. 提供有意義的初始狀態:確保初始狀態結構與預期的數據結構匹配

    // ❌ 不良初始化
    const [state, action] = useActionState(handleSubmit);
    
    // ✅ 良好初始化
    const [state, action] = useActionState(handleSubmit, {
      status: "idle",
      data: null,
      error: null,
    });
  2. 實現健全的錯誤處理:始終在操作函數中捕獲並返回錯誤,以提供清晰的反饋

    async function handleAction(prevState, formData) {
      try {
        const result = await apiCall(formData);
        return { data: result, error: null };
      } catch (err) {
        return { data: null, error: err.message };
      }
    }
  3. 避免過度使用:對於簡單的 UI 狀態,useState 可能仍然是更合適的選擇

  4. 使用 TypeScript 類型:為您的狀態和操作提供類型定義,以增加類型安全性

  5. 記憶體管理:確保在不再需要時釋放資源,避免在循環或渲染中創建原子,並考慮使用弱引用

與 Web3 開發的結合

對於 Web3 開發者,useActionState 提供了處理區塊鏈交互的強大方式。以下是一個與以太坊交互的簡單示例:

import { useActionState } from "react";
import { ethers } from "ethers";

async function sendTransaction(prevState, formData) {
  try {
    const amount = formData.get("amount");
    const recipient = formData.get("recipient");

    if (!window.ethereum) {
      return { success: false, error: "請安裝 MetaMask" };
    }

    const provider = new ethers.providers.Web3Provider(window.ethereum);
    const signer = provider.getSigner();

    // 請求帳戶訪問
    await window.ethereum.request({ method: "eth_requestAccounts" });

    // 發送交易
    const tx = await signer.sendTransaction({
      to: recipient,
      value: ethers.utils.parseEther(amount),
    });

    return {
      success: true,
      txHash: tx.hash,
      error: null,
    };
  } catch (error) {
    return {
      success: false,
      error: error.message || "交易失敗",
    };
  }
}

function Web3TransactionForm() {
  const [txState, formAction, isPending] = useActionState(sendTransaction, {
    success: false,
    txHash: null,
    error: null,
  });

  return (
    <form action={formAction}>
      <div>
        <label htmlFor="recipient">接收地址:</label>
        <input type="text" id="recipient" name="recipient" required />
      </div>

      <div>
        <label htmlFor="amount">數量 (ETH):</label>
        <input type="number" id="amount" name="amount" step="0.01" required />
      </div>

      <button type="submit" disabled={isPending}>
        {isPending ? "交易處理中..." : "發送交易"}
      </button>

      {txState.error && <div className="error">{txState.error}</div>}

      {txState.success && (
        <div className="success">交易已提交!交易哈希:{txState.txHash}</div>
      )}
    </form>
  );
}

內部原理

useActionState 的工作原理包括以下步驟:

  1. 首次渲染時,它返回提供的初始狀態
  2. 當表單操作被調用時,它接收前一個狀態作為首個參數
  3. 操作函數執行完成後,其返回值成為新的狀態
  4. React 使用這個新狀態重新渲染組件

值得注意的是,在支持 React Server Components 的框架中,即使在 JavaScript 完全加載到客戶端之前,useActionState 也能讓表單具有交互性。

總結

useActionState 是 React 19 中一個革命性的新 Hook,它通過簡化表單狀態管理和提供更加聲明式的 API,極大地改進了 React 應用程序中的表單處理和狀態管理。它的主要優勢包括減少樣板代碼、自動處理加載狀態以及與服務器組件的無縫集成。

無論您是在開發簡單的表單還是構建複雜的 Web3 應用程序,useActionState 都能為您提供更加簡潔、高效的狀態管理解決方案。隨著 React 19 的正式發布,我們可以期待看到更多基於這個強大 Hook 的創新使用方式。

作為前端開發者,特別是專注於 TypeScript 和 Web3 的開發者,掌握 useActionState 將為您的工具箱增添一個強大的工具,使您能夠更有效地構建現代、交互式的 Web 應用程序。

感謝您閱讀我的文章。歡迎隨時分享你的想法。
關於 Calpa

Calpa 擅長使用 TypeScriptReact.jsVue.js 建立 Responsive Website。

他積極參與開源社區,曾在 2019 年的香港開源大會上擔任講者,提供工作經驗和見解。此外,他也在 GitHub 上公開分享個人博客程式碼,已獲得超過 300 顆星星和 60 個分支的支持。

他熱愛學習新技術,並樂意分享經驗。他相信,唯有不斷學習才能跟上快速演變的技術環境。