zod, MUI, react-hook-formを使ってフォームを作成する

`zod`, `MUI`, `react-hook-form`を使って典型的なフォームを作成しようと思います。
Nextjs(page router)でコードは試しています。

## 環境
`zod 3.23.6`
`@mui/material 5.15.13`
`next 14.1.3`
`react 18`
`react-hook-form 7.51.4`
ユーザー名、メールアドレス、パスワード、性別をの情報を入力するフォームを想定します。

### フォームの内容を定義
```tsx
const Genders = {
  Male: "male",
  Female: "female",
  Other: "other",
} as const;
type Gender = (typeof Genders)[keyof typeof Genders];
const GenderLabels: Record<Gender, string> = {
  [Genders.Male]: "男性",
  [Genders.Female]: "女性",
  [Genders.Other]: "その他",
};


const registrationSchema = z.object({
  username: z.string().min(3, "ユーザー名は3文字以上でなければなりません"),
  email: z.string().email("正しいメールアドレスを入力してください"),
  password: z.string().min(6, "パスワードは6文字以上でなければなりません"),
  gender: z.custom<Gender>(),
});
type RegistrationFormValues = z.infer<typeof registrationSchema>;
```
`registrationSchema`にバリデーション内容を定義し`z.infer`でフォームの型を取得できます。
性別の情報は`Gender`型で定義しています。

### フォームの制御に必要なものを取得
`react-hook-form`からformの制御を行うために必要なものを取得します。

```tsx
const Page: NextPageWithLayout = () => {
  const {
    reset,
    control,
    handleSubmit,
    formState: { errors },
  } = useForm<RegistrationFormValues>({
    resolver: zodResolver(registrationSchema),
    defaultValues: {
      username: "",
      email: "",
      password: "",
      gender: Genders.Other,
    },
  });
}
```

## UIの構築
`mui`を使ってフォームのUIを構築します。

```tsx
const Page: NextPageWithLayout = () => {
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Box display={"flex"} flexDirection={"column"} gap={2}>
        <Controller
          name="username"
          control={control}
          render={({ field }) => (
            <TextField
              {...field}
              label="ユーザー名"
              error={!!errors.username}
              helperText={errors.username?.message}
            />
          )}
        />
        <Controller
          name="email"
          control={control}
          render={({ field }) => (
            <TextField
              {...field}
              label="メールアドレス"
              error={!!errors.email}
              helperText={errors.email?.message}
            />
          )}
        />
        <Controller
          name="password"
          control={control}
          render={({ field }) => (
            <TextField
              {...field}
              label="パスワード"
              error={!!errors.password}
              helperText={errors.password?.message}
            />
          )}
        />
        <FormControl component="fieldset" error={!!errors.gender}>
          <FormLabel component="legend">性別</FormLabel>
          <Controller
            name="gender"
            control={control}
            render={({ field }) => (
              <RadioGroup
                row
                {...field}
                onChange={(e) => {
                  field.onChange(e.target.value as Gender);
                }}
              >
                {Object.entries(GenderLabels).map(([value, label]) => (
                  <FormControlLabel
                    key={value}
                    value={value}
                    label={label}
                    control={<Radio />}
                  />
                ))}
              </RadioGroup>
            )}
          />
          {errors.gender && <div>{errors.gender.message}</div>}
        </FormControl>
        <Button type="submit" variant="contained" color="primary">
          登録
        </Button>
        <Image src={"https://source.unsplash.com/random"} alt="" width={200} height={40} />
      </Box>
    </form>
  );
}
```

### フォームに初期値を設定する
業務ではAPIから値を取得してフォームの初期値に設定するという場面があると思います。
初期値の設定には`react-hook-form`から取得した`reset`関数を使います。

```tsx
const Page: NextPageWithLayout = () => {
  useEffect(() => {
    const fetchData = async () => {
      setTimeout(() => {
        reset({
          username: "test",
          email: "a@b.com",
          password: "password",
          gender: Genders.Male,
        });
      }, 1000);
    };

    fetchData();
  }, [reset]);
}
```