グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



React Hook FormでZodを使ってフォームの入力値をAPIのデータ形式に変換したい!

React Hook FormとZod便利ですよね。

個人的には特にZodを使うことで、不確定なデータをZodのパースを通した後は型安全に扱えるようになることや、parse don’t validateな雰囲気が気に入っています。

今回は React Hook Form と Zod を使用した入力フォームを API のデータ形式に変換していきたいと思います。

React Hook FormとZod

まずはフォームの入力欄が年月日ごとに別れている生年月日を入力するフォームを作成したいとします。

React Hook FormとZodを使うと粗方このようなコードになるでしょうか。

import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

import "./App.css";

const schema = z.object({
  birthday: z.object({
    year: z.string(),
    month: z.string(),
    day: z.string(),
  }),
});

type Schema = z.infer<typeof schema>;

function App() {
  const { handleSubmit, register } = useForm<Schema>({
    defaultValues: {
      birthday: {
        year: "",
        month: "",
        day: "",
      },
    },
    resolver: zodResolver(schema),
  });

  const onSubmit = handleSubmit((input: Schema) => {
    console.log(input);
  });

  return (
    <form onSubmit={onSubmit}>
      <fieldset>
        <legend>生年月日</legend>
        <input type="text" id="year" {...register("birthday.year")} />
        <label htmlFor="year">年</label>
        <input type="text" id="month" {...register("birthday.month")} />
        <label htmlFor="month">月</label>
        <input type="text" id="day" {...register("birthday.day")} />
        <label htmlFor="day">日</label>
      </fieldset>
      <button>submit</button>
    </form>
  );
}

export default App;

上記の例ではフォームの入力値をconsole.logで出力しているだけですが、実際にはサーバ(Web API)に送信することになるでしょう。
この際、入力フォームの形式は年月日が複数のプロパティに分かれたオブジェクトですが、API ではRFC3339フォーマットの文字列を受け付けるといった、データの形式が入力フォームの形式と違うということが稀によくあります。

こういった時に、Zodのtransformを使うと入力フォームとAPIに送信するデータの形式のズレに対応することができるようになります。

Zodのtransformの例

const schema = z.object({
  birthday: z.object({
    year: z.string(),
    month: z.string(),
    day: z.string(),
  }),
});

例では入力フォームの形式に合わせてZodのスキーマを定義しています。
このスキーマにZodのtransformを追加します。

参照:https://zod.dev/?id=transform

const schema = z.object({
  birthday: z
    .object({
      year: z.string(),
      month: z.string(),
      day: z.string(),
    })
    .transform(({ year, month, day }) => {
      // 簡易的な日付の文字列変換処理です
      return new Date(`${year}-${month}-${day}`).toISOString();
    }),
});

Zodのtransformを使うことでZodスキーマのパース結果がAPIのデータ形式に変換されるようになります。

Zodのtransformを使った時のReact Hook FormとTypeScriptの型の問題

transformを追加したZodスキーマをそのままzodResolver関数の引数に渡すと、以下のようにTypeScriptのエラーが発生します。

Type '{ year: string; month: string; day: string; }' is not assignable to type 'string'. (tsserver 2322)

これは入力フォームの型と API が受け付ける型のズレによるものです。

// 入力フォームの型
type Schema = {
  birthday: {
    year: string;
    month: string;
    day: string;
  };
};

// transform後(APIが受けつける)の型
type Schema = {
  birthday: string;
};

React Hook Formのv7.44.0からuseFormが変換前と変換後のジェネリクス型に対応した

公式ドキュメントのリファレンスにはまだ記載がありませんが、最近のアップデートで useForm の型引数に変換後の型を取れるようになりました。

このアップデートによって、React Hook FormのuseForm関数の型引数に変換前と変換後の型を渡すことでデータ形式のズレを解消しつつ、TypeScriptのエラーも解消できるようになりました。

z.inferで型を作成していた処理を、z.inputとz.outputに変更して、変換前と変換後の型を作成します。

// z.infer から z.input と z.output で変換前と変換後の型を作成
// type Schmea = z.infer<typeof schema>;
type Input = z.input<typeof schema>;
type Output = z.output<typeof schema>;

React Hook FormのuseForm関数の型引数に変換前と変換後の型を渡します。

const { handleSubmit, register } = useForm<Input, unknown, Output>({
  // defaultValues は変換前の型です
  defaultValues: {
    birthday: {
      year: "",
      month: "",
      day: "",
    },
  },
  resolver: zodResolver(schema),
});

最後に、React Hook Form の handleSubmit 関数に渡す関数の引数の型を、変換後の型に変更します。

// Zodスキーマのパース後(RHFのバリデーション通過後)は変換後の型になる
const onSubmit = handleSubmit((input: Output) => {
  console.log(input);
});

まとめ

変更後のコードの全体を記載します。

import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

import "./App.css";

const schema = z.object({
  birthday: z
    .object({
      year: z.string(),
      month: z.string(),
      day: z.string(),
    })
    .transform(({ year, month, day }) => {
      // 簡易的な日付の文字列変換処理です
      return new Date(`${year}-${month}-${day}`).toISOString();
    }),
});

// z.infer から z.input と z.output で変換前と変換後の型を作成
// type Schmea = z.infer<typeof schema>;
type Input = z.input<typeof schema>;
type Output = z.output<typeof schema>;

function App() {
  // useFormの型引数に変換前と変換後の型を渡す
  const { handleSubmit, register } = useForm<Input, unknown, Output>({
    defaultValues: {
      birthday: {
        year: "",
        month: "",
        day: "",
      },
    },
    resolver: zodResolver(schema),
  });

  // Zodスキーマのパース後(RHFのバリデーション通過後)は変換後の型になる
  const onSubmit = handleSubmit((input: Output) => {
    console.log(input);
  });

  return (
    <form onSubmit={onSubmit}>
      <fieldset>
        <legend>生年月日</legend>
        <input type="text" id="year" {...register("birthday.year")} />
        <label htmlFor="year">年</label>
        <input type="text" id="month" {...register("birthday.month")} />
        <label htmlFor="month">月</label>
        <input type="text" id="day" {...register("birthday.day")} />
        <label htmlFor="day">日</label>
      </fieldset>
      <button>submit</button>
    </form>
  );
}

export default App;

入力フォームでのデータとAPIでデータの形式が違うといったことは稀によくあります。(2回目)

こういった痒いところに手が届くアップデートが入るのはうれしいですね。

この記事を書いた人

美髭公
美髭公事業開発部 web application engineer
2013年にアーティスに入社。システムエンジニアとしてアーティスCMSを使用したWebサイトや受託システムの構築・保守に携わる。環境構築が好き。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします