📝

useActionStateでフォーム状態を管理する

に公開

はじめに

Next.jsのフォームアクションの状態管理には複数の方法がありますが、本記事ではuseActionStateを使った実装方法を紹介します。

useActionStateとは

React v19で導入されたフックで、フォームアクションなどの非同期処理の状態を簡単に管理できます。

従来の方法との比較

useStateでも実装可能ですが、進行状態・データ・エラーなど複数のステートを管理する必要があり、コードが冗長になります。

useStateでの実装例
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
const [data, setData] = useState<any>(null);
const [error, setError] = useState<string | null>(null);

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();

  setStatus("loading");

  try {
    const title = e.target.title.value;
    const body = e.target.body.value;
    
    const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
      method: "POST",
      body: JSON.stringify({ title, body }),
      headers: {
        "Content-Type": "application/json",
      },
    });

    if (!response.ok) {
      const error = await response.text();
      setError(error);
      setStatus("error");
      return;
    }

    const post = await response.json();
    setData(post);
    setStatus("success");
  } catch (error) {
    setError(error.message);
    setStatus("error");
  }
};

return (
  <form onSubmit={handleSubmit}>
    {status === "loading" && <p>Loading...</p>}
    {status === "success" && <p>{JSON.stringify(data)}</p>}
    {status === "error" && <p>{error}</p>}
    <input type="text" name="title" />
    <textarea name="body" />
    <button type="submit">Submit</button>
  </form>
);

useActionStateを使えば、複数のステートを管理する必要がなくなり、非同期処理の進行状態・データ・エラーを一元管理できるため、コードがシンプルになります。

実装例

使用環境

本記事で使用するパッケージとAPIは以下の通りです。

{
  "@chakra-ui/react": "^3.7.0",
  "@emotion/react": "^11.14.0",
  "next": "15.1.6",
  "next-themes": "^0.4.4",
  "react": "^19.0.0",
  "react-dom": "^19.0.0",
  "react-icons": "^5.4.0"
}

APIはJSONPlaceholderを使用します。

サンプルコード

タイトルとコンテンツを入力してAPIにデータ作成リクエストを送信し、レスポンスをアラートで表示するシンプルなフォームです。useActionStateを使うことで、useStateを使わずに非同期処理の進行状態やエラーを管理できています。useEffectの依存配列にstateを指定することで、状態変更時にアラートが表示されます。

src/app/page.tsx
'use client';

import { useActionState, useEffect } from "react";
import { Button, Input, Container, Stack, Fieldset, Textarea } from "@chakra-ui/react"
import { Field } from "@/components/ui/field"
import { actionCreatePost } from "@/app/action"

export default function Home() {
  const [state, formAction] = useActionState(actionCreatePost, null);

  useEffect(() => {
    if (!state) {
      return;
    }

    if (state.status === "success") {
      alert(JSON.stringify(state.data));
    } else if (state.status === "error") {
      alert(state.error);
    }
  }, [state]);

  return (
    <Container maxW="sm" py={16}>
      <form action={formAction}>
        <Fieldset.Root size="lg" maxW="md">
          <Stack>
            <Fieldset.Legend>Post</Fieldset.Legend>
            <Fieldset.HelperText>
              Please fill out the form below to create a new post.
            </Fieldset.HelperText>
          </Stack>

          <Fieldset.Content>
            <Field label="タイトル" required>
              <Input name="title" />
            </Field>

            <Field label="コンテンツ" required>
              <Textarea name="body" />
            </Field>
          </Fieldset.Content>

          <Button type="submit" alignSelf="flex-start">
            送信
          </Button>
        </Fieldset.Root>
      </form>
    </Container>
  );
}

アクション関数

フォームデータを受け取り、APIにリクエストを送信するサーバーアクション('use server')です。レスポンスに応じて成功・エラーの状態を返します。

src/app/action.ts
'use server';

type Result = {
  title: string
  body: string
  id: number
}

export const actionCreatePost = async (prevState, formData: FormData) => {
  const title = formData.get("title") as string;
  const body = formData.get("body") as string;

  const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "POST",
    body: JSON.stringify({ title, body }),
    headers: {
      "Content-Type": "application/json",
    },
  });

  if (!response.ok) {
    const error = await response.text();

    return {
      status: 'error',
      data: null,
      error,
    };
  }

  const post = await response.json() as Result;

  return {
    status: 'success',
    data: post,
    error: null,
  };
}

まとめ

useActionStateを使うことで、フォームアクションの状態管理がシンプルになります。useStateより簡潔に非同期処理を管理でき、進行状態・データ・エラーを一元管理できます。フォーム以外の非同期処理にも応用可能なので、ぜひ活用してみてください。

参考

Discussion