【React Hook Form】Reactでリッチ&モダンな入力値検証を作ろう!

こんにちは。エヌデーデーの金子です。
Reactを使ったWebアプリケーションの入力値検証の実装を、React Hook Formというライブラリを使ってチャレンジしたというお話です。

React Hook Formとは、React開発におけるフォーム管理をモダンなUIで実現してくれるものです。
後述しますがこのライブラリ、フォーム送信を前提とした作りです。

今回対象のシステムはReact×APIのステートレス構成で、Stateで入力値を管理しているためフォーム送信は必要ありませんでした。
というコトで、あくまでクライアントの入力値検証機能としてReact Hook Formを使います。
同じようなシステム構成で本ライブラリの使用を検討している方の一助となれば、幸いです‼️

【使用バージョン】
react 18.2.0
typescript 4.7.4
react-hook-form 7.43.9

React Hook Formとは

React Hook FormはUncontrolled Componentsというフォーム要素の値をDOMそのものが管理するという思想で作られています。再レンダリングを分離することによってハイパフォーマンスなUIを提供するよ、というコトです。

下の図は左がReact Hook Form、右が通常のReactコントロールフォームです。
React Hook Formは値が更新されてもコンポーネントの再レンダリングが走っていないことが分かります。

こういった設計思想であるため、Formタグを使って最終的にSubmitするというSSR(Server Side Rendering)ライクな実装例ばかり出てきます。
公式のコードサンプルもSubmitボタンがある前提です。

ユースケースとしてReact Hook Formは独立して使用され、ReactのStateと直接結び付けることは少ないみたいです。これは、React Hook Formが内部でフォームの状態管理を行い、追加のState管理が不要であるためですね。

しかしReactを使っているのだからStateやContextを活用したReactコントロールの画面を作りたい、、そこにReact Hook Formの一部機能だけ組み込めないか?というのが今回やりたいことの概要となります。

※やってみての教訓ですが、1つの入力値をStateとReact Hook Formの2つで制御する。。という煩雑なコードになってしまうため基本的にはどちらかに統一することをお勧めします。※

使用するAPIの概要

メインで使用するAPIはuseFormです。
useFormの中に色々有用なメソッドが用意されていますので、これらを駆使していきます。

メソッド概要
resistername属性を指定してバリデーションルールを設定する。主役。
handleSubmit指定された関数にフォームデータをオブジェクトで渡す。今回は使わない。
setErrorname属性を指定して無理やりエラーを引き起こす。
clearErrorsname属性の配列を渡し、対象のエラー状態を解除。
getValues対象項目の現在値を取得。
setValue対象項目に値をセットする。Stateとは異なり、再レンダリングされない。
triggerバリデーションを発火させる。
formStateフォーム内の入力状態、検証状態を管理する。

フォーム送信が前提であれば、handleSubmitを使用しuseFormのデフォルトでよろしくやってくれます。
が、handleSubmitを使用すると当然ですが何処かでSubmitをする必要があり、これをボタンで実装すると。。

Enterキー押下でブラウザ標準で自動送信され、予期せぬタイミングで検証が走ってしまう

というコトが起き得るので、今回は使いません。

入力値検証方針

まず今回の実装例でどのような振る舞いになるのかを記載します。

1.特定のボタンを押下すると検証が行われる。

画面に来て、特定のボタンを押したらフィールドがエラー状態になります。
例えば必須フィールドに対して入力⇒入力内容破棄などの操作をしても、ボタンを押すまでは検証されません。
検証に引っかかったフィールドは赤くなり、付近にエラーメッセージが出力されます。

2.ボタン押下後、入力フィールドに対して再検証が行われる。

ボタン押下後は項目に監視が走り、必須や文字型などのチェックが入力中に起きるようになります。

3.クライアント検証を終えたらサーバ検証を行う。

サーバ検証のメッセージは「画面上部」と「対象項目付近」に出るようにします。
項目をエラー状態にするのはuseFormの関数で実現できます。
しかし1つの項目に対して複数のエラー状態を管理する事は出来ずに上書きされます
※図では項目1に対して2つエラーが出ていますが、これを同時に保持は出来ません。

ですので、サーバエラーは画面上部のエラーメッセージコンポーネントを自作して漏れなく出力させます。

ソースコードと解説

まずはカンタンな検証画面から

まずは必須で入力形式制限のあるフィールドを一個作ってみましょう。
使用するメソッドはregisterです。
第一引数にname属性、第二引数に検証ルールを渡します。
このname属性でクライアント上のエラーを管理するため、画面上で一意になるように設定します。

※ソース内で出てくるクラスはBootstrapを使用しています。

<input
  type="text"
  className={"w-25 required"}
  {...register("test1", {
    required: "項目1(全角フィールド)を入力してください。",
    pattern: {
      value: /^[^ -~。-゚]+$/,
      message: "項目1(全角フィールド)は全角50文字以内で入力してください。",
    },
    maxLength: {
      value: 50,
      message: "項目1(全角フィールド)は全角50文字以内で入力してください。",
    },
  })}
/>

formStateは項目のエラー状態を管理します。
errorsプロパティにエラーオブジェクトが格納され、registerで登録したname属性でアクセスすることが出来ます。
対象がエラーであればスタイルを変えたりメッセージを付近に出力します。

<input
  type="text"
  className={`w-25 required${
    formState.errors.test1 ? " form-control bg-error" : " form-control"
  }`}
  {...register("test1", {
    required: "項目1(全角フィールド)を入力してください。",
    pattern: {
      value: /^[^ -~。-゚]+$/,
      message: "項目1(全角フィールド)は全角50文字以内で入力してください。",
    },
    maxLength: {
      value: 50,
      message: "項目1(全角フィールド)は全角50文字以内で入力してください。",
    },
  })}
/>
<p className="text-danger">
  {/*  エラーメッセージ */}
  {formState.errors.test1?.message as string}
</p>

続いてtriggerでクライアント検証を発火させる関数を作り、ボタン押下で呼び出します。
この時、setValueのオプションで「変更時に再検証をするかどうか」の設定が出来ます。
この設定を画面上のStateで一律管理することで再検証のタイミングをカスタムします。
また、setFocusを使用してエラー時に対象項目へジャンプさせています。

// 再検証のタイミングを管理するState
const [isStartValidation, setIsStartValidation] = useState<boolean>(false);

// 検証発火関数
const onClickFunc = async () => {
    // 再検証フラグ立てる
    setIsStartValidation(true);

    // 全項目に検証走らせる
    await trigger().then((result) => {
      if (result) {
        // ここのスコープに来たらクライアント検証OK。サーバ検証を呼ぶ処理を書く。
      }
    });
  };

// triggerでformStateの状態を変えたときに一番上にフォーカスさせる
useEffect(() => {
    const firstErrorKey = Object.keys(formState.errors)[0];
    setFocus(firstErrorKey);
  }, [formState.errors]);

return (
    <>
      <div className="container-fluid">
        <div className="middiumText">
          {/*  項目1 */}
          <input
            type="text"
            className={`w-25 required${
              formState.errors.test1
                ? " form-control bg-error"
                : " form-control"
            }`}
            {...register("test1", {
              required: "項目1(全角フィールド)を入力してください。",
              pattern: {
              value: /^[^ -~。-゚]+$/,
              message: "項目1(全角フィールド)は全角50文字以内で入力してください。",
              },
              maxLength: {
              value: 50,
              message: "項目1(全角フィールド)は全角50文字以内で入力してください。",
              },
            })},
            onChange={(e: ChangeEvent<HTMLInputElement>) => {
              setValue("test1", e.target.value, {
                shouldValidate: isStartValidation, // ボタン押下後であれば再検証を行う
              });
            }}
          />
          <p className="text-danger">
            {formState.errors.test1?.message as string}
          </p>
        </div>
        <button
          type="button"
          className="btn btn-primary"
          onClick={() => {
            void onClickFunc();
          }}
        >
          検証
        </button>
      </div>
    </>
  );

これで軽い検証画面が出来ました!

ソースの全体像はこんな感じ。

import { ChangeEvent, useEffect, useState } from "react";
import { useForm } from "react-hook-form";

function ValidationComponent() {
  // 検証ルール
  const validationRule = {
    required: "項目1(全角フィールド)を入力してください。",
    pattern: {
      value: /^[^ -~。-゚]+$/,
      message: "項目1(全角フィールド)は全角50文字以内で入力してください。",
    },
    maxLength: {
      value: 50,
      message: "項目1(全角フィールド)は全角50文字以内で入力してください。",
    },
  };

  /**
   * バリデーション関連設定
   */
  const [isStartValidation, setIsStartValidation] = useState<boolean>(false);
  const {
    register, // input項目に登録する関数
    formState, // フォームの状態を管理
    setValue, // バリューセット
    trigger, // 検証発火
    setFocus, // 要素フォーカス
  } = useForm();

  const onClickFunc = async () => {
    // 再検証フラグ立てる
    setIsStartValidation(true);

    // 全項目に検証走らせる
    await trigger().then((result) => {
      if (result) {
        // クライアント検証がOKなら、サーバ通信を行う。
      }
    });
  };
  // triggerでformStateの状態を変えたときに一番上にフォーカスさせる
  useEffect(() => {
    const firstErrorKey = Object.keys(formState.errors)[0];
    setFocus(firstErrorKey);
  }, [formState.errors]);

  return (
    <>
      <div className="container-fluid">
        <div className="middiumText">
          {/*  項目1 */}
          <input
            type="text"
            className={`w-25 required${
              formState.errors.test1
                ? " form-control bg-error"
                : " form-control"
            }`}
            {...register("test1", {
              required: validationRule.required,
              pattern: validationRule.pattern,
              maxLength: validationRule.maxLength,
            })}
            onChange={(e: ChangeEvent<HTMLInputElement>) => {
              setValue("test1", e.target.value, {
                shouldValidate: isStartValidation, // ボタン押下後であれば再検証を行う
              });
            }}
          />
          <p className="text-danger">
            {formState.errors.test1?.message as string}
          </p>
        </div>
        <button
          type="button"
          className="btn btn-primary"
          onClick={() => {
            void onClickFunc();
          }}
        >
          検証
        </button>
      </div>
    </>
  );
}

export default ValidationComponent;

サーバーエラーを扱うには

では続いてサーバー検証エラーについての処理を作っていきます。
まずメッセージを画面上部に出力するコンポーネントから。
エラーメッセージの配列を受け取るように作ります。
※ソースにfontawesomeのアイコンを使っていますので、ご注意を!

/**
 * サーバーエラーメッセージを管理するコンポーネント
 * @param
 * @returns
 */
function ServerErrorMsgComponent(errorMsg: string[] | undefined) {
  return (
    <div className="row">
      <div className="col-sm-12 col-md-12 col-lg-12 col-xl-12">
        <section
          id="sec00"
          className="sec secError"
          style={{ display: errorMsg ? "" : "none" }}
        >
          <div className="container-fluid alertArea">
            {errorMsg?.map((msg) => (
              <div className="errorTilte">
                <div className="info-left">
                  <p className="errorMsgdesign">
                    <i
                      className="fa fa-exclamation-triangle"
                      aria-hidden="true"
                    />
                     {msg}
                  </p>
                </div>
              </div>
            ))}
          </div>
        </section>
      </div>
    </div>
  );
}
export default ServerErrorMsgComponent;

次に画面側に戻ってサーバーエラーメッセージを取り扱う処理を作ります。
サーバーエラーは以下のオブジェクトで返ってくるモノと仮定します。
ここはAPIの処理に大きく依存するところなので、エラーオブジェクトは適宜調整をしてみてください。

interface ErrorContents {
   key: string; // 対象項目のname
   values: string[]; // エラーメッセージ
}

サーバーエラーメッセージはState管理します。
対象項目付近にはsetErrorを使ってエラー状態を無理やり引き起こします。
type=customにすればregisterで登録した検証ルールとは別のエラーと出来ますが、
検証ルールはサーバーにしかないため再検証時に項目のエラー状態は解放されますのでご注意を。

import { ChangeEvent, useEffect, useState } from "react";
import { useForm } from "react-hook-form";

function ValidationComponent() {
  // 検証ルール
  const validationRule = {
    required: "項目1(全角フィールド)を入力してください。",
    pattern: {
      value: /^[^ -~。-゚]+$/,
      message: "項目1(全角フィールド)は全角50文字以内で入力してください。",
    },
    maxLength: {
      value: 50,
      message: "項目1(全角フィールド)は全角50文字以内で入力してください。",
    },
  };

  /**
   * バリデーション関連設定
   */
  const [errorMsgList, setErrorMsgList] = useState<string[]>();
  const [isStartValidation, setIsStartValidation] = useState<boolean>(false);
  const {
    register, // input項目に登録する関数
    formState, // フォームの状態を管理
    setError, // 無理やりエラーを起こす
    setValue, // バリューセット
    trigger, // 検証発火
    setFocus, // 要素フォーカス
  } = useForm();

  const handleServerError = () => {
    // API処理の結果、以下のエラーが返ってくるものと仮定
    interface ErrorContents {
      key: string;
      values: string[];
    }
    const errorList: ErrorContents[] = [
      {
        key: "test1",
        values: [
          "項目1に対するサーバからのエラーです。(1つ目)",
          "項目1に対するサーバからのエラーです。(2つ目)",
        ],
      }
    ];
    const msgList = [] as string[];

    errorList.forEach((item) => {
      // 同じサーバエラーメッセージが無かったらセット
      item.values.forEach((value) => {
        if (!msgList.includes(value)) {
          msgList.push(value);
        }
      });

      // 項目付近に対象のエラーメッセージリストのラストをセット
      const errorMessage = item.values[item.values.length - 1];
      setError(item.key, {
        type: "custom",
        message: errorMessage,
      });
    });

    setErrorMsgList(msgList);
  };
  const onClickFunc = async () => {
    // フラグ立てる
    setIsStartValidation(true);

    // 全項目に検証走らせる
    await trigger().then((result) => {
      if (result) {
        // クライアント検証がOKなら、サーバ通信を行う。
        // ここでは固定でエラーが返ってくるものと仮定。
        handleServerError();
      }
    });
  };
  // triggerでformStateの状態を変えたときに一番上にフォーカスさせる
  useEffect(() => {
    const firstErrorKey = Object.keys(formState.errors)[0];
    setFocus(firstErrorKey);
  }, [formState.errors]);

  return (
    <>
      {/* サーバエラーメッセージ */}
      <div className="mb-5">{ServerErrorMsgComponent(errorMsgList)}</div>

      <div className="container-fluid">
        <div className="middiumText">
          {/*  項目1 */}
          <input
            type="text"
            className={`w-25 required${
              formState.errors.test1
                ? " form-control bg-error"
                : " form-control"
            }`}
            {...register("test1", {
              required: validationRule.required,
              pattern: validationRule.pattern,
              maxLength: validationRule.maxLength,
            })}
            onChange={(e: ChangeEvent<HTMLInputElement>) => {
              setValue("test1", e.target.value, {
                shouldValidate: isStartValidation, // ボタン押下後であれば再検証を行う
              });
            }}
          />
          <p className="text-danger">
            {formState.errors.test1?.message as string}
          </p>
        </div>
        <button
          type="button"
          className="btn btn-primary"
          onClick={() => {
            void onClickFunc();
          }}
        >
          検証
        </button>
      </div>
    </>
  );
}

export default ValidationComponent;

これでクライアント検証、サーバ検証に対応した画面が作れます。

部品と関数を外出しする

最後に、共通的に使える部分を外出しして仕上げです。
検証ルールやメッセージを他の部分でも使う検証対応したテキストボックスやプルダウンを部品にしたい。。
等の場合はご参考に‼️

全体構成

components
 |_common
 |  |_validation
 |    |_ServerErrorMsgComponent.tsx     サーバーエラーメッセージコンポーネント(※上述のソース)
 |    |_ValidationUtil.tsx              検証関数クラス
 |    |_ValidationMessage.tsx           エラーメッセージ
 |    |_ValidationRule.tsx              検証ルール
 |    |_ValidDropDownListCopmponent.tsx 検証対応プルダウン
 |    |_ValidTextBoxComponent.tsx       検証対応テキストボックス
 |_test
    |_ValidationComponent.tsx           画面コンポーネント

共通部品・関数

ValidationUtil.tsx

検証に使う共通関数クラスです。
サーバエラーのハンドリングを切り出します。

interface ErrorContents {
  key: string;
  values: string[];
}
/**
 * バリデーションに関する共通機能を設定するUtil
 */
class ValidationUtil {
  /**
   * 各項目にバリデーションメッセージを設定するサンプル関数
   */
  static setValidMessage = (
    error: any,
    setErrorMsgList: React.Dispatch<React.SetStateAction<string[] | undefined>>,
    setError: any
   ) => {

      // 項目エラーがあった場合、バリデーション部品に情報を連携する
      // JSON文字列を取得
      const errorList: ErrorContents[] = [
       {
        key: "test1",
        values: [
          "項目1に対するサーバからのエラーです。(1つ目)",
          "項目1に対するサーバからのエラーです。(2つ目)",
        ],
       },
       { key: "test2", values: ["項目2に対するサーバからのエラーです。"] }
      ];
      const msgList = [] as string[];

      errorList.forEach((item) => {
        // 同じサーバエラーメッセージが無かったらセット
        item.values.forEach((value) => {
          if (!msgList.includes(value)) {
            msgList.push(value);
          }
        });

        //項目付近に対象のエラーメッセージリストのラストをセット(※項目付近には一つしか出力できない)
        const errorMessage = item.values[item.values.length - 1];

        setError(item.key, {
         type: "custom",
         message,
        });
      });

      setErrorMsgList(msgList);
  };

  /**
   * バリデーションメッセージの{}を置換する共通関数
   */
  static replaceTemplate = (template: string, ...args: string[]) => {
    let index = 0;
    return template.replace(/\{\}/g, () => args[index++]);
  };
}

export default ValidationUtil;
ValidationMessage.tsx

エラーメッセージを一元管理するのに使います。

/**
 * 検証時のエラーメッセージを管理するクラス
 */
class ValidationMessage {
  static requiredError = "{}を入力してください。";

  static stringRegularExpression = "{}は{}{}文字以内で入力してください。";

  static numberRegularexpression = "{}は{}{}桁以下で入力してください。";

  static invalidInput = "{}の入力が不正です。";

  static formatExpression = "{}は{}で入力してください。";

  /** 形式不正文言 */
  static formatError = "{}の形式が不正です。";

  static selectError = "{}を選択してください。";
}
export default ValidationMessage;
ValidationRule.tsx

検証ルールを管理します。
項目ごとのエラーメッセージはValidationUtllから置き換えます。

import ValidationUtil from "./ValidationUtil";
import ValidationMessage from "./ValidationMessage";

/**
 * 検証ルールを管理するクラス
 */
class ValidationRule {
  /** 全角チェック */
  static zenkakuRegex = /^[^ -~。-゚]+$/;
}
export default ValidationRule;

/**
 * 項目1(全角フィールド)
 */
export const test1Rule = {
  required: ValidationUtil.replaceTemplate(
    ValidationMessage.requiredError,
    "項目1(全角フィールド)"
  ),
  pattern: {
    value: ValidationRule.zenkakuRegex,
    message: ValidationUtil.replaceTemplate(
      ValidationMessage.stringRegularExpression,
      "項目1(全角フィールド)",
      "全角",
      "50"
    ),
  },
  maxLength: {
    value: 50,
    message: ValidationUtil.replaceTemplate(
      ValidationMessage.stringRegularExpression,
      "項目1(全角フィールド)",
      "全角",
      "50"
    ),
  },
};
/**
 * 項目2
 */
export const test2Rule= {
  required: ValidationUtil.replaceTemplate(
    ValidationMessage.selectError,
    "項目2"
  ),
};
/**
 * 項目3(全角フィールド)
 */
export const test3Rule= {
  pattern: {
    value: ValidationRule.zenkakuRegex,
    message: ValidationUtil.replaceTemplate(
      ValidationMessage.stringRegularExpression,
      "項目3(全角フィールド)",
      "全角",
      "25"
    ),
  },
  maxLength: {
    value: 25,
    message: ValidationUtil.replaceTemplate(
      ValidationMessage.stringRegularExpression,
      "項目3(全角フィールド)",
      "全角",
      "25"
    ),
  },
};
ValidTextBoxComponent.tsx

検証対応テキストボックスです。
registerをpropsで受け取るためにUseFormRegisterReturnを使います。
他にも、formStateのエラー状態を管理するためのプロパティをReact Hook Formからインポートしておきます。

import { FC } from "react";
import {
  UseFormRegisterReturn,
  FieldError,
  Merge,
  FieldErrorsImpl,
} from "react-hook-form";

interface ValidProps {
  type?: string;
  className?: string;
  fieldError: FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined;
  register: UseFormRegisterReturn;
  maxLength?: number;
  required?: boolean;
  disabled?: boolean;
  errorDisplay?: boolean;
  placeholder?: string;
}

/**
 * 入力値検証をするテキストボックス
 * @param param0
 * @returns
 */
const ValidTextBoxComponent: FC<ValidProps> = ({
  type = "text",
  className = "",
  fieldError,
  register,
  maxLength,
  required = false,
  disabled = false,
  errorDisplay = true,
  placeholder = "",
}) => (
  <>
    <input
      type={type}
      className={
        className +
        (fieldError ? " form-control bg-error" : " form-control") +
        (required ? " required" : "")
      }
      {...register}
      maxLength={maxLength}
      disabled={disabled}
      placeholder={placeholder}
    />
    {errorDisplay ? (
      <p className="text-danger">
        {fieldError?.message as string}
      </p>
    ) : (
      <></>
    )}
  </>
);
export default ValidTextBoxComponent;
ValidDropDownListCopmponent.tsx

検証対応プルダウンです。
テキストボックスと同じ要領で、React Hook Formのプロパティを受け取ります。

import { FC } from "react";
import {
  UseFormRegisterReturn,
  FieldError,
  Merge,
  FieldErrorsImpl,
} from "react-hook-form";

interface ValidDropDownProps {
  name?: string;
  id?: string;
  value?: string;
  className?: string;
  optionArrayList?: string[][];
  disabled?: boolean;
  required?: boolean;
  fieldError?: FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined;
  register?: UseFormRegisterReturn;
}
/**
 * 入力値検証をするドロップダウンリスト
 * @param param0
 * @returns
 */
const ValidDropDownListCopmponent: FC<ValidDropDownProps> = ({
  name = "",
  id = "",
  value = "",
  className = "",
  optionArrayList = [["", ""]],
  disabled = false,
  required = false,
  fieldError,
  register,
}) => (
  <>
    <select
      name={name}
      id={id}
      value={value}
      className={`${className} ${"form-control selectDropdown"} ${
        fieldError ? " bg-error" : ""
      }${required ? " required" : ""}`}
      {...register}
      disabled={disabled}
    >
      {optionArrayList.map((optionArray) => (
        <option
          className={required ? "required" : ""}
          value={optionArray[0]}
          disabled={optionArray[2] === "1"}
        >
          {optionArray[1]}
        </option>
      ))}
    </select>
    <p className="text-danger">
      {fieldError?.message as string}
    </p>
  </>
);

export default ValidDropDownListCopmponent;

以上で、フォーム送信をせずReact Hook Formを使った入力値検証が出来るようになります🎊
恐らくそんなに癖なく使いまわせるのではないでしょうか。

React Hook FormとStateを紐づける

さて、React Hook Formで入力値検証は出来た。。
ここからregister管理とState管理を連動させていきます。

React Hook Formでのみ画面を作る場合、以降の手順は不要です。

部品側

まず部品側に入力値でStateを更新させるようにします。

import { FocusEvent, ChangeEvent, FC } from "react";
import {
  UseFormRegisterReturn,
  FieldError,
  Merge,
  FieldErrorsImpl,
} from "react-hook-form";

interface ValidProps {
  type?: string;
  className?: string;
  fieldError: FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined;
  setState: (value: string) => void;
  register: UseFormRegisterReturn;
  maxLength?: number;
  required?: boolean;
  disabled?: boolean;
  errorDisplay?: boolean;
  placeholder?: string;
}

/**
 * 入力値検証をするテキストボックス
 * @param param0
 * @returns
 */
const ValidTextBoxComponent: FC<ValidProps> = ({
  type = "text",
  className = "",
  fieldError,
  setState = function dummy(): void {},
  register,
  convertFunc,
  maxLength,
  required = false,
  disabled = false,
  errorDisplay = true,
  placeholder = "",
}) => (
  <>
    <input
      type={type}
      className={
        className +
        (fieldError ? " form-control bg-error" : " form-control") +
        (required ? " required" : "")
      }
      {...register}
      onChange={(e: ChangeEvent<HTMLInputElement>) => {
        setState(e.target.value);
      }}
      maxLength={maxLength}
      disabled={disabled}
      placeholder={placeholder}
    />
    {errorDisplay ? (
      <p className="text-danger labelTextWeight">
        {fieldError?.message as string}
      </p>
    ) : (
      <></>
    )}
  </>
);
export default ValidTextBoxComponent;

呼び出し側

呼び出し側ではregisterのnameと同じStateを宣言し、State更新時にReact Hook Formで管理される値も更新されるように修正します。
またregisterのオプションで初期値にStateの値を設定し、実際のユーザ入力値で更新されるのはStateになるよう部品を呼び出します。

import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import ValidationUtil from "../../common/validation/ValidationUtil";
import ServerErrorMsgComponent from "../../common/validation/ServerErrorMsgComponent";
import test1Rule from "../../common/validation/ValidationRule";
import ValidTextBoxComponent from "../../common/validation/ValidTextBoxComponent";

function ValidationComponent() {
  // 項目1
  const [test1, setTest1] = useState<string>("");

  /**
   * バリデーション関連設定
   */
  const [errorMsgList, setErrorMsgList] = useState<string[]>();
  const [isStartValidation, setIsStartValidation] = useState<boolean>(false);
  const {
    register, // input項目に登録する関数
    formState, // フォームの状態を管理
    setError, // 無理やりエラーをセットする用
    setValue, // バリューセット
    trigger, // 検証発火
    setFocus, // 要素フォーカス
  } = useForm();
  const [errorMsgList, setErrorMsgList] = useState<string[]>();

 // StateとReact Hook Form管理オブジェクトを紐づける
  useEffect(() => {
    setValue("test1", test1, {
      shouldValidate: isStartValidation
    });
  }, [test1]);

  const onClickFunc = async () => {
    // フラグ立てる
    setIsStartValidation(true);

    // 全項目に検証走らせる
    await trigger().then((result) => {
      if (result) {
        // クライアント検証がOKなら、サーバ通信を行う。
        // ここでは固定でエラーが返ってくるものと仮定。
        ValidationUtil.setValidMessage("error-sample",setErrorMsgList,setError);
      }
    });
  };
  // triggerでformStateの状態を変えたときに一番上にフォーカスさせる
  useEffect(() => {
    const firstErrorKey = Object.keys(formState.errors)[0];
    setFocus(firstErrorKey);
  }, [formState.errors]);

  return (
    <>
      {/* サーバエラーメッセージ */}
      <div className="mb-5">{ServerErrorMsgComponent(errorMsgList)}</div>

      <div className="container-fluid">
        <div className="middiumText">
          {/*  項目1 */}
          <ValidTextBoxComponent
            type="text"
            register={register("test1", {
              required: test1Rule.required,
              pattern: test1Rule.pattern,
              maxLength: test1Rule.maxLength,
              value: test1,
            })}
            className="w-25"
            fieldError={formState.errors.test1}
            setState={setTest1}
            maxLength={test1Rule.maxLength.value}
            required
          />
        </div>
        <button
          type="button"
          className="btn btn-primary"
          onClick={() => {
            void onClickFunc();
          }}
        >
          検証
        </button>
      </div>
    </>
  );
}

export default ValidationComponent;

。。。かなり力業ですね。。。

終わりに

いかがでしたでしょうか。
Reactと親和性のあるバリデーションライブラリは他にもたくさんありますが、その中でもReact Hook Formは直感的で使いやすいなぁという印象でした。
ReactのライブラリなのにDOM管理前提??という疑問が個人的に残りますが、使いたい機能だけうまいこと抜き出して使えばガチガチのReactコントロールフォームより断然軽く作れますので、是非トライしてみてください。