10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Next.js】ZodとReact Hook Formを使ってフォームバリデーションを実装してみた

Last updated at Posted at 2023-05-21

はじめに

この記事では、React(Next.js)で最近よく聞くTSと相性が良いZodとReactでよく使われているReact Hook Formを使ってフォームバリデーションの実装をしてみて、とても簡単で便利だなと感じたので紹介してみます。

今回やること

Next.jsでZodReact Hook Formを組み合わせて使用してログイン機能のフォームバリデーションを実装してみます。

前提(準備)

今回下記2つを用いてフォームバリデーションを実装します。
自分の環境はフロントにReact(Next.js)とサーバーサイドにAPIサーバとしてPHP(Laravel)を使っています。

  • Zod
    • 型安全なスキーマ定義とバリデーションを提供するライブラリ
  • React Hook Form
    • React のフォーム状態管理を簡単にするためのカスタムフックを使用できるライブラリ

実際にやってみる

概要

以下のコードは、Next.js アプリケーションでログイン機能を実装するための一例です。
ログインフォームにメールアドレスとパスワードを入力し、送信するとサーバーでバリデーションが行われます。
バリデーションエラーがある場合、エラーメッセージが表示されます。

Zod スキーマ定義

  • このスキーマでは、email は文字列で、最低 1 文字以上でメールアドレスの形式であること、password は文字列で、最低 8 文字以上であることが必要です。
useLogin.ts
const schema = z.object({
  email: z
    .string()
    .min(1, { message: '入力必須です' })
    .email({ message: 'メールアドレスの形式ではありません' }),
  password: z.string().min(8, { message: '8文字以上入力する必要があります' }),
});

useForm の設定

次に、React Hook Form の useForm を使ってフォームの状態管理を設定します。
resolver オプションに zodResolver(schema) を渡すことで、Zod のバリデーションスキーマが適用されます。

useLogin.ts
const {
  register,
  watch,
  handleSubmit,
  formState: { errors, isDirty },
} = useForm({
  resolver: zodResolver(schema),
});

useForm() からはいくつかの関数が返されますが、今回は必要なものに限定してます。
register はフォーム要素に適用するための属性を提供し、watch はフォームの現在の値を取得するために使用されます。
handleSubmit はフォームの送信処理を行います。
また、formState から errorsisDirty を取得しています。
errors はバリデーションエラーの情報が含まれ、isDirty はフォームの値が変更されたかどうかを示すboolean値です。

ログイン処理(カスタムフック)

下記のuseLogin()をカスタムフックとして追加して、その中にlogin関数を追加します。
まず CSRF 保護のために /sanctum/csrf-cookie エンドポイントを呼び出します(今回の内容には関係ないため説明はなし)
次に、入力されたメールアドレスとパスワードを /login エンドポイントに POST してログイン処理を行います。ログインに成功した場合、ホームページに遷移します。
ここでは、入力された値を先程useForm()から取得したwatch()関数から取得できます。

useLogin.ts
const useLogin = () => {
    const router = useRouter();
    const [validationMessage, setValidationMessage] = useState('');
    
    const login = () => {
      // Submit時の入力値を取得
      const inputData = watch();
    
      axiosApi
        // CSRF保護の初期化
        .get('/sanctum/csrf-cookie')
        .then((res) => {
          // ログイン処理
          axiosApi
            .post('/login', inputData)
            .then((response: AxiosResponse) => {
              // ログイン成功
              router.push('/home');
            })
            // 省略: エラーハンドリング
        });
    };

    return {
      validationMessage,
      login,
      register,
      handleSubmit,
      errors,
      isDirty,
    };
}

useFormから返される各関数の説明:

  • register: フォームの入力要素に渡す関数。name属性、ref、onBlur、onChangeが設定されます。
  • watch: 入力値をリアルタイムで取得するための関数。
  • handleSubmit: Submit時の処理をラップする関数。
  • errors: バリデーションエラーを取得するためのオブジェクト。
  • isDirty: 入力値が変更されたかどうかを示すブール値。

このフックでは、ログイン処理が成功した場合、ユーザーはホームページにリダイレクトされます。バリデーションエラーが発生した場合は、エラーメッセージが表示されます。

Loginコンポーネント

Loginコンポーネントでは、useLoginフックを使用してログインフォームを実装します。

Login.tsx
const Login: React.FC = () => {
  const { login, validationMessage, register, handleSubmit, errors, isDirty } =
    useLogin();

  //...省略

  return (
    <>
      {/* ここにフォームの実装が続く */}
    </>
  );
};

フォームの実装では、register関数を各入力要素に渡すことで、フォームの状態を管理します。
また、errorsオブジェクトを使用してバリデーションエラーメッセージを表示します。

そして、handleSubmit関数をフォームのonSubmitイベントに渡します。これにより、フォームがサブミットされたときに先程カスタムフックにおいたlogin関数が実行されます。

※inputタグがコンポーネントになっているのは、radix-ui(shadcn)を使用してます。

Login.tsx
//...省略

<form onSubmit={handleSubmit(login)}>
    <Input
      id='email'
      type='email'
      placeholder='test@mail.com'
      {...register('email')} // name, ref, onChange, onBlurが設定される
    />
    
    {errors.email && (
      <p className='py-3 text-sm'>
        {errors.email.message as string}
      </p>
    )}
</form>

//...省略

最後に、handleSubmit関数をフォームのonSubmitイベントに渡します。これにより、フォームがサブミットされたときにlogin関数が実行されます。

終わりに

最初はregsterあたりが小難しく感じたのですが、実際に使ってみると普通にフォームバリデーションを実装してたときよりも、コードの記述力がとても減りました!
そしてなんといってもシンプルだしstate管理などを書かなくてよくなったのが楽ですね。
Unontrolled Componentsのパターン(フォームやインプットなどのコンポーネントが、コンポーネント内部で値を管理して外部に依存しない)を採用しており、レンダリング回数を減らすことが出来るのも◯です。

zodも同様シンプルで実装しやすく型安全が確保できるので、よき組み合わせでした!

参考

  • zod

  • React Form Hook

10
4
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
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?