React Hook Form: Zodを使ってバリデーション

# React Hook Form と Zod

React Hook Form はフォーム管理管理ライブラリ、
Zodはバリデーションライブラリです。
resolverライブラリを使って、React Hook Form と Zod を統合します。

## Zod使用前のコード

```typescript
import { SubmitHandler, useForm } from "react-hook-form";

// これから変更する点
type FormFields = {
  email: string
  password: string
};

const Form = () => {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    setError,
  } = useForm<FormFields>();　// これから変更する点

  const onSubmit: SubmitHandler<FormFields> = async (data) => {

    try {
      const response = await fetch("https://example.com/api/login", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
      });

      if (!response.ok) {
        throw new Error("Failed to log in");
      }

      console.log("Login successful");
    } catch (error) {
      setError("root", { message: error.message || "Failed to log in" });
    }
  };
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        type="text"
        // これから変更する点
        {...register("email", {
          required: "Email is required",
          validate: (value) => {
            if (!value.includes("@")) {
              return "Email must include @";
            }
            return undefined;
          },
        })}
      />
      {errors.email && (
        <div>{errors.email.message}</div>
      )}
      <input
        type="text"
        // これから変更する点
        {...register("password", {
          required: "Password is required",
          minLength: {
            value: 8,
            message: "Password must have at least 8 characters",
          },
        })}
      />
      {errors.password && (
        <div>{errors.password.message}</div>
      )}
      <button
        type="submit"
        disabled={isSubmitting}
      >
        {isSubmitting ? "Loading..." : "Submit"}
      </button>
      {errors.root && <div>{errors.root.message}</div>}
    </form>
  );
};

export default Form;
```

## Zod使用後のコード

```typescript
import { SubmitHandler, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; // resolverのimport
import { z } from "zod"; // zのimport

// スキーマの作成
const schema = z.object({
  email: z
    .string()
    .email("Please enter a valid email address")
    .nonempty("Email is required"),
  password: z
    .string()
    .min(8, "Password must be at least 8 characters")
    .nonempty("Password is required"),
})

// 型はスキーマから推論
type FormFields = z.infer<typeof schema>

const Form = () => {
  // スキーマを渡す
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    setError,
  } = useForm<FormFields>({
    resolver: zodResolver(schema)
  });

  const onSubmit: SubmitHandler<FormFields> = async (data) => {

    try {
      const response = await fetch("https://example.com/api/login", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
      });

      if (!response.ok) {
        throw new Error("Failed to log in");
      }

      console.log("Login successful");
    } catch (error) {
      setError("root", { message: error.message || "Failed to log in" });
    }
  };
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        type="text"
        // バリデーションはスキーマに移動したので削除
        {...register("email")}
      />
      {errors.email && (
        <div>{errors.email.message}</div>
      )}
      <input
        type="text"
        // バリデーションはスキーマに移動したので削除
        {...register("password")}
      />
      {errors.password && (
        <div>{errors.password.message}</div>
      )}
      <button
        type="submit"
        disabled={isSubmitting}
      >
        {isSubmitting ? "Loading..." : "Submit"}
      </button>
      {errors.root && <div>{errors.root.message}</div>}
    </form>
  );
};

export default Form;
```

### 改善した点

改善した点は以下です。
- スキーマでバリデーションを作成したことによって、１箇所にまとめられた
- スキーマから型を推論することによって、型を自分で作成する必要がなくなった
- register部分にバリデーションを書く必要がなくなり、レンダリングの部分がすっきりした