先日、Xにて下記の投稿を見つけました!
ReactのuseActionState(フックの詳細は以下リンク)と、shadcn/uiのtoast(詳細は以下リンク)を使った実装をされています。
useActionState
https://ja.react.dev/reference/react/useActionState
toast
https://ui.shadcn.com/docs/components/sonner
Xの実装素晴らしいのですが、私の実装も紹介したいと思い書いてみました。
処理の流れ
1.フォーム入力後、submitボタンを押下
2.form にactin={Action}と記載しているため、useActionStateの処理が始まります。
3.フォーム内のname属性を持つ要素の値をobject形式で取得し、FormDataとしてActionに渡される
4.zodでのバリデーション
5.バックエンドにリクエスト→レスポンス返ってくる
6.正常に処理が終わればtoasterで通知
上記の処理の間、pendingがtrueになりますので、送信中..の表示に変わります。
処理が終われば、falseに変わります、
まずは、useActionStateを使ったフォームづくりです。
下記のように実装します。
// src/app/signup/page.tsx
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 p-4">
<div className="w-full max-w-md p-8 space-y-8 bg-white rounded-lg shadow-md">
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900">アカウント作成</h1>
<p className="mt-2 text-sm text-gray-600">必要情報を入力してアカウントを作成してください</p>
</div>
// action={formAction}を入れる必要があります。
<form action={formAction} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="username" className="text-sm font-medium">ユーザー名</Label>
<Input
type="text"
name="username"
id="username"
placeholder="ユーザー名を入力"
className="w-full px-3 py-2 border rounded-md"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="email" className="text-sm font-medium">メールアドレス</Label>
<Input
type="email"
name="email"
id="email"
placeholder="example@example.com"
className="w-full px-3 py-2 border rounded-md"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-sm font-medium">パスワード</Label>
<Input
type="password"
name="password"
id="password"
placeholder="8文字以上で入力"
className="w-full px-3 py-2 border rounded-md"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword" className="text-sm font-medium">パスワード(確認)</Label>
<Input
type="password"
name="confirmPassword"
id="confirmPassword"
placeholder="同じパスワードを再入力"
className="w-full px-3 py-2 border rounded-md"
required
/>
</div>
// submitされることで、firnActionが動きます。
<Button
type="submit"
className="w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-md transition duration-200"
disabled={isPending}
>
アカウント作成 {isPending && "..."}
</Button>
</form>
<div className="mt-6 text-center text-sm">
<p className="text-gray-600">
すでにアカウントをお持ちですか?{" "}
<a href="/login" className="font-medium text-blue-600 hover:text-blue-500">
ログイン
</a>
</p>
</div>
</div>
</div>
);
};
export default Signup;
続いて、useActionStateのActionを使っていきます。
今回は処理の後半にtoasterを表示させる実装をするのでその部分はクライアントコンポーネントに書いていきます。
// src/app/signup/page.tsx
"use client";
import React from "react";
import { useActionState } from "react";
import { signupServerAction } from "./action";
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Button } from "@/components/ui/button";
import { toast } from "sonner"
const signupAction = async (state: void | null, formData: FormData) => {
const result = await signupServerAction(formData);
if (result.success) {
toast.success("登録に成功しました");
} else {
toast.error("登録に失敗しました");
}
};
const [, formAction, isPending] = useActionState(signupAction, null);
バックエンドのエンドポイントとのやり取りはサーバコンポーネントに書きます。
※本来ならzodでバリデーションを書いたり、ほかにもいろいろ書きますが、今回はサンプルなので必要最小限にしています。
// src/app/signup/action.ts
"use server";
import { apiClient } from "@/lib/api-client";
type SignupResponse = {
userId?: string;
message?: string;
};
export const signupServerAction = async (formData: FormData) => {
const username = formData.get("username") as string;
const email = formData.get("email") as string;
const password = formData.get("password") as string;
// バリデーション
if (!username || !password || !email) {
return { success: false, message: "ユーザー名とパスワードとメールアドレスは必須です" };
}
try {
// APIを呼び出し
const response = await apiClient.post<SignupResponse>('/users/register', {
username,
password,
email
});
if (response.error) {
return { success: false, message: response.error };
}
return { success: true, message: "登録に成功しました", data: response.data };
} catch {
return { success: false, message: "サーバーエラーが発生しました" };
}
};
これで完成です!