📝
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