1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsのuseActionStateを使ったトースト通知。私の書き方

Posted at

先日、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: "サーバーエラーが発生しました" };
  }
};


これで完成です!

成功すれば下記のように動きます。
Image from Gyazo

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?