Nextjs14 Sever ActionとZodでバリデーションを実装する

投稿日 2024-06-06

更新日 2024-08-18

このブログ記事では、Next.js 14、Zod、Server Actionを使用して、サーバーサイドでフォームのバリデーションを行い、エラーメッセージをフォーム内に表示する方法を説明します。

はじめに

このブログ記事では、ZodとServer Actionを使用して、Next.js 14でサーバーサイドのバリデーションを実装する方法を説明します。Zodは、強力で使いやすいスキーマ定義ライブラリです。Server Actionは、Next.js 14で導入された新しい機能で、サーバーサイドでフォームデータを処理することができます。

バリデーションのセットアップ

Zodのインストール

以下のコマンドでZodをインストールします。

Command
npm i zod

スキーマの定義

続いてバリデーションを定義します。

今回はコンタクトフォームのバリデーションを行います。

下記はお名前1文字以下、メールアドレスのフォーマットミス、メッセージ1文字以下の場合、各{message:}で定義した文字列でバリデーションエラーを返します。

lib/validate.ts
import { z } from 'zod';

export const validate = z.object({
  name: z.string().min(1, { message: 'Name is required' }),
  email: z.string().email({ message: 'Invalid email' }),
  message: z.string().min(1, { message: 'Message is required' }),
});

ServerActionの実装

lib/ServerAction.ts
import { validate } from './validate';

export type Errors = {
  erros?: {
    name?: string[];
    email?: string[];
    message?: string[];
  };
  message?: null | string;
};

import { validate } from './validate';

export type Errors = {
  errors?: {
    name?: string[];
    email?: string[];
    message?: string[];
  };
  message?: null | string;
};

export async function ServerAction(prevState: any, formData: FormData) {
  const validateResult = validate.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    message: formData.get('message'),
  });
  await new Promise((resolve) => setTimeout(resolve, 2000));
  if (!validateResult.success) {
    const errors = {
      errors: validateResult.error.flatten().fieldErrors,
      message: 'Validation error',
    };
    return errors;
  }
  // これ以下にサーバーとの通信処理を書く
}

フォームの実装

app/page.tsx
'use client';
import { ServerAction } from './lib/ServerAction';
import { Errors } from './lib/ServerAction';
import { useRef } from 'react';
import { useFormState, useFormStatus } from 'react-dom';
import { useRouter } from 'next/navigation';
import Buton from '@/components/button';
export default function Page() {
  const initialState: Errors = {
    errors: {
      name: [],
      email: [],
      message: [],
    },
    message: null,
  };
  const formRef = useRef<HTMLFormElement>(null);
  const [state, dispatch] = useFormState(ServerAction, initialState);
  const router = useRouter();
  const { pending } = useFormStatus();

  return (
    <div className='mx-64 my-10 rounded-lg bg-white '>
      <form
        action={async (payload: FormData) => {
          dispatch(payload);
          if (state?.errors === null) {
            router.push('/success');
          }
        }}
        ref={formRef}
        className='p-8 text-xl font-medium space-y-4'
      >
        {state?.message && (
          <p className='text-red-500 text-2xl font-bold'>{state.message}</p>
        )}
        <label className=''>Name</label>
        <input
          name='name'
          className='w-full p-3 rounded-xl border border-black'
        />
        {state?.errors?.name && (
          <p className='text-red-500 text-xs'>{state.errors?.name}</p>
        )}
        <label>Email</label>
        <input
          name='email'
          className='w-full p-3 rounded-xl border border-black'
        />
        {state?.errors?.email && (
          <p className='text-red-500 text-xs'>{state.errors?.email}</p>
        )}
        <label>Message</label>
        <textarea
          name='message'
          className='w-full rounded-xl border border-black'
          rows={5}
        />
        {state?.errors?.message && (
          <p className='text-red-500 text-xs mt-1'>{state.errors?.message}</p>
        )}
        <Buton />
      </form>
    </div>
  );
}

ついでにuseFormStatusを使用してペンディングも実装しました。

ペンディングはボタンをコンポーネント化しないとエラーが出でしまいます。原因はわかりません。

以下ボタンコンポーネントです。

components/button.tsx
import { useFormStatus } from 'react-dom';
const Buton: React.FC = () => {
  const { pending } = useFormStatus();
  return (
    <button
      type='submit'
      className='px-8 py-3 bg-blue-500 text-white rounded-md'
      disabled={pending}
    >
      {pending ? 'Sending' : ' Send '}
    </button>
  );
};

export default Buton;

まとめ

Nextjs14でZodとServer Actionを使用してサーバーサイドでバリデーションを行いエラーメッセージをフォーム内に表示させました。

今回はuseFormStateで実装しましたが、Nextjs15RCからuseServerAction が使用可能になりました。Nextjs15がstableとなった時またuseActionStateを紹介したいと思います。

また、formのactionをpayloadを引数にすることでサーバーでの処理が終了した後クライアントでの処理を記述することができます。この情報はネットで見つけることができなかったので共有させていただきました。どなたかの参考になれば幸いです。

それではまたお会いしましょう。

参考文献