【Next.js】react-hook-formを用いたバリデーションチェック

前回Next.jsをインストールしたところで、次はログイン画面を作成していこうと思います。
まず本記事ではログイン画面の認証アクション云々の前に、フォームやバリデーション機能について実装します。
また今後勤怠管理システムを作成していくため、システム名をクラウド労務(CloudWork)として進めていきます。

前回の記事はこちら。

目次

必要パッケージをインストールする

バリデーションチェックを実装するにあたり必要なパッケージをインストールします。

npm install react-hook-form yup @hookform/resolvers

使用するパッケージについて解説します。
react-hook-formは、Reactでフォームの管理を簡単にするためのライブラリです。フォームの状態管理やバリデーションを効率的に行うために使用します。
Yup は、JavaScriptのオブジェクトのスキーマバリデーションを行うためのライブラリです。特にフォームの入力データやオブジェクトの構造が期待通りであるかを検証するために広く使われています。react-hook-formなどのフォームライブラリと併用することで、バリデーションの処理を効率的に行えます。
@hookform/resolvers は、Reactのフォームライブラリであるreact-hook-formと、バリデーションライブラリ(例えばYupやJoiなど)を統合するためのパッケージです。このパッケージを使用することで、バリデーションライブラリのスキーマとreact-hook-formを簡単に連携させることができます。

ログイン画面を作成する

まず、ログイン画面で使用するコンポーネントを作成します。
src/components/elements/form.jsxを作成し、以下を記述します。
これは、ログイン画面で使用するメールやパスワードのinputタグ部分をコンポーネント化しています。
デザインはtailwindcssで装飾してみました。

/**
 * ログイン前フォーム(テキスト)
 * @param {*} param0 
 * @returns 
 */
export const InputTextForm = ({ title, name, errorMessage }) => {
  return (
    <div className="mt-6">
      <label
        htmlFor={name}
        className="mb-2 inline-block text-sm text-gray-700 sm:text-base"
      >
        {title}
      </label>
      <input
        type="text"
        id={name}
        name={name}
        autoComplete={name}
        className="w-full rounded border px-3 py-2 text-gray-700 outline-none ring-emerald-400 transition focus:ring"
      />
      <p className="text-red-500 text-sm mt-1">{errorMessage}</p>
    </div>
  );
};

/**
 * ログイン前フォーム(パスワード)
 * @param {*} param0
 * @returns
 */
export const InputPassForm = ({ title, name, errorMessage }) => {
  return (
    <div className="mt-6">
      <label
        htmlFor={name}
        className="mb-2 inline-block text-sm text-gray-700 sm:text-base"
      >
        {title}
      </label>
      <input
        type="password"
        id={name}
        name={name}
        autoComplete={name}
        className="w-full rounded border px-3 py-2 text-gray-700 outline-none ring-emerald-400 transition focus:ring"
      />
      <p className="text-red-500 text-sm mt-1">{errorMessage}</p>
    </div>
  );
};

次にボタン用のコンポーネントを作成します。
src/components/elements/button.jsx

/**
 * ログイン前ボタン(emerald)
 * @param {*} param0
 * @returns
 */
export const ButtonBgEmerald = ({ title }) => {
  return (
    <div className="mt-6">
      <button
        type="submit"
        className="w-full px-6 py-3 text-sm tracking-wide text-white capitalize transition-colors duration-300 transform bg-emerald-500 rounded-lg hover:bg-emerald-400 focus:outline-none focus:ring focus:ring-emerald-400"
      >
        {title}
      </button>
    </div>
  );
};

次にフッターを作成します。
src/components/templates/footer.jsx

/**
 * フッター(ログイン前)
 * @returns
 */
export const NoAuthFooter = () => {
  return (
    <footer className="mt-auto">
      <div className="container flex flex-col items-center p-1">
        <p className="text-sm text-gray-700">
          © Copyright 2024. Toruko Enginner.
        </p>
      </div>
    </footer>
  );
};

次に見出し用のコンポーネントを作成します。
src/components/elements/head.jsx

/**
 * 見出し1
 * @param {*} param0
 * @returns
 */
export const NoAuthHeading1 = ({ title }) => {
  return (
    <h1 className="text-3xl font-bold text-gray-700 text-center">{title}</h1>
  );
};

そしてレイアウトを作成します。
ヘッダーやフッター、サイドナビが共通であることが多いですが、これらをレイアウトと呼びます。
毎回各ソースに記述するのは面倒なので、レイアウト用のコンポーネントを作成しておき、そちらに記述をまとめます。
src/components/templates/layout.jsx

import Head from "next/head";
import { NoAuthFooter } from "@/components/templates/footer";

/**
 * レイアウト
 * @param {*} param0
 * @returns
 */
export const NoAuthLayout = ({ children }) => {
  return (
    <>
      <Head>
        <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>クラウド労務</title>
      </Head>
      <div className="flex flex-col items-center justify-center h-screen">
        {children}
        <NoAuthFooter />
      </div>
    </>
  );
};

最後にログイン画面用を作成し、各コンポーネントを使用します。
パスワードをお忘れの方はパスワード再発行用の画面を作成する予定ですが、まだ決まっていないのでリンクは#にしておきます。
先ほど作成したNoAuthLayoutを呼び出し、その中にformを作成します。inputタグやbuttonタグはあらかじめ作成しておいたコンポーネントを使用します。
src/pages/auth.jsx

import React from "react";
import { ButtonBgEmerald } from "@/components/elements/button";
import { InputTextForm, InputPassForm } from "@/components/elements/form";
import { NoAuthLayout } from "@/components/templates/layout";
import { NoAuthHeading1 } from "@/components/elements/head";

/**
 * ログインフォーム
 * @returns
 */
export default function AuthForm() {
  return (
    <NoAuthLayout>
      <form className="w-full max-w-md mt-auto">
        <NoAuthHeading1 title="クラウド労務" />
        <InputTextForm
          title="メールアドレス"
          name="email"
          errorMessage={errors.email?.message}
        />
        <InputPassForm
          title="パスワード"
          type="password"
          name="password"
          errorMessage={errors.password?.message}
        />
        <ButtonBgEmerald title="ログイン" />
        <div className="mt-6 text-center ">
          <a
            className="text-sm text-emerald-500 hover:text-emerald-400"
            href="#"
          >
            パスワードをお忘れの方
          </a>
        </div>
      </form>
    </NoAuthLayout>
  );
}

下記コマンドでサーバーを立ち上げ、http://localhost:3000 を表示し、動作を確認します。

cd cloud-work
npm run dev

以下ページが表示されていればOKです。

バリデーションチェックの実装

ログイン画面を作成したところで、次にバリデーションチェックを実装していきます。
まず、バリデーション定義やエラー時のメッセージ用のconfigファイルを作成します。
yup.jp.jsにはバリデーション定義を記載します。各定義とそれに紐づくエラーメッセージを記載します。
今回はパスワードには半角英数字をそれぞれ1種類ずつ使用することを義務付けるため別途正規表現を用意しました。
src/config/yup.jp.js

import * as yup from "yup";

// 半角英(小文字大文字)数字それぞれ1種類ずつ
export const passwordRule = /(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])[0-9a-zA-Z-]*$/;

/**
 * バリデーションルール
 */
const jpLocale = {
  mixed: {
    required: (param) => `${param.label}は必須です。`,
    oneOf: (param) =>
      `${param.label}は${param.values}のいずれかで入力してください。`,
  },
  string: {
    length: (param) =>
      `${param.label}は${param.length}文字ちょうどで入力してください。`,
    min: (param) => `${param.label}は${param.min}文字以上で入力してください。`,
    max: (param) => `${param.label}は${param.max}文字以下で入力してください。`,
    matches: (param) => {
      switch (param.regex) {
        case passwordRule:
          return "半角英大文字英小文字数字をそれぞれ1種類ずつ含んだ形で入力してください。";
        default:
          return `${param.label}は「${param.regex}」形式に一致していなければなりません。`;
      }
    },
    email: (param) => `${param.label}はメールアドレス形式で入力してください。`,
    url: (param) => `${param.label}はURL形式で入力してください。`,
  },
  number: {
    min: (param) => `${param.label}は${param.min}以上で入力してください。`,
    max: (param) => `${param.label}は${param.max}以下で入力してください。`,
    lessThan: (param) =>
      `${param.label}は${param.less}未満で入力してください。`,
    moreThan: (param) =>
      `${param.label}は${param.more}より大きくなければなりません。`,
    positive: (param) => `${param.label}は正数で入力してください。`,
    negative: (param) => `${param.label}は負数で入力してください。`,
    integer: (param) => `${param.label}は整数で入力してください。`,
  },
  date: {
    min: (param) =>
      `${param.label}は${param.min}より未来日で入力してください。`,
    max: (param) =>
      `${param.label}は${param.max}より過去日で入力してください。`,
  },
};

yup.setLocale(jpLocale);
export default yup;

auth.jsxにバリデーション定義を記述します。
schemaにバリデーション内容を定義します。定義したschemaの内容でuseFormを生成します。
inputタグのerrorMessageはreact-hook-formでバリデーションチェックした際にエラーだった文字列を指定するためのものです。
src/pages/auth.jsx

import React from "react";
import { ButtonBgEmerald } from "@/components/elements/button";
import { InputTextForm, InputPassForm } from "@/components/elements/form";
import { NoAuthLayout } from "@/components/templates/layout";
import { NoAuthHeading1 } from "@/components/elements/head";
import yup from "@/config/yup.jp";
import { useRouter } from "next/navigation";
import { passwordRule } from "@/config/yup.jp";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";

/* バリデーション定義 */
const schema = yup.object({
  email: yup.string().label("メールアドレス").required().email(),
  password: yup
    .string()
    .label("パスワード")
    .required()
    .min(8)
    .max(32)
    .matches(passwordRule),
});

/**
 * ログインフォーム
 * @returns
 */
export default function AuthForm() {
  const router = useRouter();
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    defaultValues: {},
    resolver: yupResolver(schema),
  });

  /* ログイン処理 */
  const onSubmit = async (data) => {
    console.log("data", data);
  };

  return (
    <NoAuthLayout>
      <form
        onSubmit={handleSubmit(onSubmit)}
        className="w-full max-w-md mt-auto"
      >
        <NoAuthHeading1 title="クラウド労務" />
        <InputTextForm
          title="メールアドレス"
          name="email"
          errorMessage={errors.email?.message}
          register={register}
        />
        <InputPassForm
          title="パスワード"
          type="password"
          name="password"
          errorMessage={errors.password?.message}
          register={register}
        />
        <ButtonBgEmerald title="ログイン" type="submit" />
        <div className="mt-6 text-center ">
          <a
            className="text-sm text-emerald-500 hover:text-emerald-400"
            href="#"
          >
            パスワードをお忘れの方
          </a>
        </div>
      </form>
    </NoAuthLayout>
  );
}

form.jsxも修正します。(registerの受取を追加)
src/components/elements/form.jsx

/**
 * ログイン前フォーム(テキスト)
 * @param {*} param0
 * @returns
 */
export const InputTextForm = ({ title, name, register, errorMessage }) => {
  return (
    <div className="mt-6">
      <label
        htmlFor={name}
        className="mb-2 inline-block text-sm text-gray-700 sm:text-base"
      >
        {title}
      </label>
      <input
        type="text"
        id={name}
        name={name}
        autoComplete={name}
        className="w-full rounded border px-3 py-2 text-gray-700 outline-none ring-emerald-400 transition focus:ring"
        {...register(name)}
      />
      <p className="text-red-500 text-sm mt-1">{errorMessage}</p>
    </div>
  );
};

/**
 * ログイン前フォーム(パスワード)
 * @param {*} param0
 * @returns
 */
export const InputPassForm = ({ title, name, register, errorMessage }) => {
  return (
    <div className="mt-6">
      <label
        htmlFor={name}
        className="mb-2 inline-block text-sm text-gray-700 sm:text-base"
      >
        {title}
      </label>
      <input
        type="password"
        id={name}
        name={name}
        autoComplete={name}
        className="w-full rounded border px-3 py-2 text-gray-700 outline-none ring-emerald-400 transition focus:ring"
        {...register(name)}
      />
      <p className="text-red-500 text-sm mt-1">{errorMessage}</p>
    </div>
  );
};

画面を操作し、エラーメッセージが表示されているかを確認します。

最後にバリデーションエラーにならないようにフォームを入力して、開発者ツールに入力した内容が表示されれば成功です。

コードは、以下のGitHubリポジトリで確認できます。

よかったらシェアしてね!
  • URLをコピーしました!

コメント

コメントする

目次