【備忘録】HTML5のinput type="date"とreact-hook-formを組み合わせたバリデーションチェックがうまくいかなかった件

## 前提：使用技術・ライブラリ

- **HTML5**
- **React**
- **TypeScript**
- **react-hook-form**: フォーム管理ライブラリ
- **zod**: 型バリデーションライブラリ

## 事象
### HTMLのInput type="date"に存在しない日付を入力する
HTMLの`<input type="date">`タグを使用して日付を入力するフォームを作成。
このフォームでは日付の入力が任意項目となっており、zodを使用して入力値が日付形式か否かのバリデーションを行っている。

```tsx
import React from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';

const dateSchema = z.object({
  dateStr: z
  .union([
    z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "日付形式(YYYY-MM-DD)で入力してください"),
    z.literal(""),
    z.undefined(),
  ])
  .transform((val) => (val === "" ? undefined : val)),
});

const MyForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(dateSchema),
  });

  const onSubmit = (data) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input type="date" {...register('dateStr')} />
      {errors.date && <span>{errors.date.message}</span>}
      <button type="submit">Submit</button>
    </form>
  );
};

export default MyForm;
```

### 存在しない日付を入力し、Submitボタンを押下する
例えば「2025-02-29」のような、実際には存在しない日付を入力する。
この場合、期待される動作はバリデーションエラー（入力形式エラー）が発生すること。
しかし、実際にはバリデーションエラーが発生しない……

### 原因
どうやら、存在しない日付を入力すると、`input`タグの`value`が`undefined`になる模様。
今回のフォームで入力する値はundefined許容なので、バリデーションチェックをPASSしてしまっていた。

```tsx
const MyForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(dateSchema),
  });

  const onSubmit = (data) => {
    console.log(data);
    // data.date will be undefined if the date is invalid
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input type="date" {...register('dateStr')} />
      {errors.date && <span>{errors.date.message}</span>}
      <button type="submit">Submit</button>
    </form>
  );
};
```

## 私がとった回避策
### MUI XのDatePickerを使用＆ごにょごにょ
この問題を回避するために、MUI XのDatePickerに乗り換えることにした。
DatePickerの場合、同じ操作を行った際のvalueはundefinedではなく`Invalid Date`になるので、フォーム未入力の場合と存在しない日付を入力した場合とを区別することができたのだ。
MUIは導入済みだったので、UIデザインの統一性という面でも有効だと判断。

ただし、DatePicker自体にそこそこ癖がある印象。
結構ゴニョゴニョ書かないとDatePickerの日付キャストやらなんやらがうまくいかず苦戦。。。
特にonChange関数内で日付型として解釈できるか否かを検証し、必要に応じてreact-hook-formのfieldにセットしないとうまく動いてくれない。
もう少し良い書き方がないか調べたい…………。

以下、備忘録として記載。

**やったこと**
- valueを明示的に日付型にキャストする
- onChange内部で入力値が存在するか否か/日付型として解釈できるか否かをチェックし、react-hook-formで管理しているfiled情報に手動で値（適切/不適切な日付型）をセットする
  - 詳細は下記コードのコメントアウトに記載

```tsx
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { LocalizationProvider, DatePicker } from '@mui/x-date-pickers';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { FormatOptions, format } from "date-fns";
import { ja } from "date-fns/locale";

const dateSchema = z.date().refine((date) => {
  return !isNaN(date.getTime());
}, {
  message: 'Invalid date format',
});

const formatDate = (date: string | number | Date, formatStr: string, options?: FormatOptions) => {
  const customizedOptions: FormatOptions = { ...options, locale: ja };
  return format(date, formatStr, customizedOptions);
};

const MyForm = () => {
  const { control, handleSubmit } = useForm({
    resolver: zodResolver(dateSchema),
  });

  const onSubmit = (data) => {
    console.log(data);
  };

  return (
    <LocalizationProvider dateAdapter={AdapterDateFns}>
      <form onSubmit={handleSubmit(onSubmit)}>
        <Controller
          name="dateStr"
          control={control}
          render={({ field, fieldState }) => (
              <DatePicker
                value={field.value ? new Date(field.value) : undefined}
                inputRef={field.ref}
                onChange={(date) => {
                  // 1. 入力値が存在するか否かチェックする
                  if (date) {
                    // 2. 入力値が日付型として解釈できるかチェックする
                    const isValid = checkIsValidDate(date);
                    if (!isValid) {
                      // 3. 日付が不正な場合は、手動で不正な値をフィールドにセットする(フィールドにバリデーションエラーを発火させるための暫定対応)
                      field.onChange("yyyy-MM-dd HH:ss");
                      console.log("[debug]INVALID DATE");
                    } else {
                      // 4. 日付が正しい場合は、手動で"yyyy-MM-dd"形式の日付文字列をセット
                      field.onChange(formatDate(date, "yyyy-MM-dd"));
                      console.log("[debug]VALID DATE", formatDate(date, "yyyy-MM-dd"));
                    }
                  } else {
                    // 5. 日付がnullの場合は、空文字をセットする
                    field.onChange("");
                    console.log("[debug]date is null");
                  }
                }}
                slotProps={{
                  textField: {
                    error: Boolean(fieldState.error),
                    helperText: fieldState.error?.message,
                  },
                }}
            />
          )}
        />
        <button type="submit">Submit</button>
      </form>
    </LocalizationProvider>
  );
};

export default MyForm;
```