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

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

## 今回やること
Next.jsで`Zod`と`React Hook Form`を組み合わせて使用してログイン機能のフォームバリデーションを実装してみます。

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

- [Zod](https://zod.dev/) 
    - 型安全なスキーマ定義とバリデーションを提供するライブラリ
- [React Hook Form](https://react-hook-form.com/)
    - React のフォーム状態管理を簡単にするためのカスタムフックを使用できるライブラリ

https://zod.dev/

https://react-hook-form.com/

## 実際にやってみる

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

### Zod スキーマ定義
- このスキーマでは、email は文字列で、最低 1 文字以上でメールアドレスの形式であること、password は文字列で、最低 8 文字以上であることが必要です。
```React: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 のバリデーションスキーマが適用されます。

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

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

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

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

  //...省略

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

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

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

※inputタグがコンポーネントになっているのは、radix-ui(shadcn)を使用してます。
```react: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

https://zod.dev/

- React Form Hook
 
https://react-hook-form.com/