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

作者: Calpa Liu
字數:2179
出版: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 個分支的支持。

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

熱門文章

最新文章

圖片管理中心
管理圖片資源
IP 查詢
快速查詢和定位 IP 地址的地理位置和相關信息
Python 運行器
無需後端、無需登入,只需打開瀏覽器即可運行 Python 代碼(由 Pyodide 提供支持)
封面圖生成器
自動創建適合各種平台的文章封面圖
原作(青山剛昌)產生器
一鍵創建原作(青山剛昌)的封面圖
日本色彩
探索和使用傳統日本色彩
部落格內容洞察儀表板
以視覺化儀表板方式追蹤文章成效、分享熱度與分類分布,協助創作者掌握內容表現。
蒙特卡羅估算 π
使用蒙特卡羅方法演示 π 值的估算過程
LLM
使用 LLM 模型進行聊天
活動圖生成器
一鍵創建活動的封面圖
Wagmi Card
一鍵創建 Wagmi 的封面圖
Facebook Quote
Facebook Quote
Music Macro Language (MML) Studio
用程式語法編寫旋律,用音符構築想像
Blurhash
一鍵創建 Blurhash
文字分類器
使用 MediaPipe TextClassifier 分類文字
前端工程師免費工具資源
前端工程師免費工具資源
後端工程師免費工具資源
後端工程師免費工具資源
全端工程師免費工具資源
全端工程師免費工具資源
Web3 工程師免費工具資源
Web3 工程師免費工具資源
紫微斗數排盤系統|結合 AI 的命盤性格與事業財務分析生成器
紫微斗數排盤工具,輸入生日與時辰,自動生成完整命盤分析提示(Prompt)。結合最專業紫微理論與 AI 助力,助你深入解析性格、事業、財務與人際課題。免費使用,適合命理師及紫微愛好者。
PixAI Prompt 組合器|快速打造可用於 AI 繪圖的語言拼圖
使用 PixAI 卻不會寫 prompt?這個工具幫你一鍵組裝角色、表情、風格語彙,輸出高品質繪圖提示語句(Prompt),可直接貼入 PixAI 使用。適合插畫師、創作者、AI 新手與 VTuber 角色開發者。