White Box技術部

WEB開発のあれこれ(と何か)

React Hook Formで複数項目間のバリデーション(相関チェック)

相関チェックって言葉、そういえばもう全然聞かないなーとは思いつつ、
React Hook Formで、複数項目をまたぐ入力値のチェックを実装したので知見を残しておきます。

↓こういうやつ

手順

ここでは郵便番号のように情報としては1つのものを、複数の入力項目で入力する場合を例にします。 また、手順は相関チェックをする場合に注意する箇所に絞って記載しています。

1. Formの型定義

分割した項目に対応した型(Zipcode)を定義し、それをReact Hook Formで扱うForm項目の型に設定します。

type Zipcode = {
  zip1: string;
  zip2: string;
};

type SampleForm = {
  zipcode: Zipcode;
}

2. バリデーションを設定

バリデーションはregisterのvalidateを使って記述します。 合わせて、チェック対応項目それぞれの変化をバリデーション結果に反映させるために、depsも利用する必要があります。

const validCondition = 
  {
    validate: {
      validateNumber: (_: number, formValues: SampleForm) => {
        const isValid1 = /^[0-9]{3}$/.test(formValues.zipcode.zip1);
        const isValid2 = /^[0-9]{4}$/.test(formValues.zipcode.zip2);
        return (isValid1 && isValid2) || "正しい郵便番号を入力してください";
      },
    },
    deps: ["zipcode.zip1", "zipcode.zip2"],
  };

今回の場合、バリデーションは同じ条件をzip1とzip2の両項目で使うことになります。

3. エラーメッセージを共通化

入力フィールド自体は複数あるので、zipcode.zip1とzipcode.zip2はそれぞれがエラーメッセージを持ちます。 とはいえ、この場合どちらも同じメッセージなので、以下のようにまとめます。

const errorMessage = errors?.zipcode?.zip1?.message || errors?.zipcode?.zip2?.message;

エラーメッセージ以外にも、項目のCSS表現を統一するためには、isDirtyのような状態も共有する必要があります。

4. Formの組み立て

あとは通常と同じようにReact Hook Formを利用してFormを組み立てれば、チェックが行われます。

以下はサンプルです。 InputTextFieldなどはこの記事で触れていない独自コンポーネントですが、概ねinputタグのようなものです。 あと、CSS情報も除いています。

<div>
  <label>
    {label}
    {required && (
      <div>
        <span style={{ marginLeft: "2px", color: "#dc3545" }}> *</span>
      </div>
    )}
  </label>
  <div>
    <div>
      <InputTextField
        type="tel"
        placeholder={"000"}
        required={required}
        invalid={errorMessage ? true : false}
        valid={isDirty}
        {...register(`zipcode.zip1`, validCondition)}
      />
    </div>
    <div>-</div>
    <div>
      <InputTextField
        type="tel"
        placeholder={"0000"}
        required={required}
        invalid={errorMessage ? true : false}
        valid={isDirty}
        {...register(`zipcode.zip2`, validCondition)}
      />
    </div>
  </div>
  <FormFeedback>{errorMessage}</FormFeedback>
</div>

おまけ

↑で触れてなかったInputTextFieldですが、普通のコンポーネントだとregisterを使ったときにrefに関するエラーが出るので、forwardRefを使って対策しています。

import classnames from "classnames/bind";
import { forwardRef } from "react";
import styles from "./index.module.scss";

const cx = classnames.bind(styles);

type Props = React.ComponentPropsWithoutRef<"input"> & {
  valid?: boolean;
  invalid?: boolean;
};

export const InputTextField = forwardRef<HTMLInputElement, Props>(
  (props, ref) => {
    const { valid, invalid, ...rest } = props;
    return (
      <input
        className={cx("input", props.className, {
          readonly: props.disabled || props.readOnly,
          valid: valid,
          invalid: invalid,
        })}
        {...rest}
        ref={ref}
      />
    );
  }
);
InputTextField.displayName = "InputTextField";

参考