🍒

失敗から学ぶ React Hook Form + Zod を使った相関バリデーションの実装

2023/06/13に公開

React Hook Fomr + Zod を使ったフォームで「相関バリデーション」を実装した際に、初手からうまくいかずに試行錯誤しながら実装しました。

失敗ケースとそれをどう解消するかをまとめます。

実装するもの

  • Inputは3つ
    • A: テキストフィールドで、1文字以上を入力必須。
    • B: numberフィールドで、デフォルト0。B+C が3以上の場合に有効。(いわゆる相関チェック、相関バリデーション)
    • C: numberフィールドで、デフォルト0。
  • バリデーションエラーの場合は、各Inputの隣にエラーメッセージを表示
  • バリデーションエラーでない場合は、submitボタンを押せる

実装するサンプル

土台となるフォーム実装

まずはベースとして、バリデーションなしのフォームを実装しましょう。

npm パッケージ

https://github.com/r-tomiyama/sample_rhf-zod/blob/15e0f884847f6bc8e2278a3f599b529ad083e42a/package.json#L5-L14

control を受け取り利用する Input コンポーネントの実装

※ any は、サンプルとして汎用性のあるものを楽して用意するために、妥協して使っています。

テキストフィールド

https://github.com/r-tomiyama/sample_rhf-zod/blob/15e0f884847f6bc8e2278a3f599b529ad083e42a/src/components/Input.tsx#L4-L21

numberフィールド

https://github.com/r-tomiyama/sample_rhf-zod/blob/15e0f884847f6bc8e2278a3f599b529ad083e42a/src/components/NumberInputWithoutTrigger.tsx#L4-L21

スキーマ

https://github.com/r-tomiyama/sample_rhf-zod/blob/a75b0000dbf2521542d79aa0f8410066961f92f0/src/pages/Sample/schema.ts#L3-L9

useForm を呼び出すカスタムフック

https://github.com/r-tomiyama/sample_rhf-zod/blob/a75b0000dbf2521542d79aa0f8410066961f92f0/src/pages/Sample/useSampleForm.ts#L5-L22

フォーム表示

https://github.com/r-tomiyama/sample_rhf-zod/blob/a75b0000dbf2521542d79aa0f8410066961f92f0/src/pages/Sample/index.tsx#L6-L27

バリデーションなしのフォームが完成しました。

バリデーション実装

バリデーションを実装してみます。
フックやフォームを表示するコンポーネントはそのままで、スキーマだけ変更します。

https://github.com/r-tomiyama/sample_rhf-zod/blob/a75b0000dbf2521542d79aa0f8410066961f92f0/src/pages/InvalidSample1/schema.ts#L3-L18

動作確認します。

失敗ケース① エラーになると、transform を通らない

動作確認してみると「プロパティB, C」が要件を満たしたとしても、「プロパティA」が空でエラーの場合には「プロパティB」のエラー表示が消えないことがわかります。

これはメソッドチェーンの根元のz.object({})でエラーになっている場合、transformを通らずにrefineまで到達するためです。
ドキュメントではそういった仕様であることの説明が見つかりませんでしたが、console.logtransformに仕込んでみたところそういう動作になっていることがわかりました。

「プロパティA」のエラーが「プロパティB, C」に影響しないように、次のようにネストした object に変更してみることにします。

z
  .object({
    a: z.string(),
    numbers: z
      .object({
        b: z.string(),
        c: z.string(),
      })
      ...

※ ちなみに、transformを通らない場合にrefineに至ってほしくないだけであれば、refineの前にcatch(() => z.NEVER)を挟むだけとかでもよいです。

スキーマをネストした object に変更

先程の反省をコードに反映します。

まずスキーマを次のようにします。

https://github.com/r-tomiyama/sample_rhf-zod/blob/a75b0000dbf2521542d79aa0f8410066961f92f0/src/pages/InvalidSample2/schema.ts#L3-L24

refinepath['b']のままにしておくことが注意点です。
この書き方でエラーになった際のpath['numbers', 'b']になるようです。
ドキュメントには appended to error path としか書いてなかった(と思う)ので、ミスりやすいポイントです。

伴って、フックのデフォルト値を変更します。

https://github.com/r-tomiyama/sample_rhf-zod/blob/a75b0000dbf2521542d79aa0f8410066961f92f0/src/pages/InvalidSample2/useSampleForm.ts#L7-L13

Inputコンポーネントの呼び出しも次のように変更します。

https://github.com/r-tomiyama/sample_rhf-zod/blob/a75b0000dbf2521542d79aa0f8410066961f92f0/src/pages/InvalidSample2/index.tsx#L16-L18

変更が完了したので、動作確認をします。

先程の問題は解消しました。「プロパティA」が空であっても、「プロパティB, C」のエラー表示は適切になっています。

失敗ケース② 相関バリデーションのエラーが表示されない場合がある

ただし今度は別の問題が見つかります(正確には、失敗ケース1の時から起きている事象ですが)。

「プロパティC」を変更し「プロパティB, C」が有効になるようにしても、「プロパティB, C」のエラー表示が消えません。
ただ submit ボタンは押せるようになっているので、formState は適切に更新されているようです。

どうやら「プロパティC」が変更されても「プロパティB」は再レンダリングされないようです。
次のようにControllerの render 関数 にconsole.logを仕込むとそれが確認できます。

export default function NumberInput(props: {
  name: any;
  control: Control<FieldValues, any>;
}) {
  return (
    <Controller
      name={props.name}
      control={props.control}
      render={({ field, fieldState: { error } }) => {
        // ここ
        console.log(props);
        return (
          <div>
            <label>プロパティ{props.name}: </label>
            <input type="number" {...field} />
            {error?.message}
          </div>
        );
      }}
    />
  );
}

こういうときは、triggerを使うと良さそうです。

trigger を使う

まずは Input コンポーネントを trigger を受け取るものに変更します。

https://github.com/r-tomiyama/sample_rhf-zod/blob/a75b0000dbf2521542d79aa0f8410066961f92f0/src/components/NumberInput.tsx#L9-L37

最後に、フックが返すtriggerを Input コンポーネントに渡します。

https://github.com/r-tomiyama/sample_rhf-zod/blob/a75b0000dbf2521542d79aa0f8410066961f92f0/src/pages/ValidSample/index.tsx#L7-L12

https://github.com/r-tomiyama/sample_rhf-zod/blob/a75b0000dbf2521542d79aa0f8410066961f92f0/src/pages/ValidSample/index.tsx#L19-L24

完成しました。
動作確認すると、正常に動いてることがわかります。

終わりに

今回利用したコードはこちらにあります。

Discussion