⚛️

【React 19】useActionStateって何?

2024/10/27に公開

はじめに

React 19の新機能でuseActionStateというフックが新たに導入されました。
今回はuseActionStateについてuseStateと比較して解説していきます。

用語解説
  • アクション: 非同期関数
  • ServerActions: サーバ上で実行される非同期関数
  • ハイドレーション: イベントハンドラの登録やインタラクティブな状態にすること

useActionStateとは

useActionStateは、フォームの状態管理を行うために設計されたフックです。
アクションの実行とその結果の管理を簡素化することができます。

useActionStateのメリット

  • フォームが送信されたときに自動的にステートや実行状態を更新してくれる。
  • アクションの実行状態(isPending)とその結果(state)を一元管理できる。
  • ServerActionsと組み合わせて使うことで、ハイドレーションが完了する前にレスポンスの表示を可能にする。

useActionStateの使い方

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

引数

  • action: フォームが送信されたときに呼び出される関数。
    • currentState (第1引数): 現在のステート
    • formData (第2引数): フォームの入力値
  • initialState: stateの初期状態。
  • permalink (省略可能): フォーム送信後にリダイレクトするユニークなページURL。

戻り値

  • state: アクション実行後のステート。(1度も実行されてない場合はinitialStateが返る)
  • formAction: form要素のactionプロパティに渡す関数。
  • isPending: アクションが実行中かを表すブール値。(useTransitionのisPendingと同じ)

サンプルコード

お問い合わせフォームの実装を想定して、useStateuseActionStateのそれぞれで同じ機能のフォームを実装したサンプルコードになります。

useStateの場合

import { type FormEvent, useState } from "react"

// お問い合わせ内容を送信する関数
const submitInquiry = async (name: string, email: string, details: string): Promise<boolean> => {
  // お問い合わせ内容をAPIにPOSTする (今回は3秒待機で模倣)
  await new Promise(resolve => setTimeout(resolve, 3000))

  // POSTの結果に応じてbooleanを返す (今回はランダムでbooleanを返す)
  return Math.random() < 0.5
}

const InquiryForm = () => {
  const [isPending, setIsPending] = useState<boolean>(false) // 実行状態フラグ
  const [isError, setIsError] = useState<boolean>(false) // エラーフラグ
  const [message, setMessage] = useState<string>("") // メッセージ

  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    // submitイベントをキャンセル
    e.preventDefault()

    // 実行中にする
    setIsPending(true)

    // フォームデータを取得
    const formData = new FormData(e.currentTarget)
    const name = formData.get("name")?.toString() ?? ""
    const email = formData.get("email")?.toString()?? ""
    const details = formData.get("details")?.toString() ?? ""

    // フォームデータを送信
    const isSuccess = await submitInquiry(name, email, details)
    
    // 結果に応じてステートを変更
    if (isSuccess) {
      setIsError(false)
      setMessage("送信完了")
    } else {
      setIsError(true)
      setMessage("送信失敗")
    }

    // 実行状態を解除
    setIsPending(false)
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>useStateで実装</p>
      <label>
        <div>名前</div>
        <input type="text" name="name" required />
      </label>
      <label>
        <div>メールアドレス</div>
        <input type="email" name="email" required />
      </label>
      <label>
        <div>お問い合わせ内容</div>
        <textarea name="details" required />
      </label>
      {message && (
        <p className={isError ? "text-red-500" : ""}>{message}</p>
      )}
      <div>
        <button type="submit" disabled={isPending}>送信</button>
      </div>
    </form>
  )
}

export default InquiryForm

useActionStateの場合

import { useActionState } from "react"

type FormState = {
  isError: boolean
  message: string
}

// お問い合わせ内容を送信する関数
const submitInquiry = async (name: string, email: string, details: string): Promise<boolean> => {
  // お問い合わせ内容をAPIにPOSTする (今回は3秒待機で模倣)
  await new Promise(resolve => setTimeout(resolve, 3000))

  // POSTの結果に応じてbooleanを返す (今回はランダムでbooleanを返す)
  return Math.random() < 0.5
}

const InquiryForm2 = () => {
  const [formState, formAction, isPending] = useActionState<FormState, FormData>(
    async (currentState, formData) => {
      // フォームデータを取得
      const name = formData.get("name")?.toString() ?? ""
      const email = formData.get("email")?.toString() ?? ""
      const details = formData.get("details")?.toString() ?? ""

      // フォームデータを送信
      const isSuccess = await submitInquiry(name, email, details)

      // 結果に応じてformStateを返す
      if (isSuccess) {
        return { isError: false, message: "送信完了" }
      }
      return { isError: true, message: "送信失敗" }
    },
    { isError: false, message: "" }, // formStateの初期値,
  )

  return (
    <form action={formAction}>
      <p>useActionStateで実装</p>
      <label>
        <div>名前</div>
        <input type="text" name="name" required />
      </label>
      <label>
        <div>メールアドレス</div>
        <input type="email" name="email" required />
      </label>
      <label>
        <div>お問い合わせ内容</div>
        <textarea name="details" required />
      </label>
      {formState.message && (
        <p>{formState.isError && "エラー: "}{formState.message}</p>
      )}
      <div>
        <button type="submit" disabled={isPending}>送信</button>
      </div>
    </form>
  )
}

export default InquiryForm2

デモ画面

最後に

useActionStateは、フォーム管理を簡素化できるとても便利なフックです。
今まで個々にuseStateで管理していたのをuseActionStateでまとめて管理できるので、コードもスッキリして見やすくなります。
ServerActionsと組み合わせることで、パフォーマンスやユーザー体験の向上もさせることができます。
みなさんもぜひ使ってみてください。

参考資料

https://ja.react.dev/reference/react/useActionState
https://dev.classmethod.jp/articles/react-19-understanding-use-action-state/

GitHubで編集を提案

Discussion