Reactでのフロント開発において、入力フォーム等のフロントエンドでもバリデーションを気軽に実装することができるライブラリであるReact-Hook-Formを使っている中でどうしてもライブラリの標準機能で対応できないことがあったのでそれについての解決法をまとめます。
React-Hook-Formの基本的な使い方は別の記事にまとめています。

解決したい問題
解決したい問題の前に前提条件は以下です。
- 1つの画面に入力フォームA、入力フォームB、入力フォームCの計3の入力フォーム(
input
タグのtype='text'
)があり、それは1つのフォーム(form
タグ)の括りとする - 各入力フォームのコーディング順、配置順共にA→B→C
- 入力フォームBはデフォルトでは非表示、何かしらのインプット値によって表示と非表示を切り替える
- 3つの入力フォーム全て必須入力のバリデーションをReact-Hook-Formで設定している(
required
) shouldFocusError: true
を設定してバリデーションエラー時にはエラーがある要素の1つ目にフォーカスする

この前提で発生する問題はここからで、以下の状態で送信ボタンを押したとします。
- 入力フォームAは文字列を入力
- 入力フォームBは何も入力しない
- 入力フォームCは何も入力しない
これ、正しい(というか期待する)挙動は「入力フォームBにフォーカスされる」ですが、実際には入力フォームCにフォーカスが当たります。

この記事をこの問題の解決までのプロセスも含めてまとめます。
原因の解明
色々調べたのですが、これを解決できる術を思いつかなかったので、ツイートしました(笑)
すると、公式アカウントからリプライが…!
ということでReact-Hook-FormのGitHubでDiscussionを立てて質問しました。(DeepLフル活用です)
詳細が気になる方は上のDiscussionを見に行って欲しいのですが、要約すると
- 原因は
register
は画面に表示された順番で登録するので、画面上の入力フォームの配置順はA→B→Cになるけど、今回のケースだと画面にレンダリングされた順がA→C→BになるのでBとCでバリデーションエラーがあった場合はCが先に検出されフォーカス対象はCになってしまう。 - 現在のReact-Hook-Formには
register
での登録順を調整する機能がない。(実装が大変なんだ、みたいなコメントもありました)
という感じでした。
原因がわかったのでかなりスッキリしたのですが、さてどうしようとなりました。
バリデーション時のスクロールを自作する
Discussionに以下の返信をもらいました。
yes,
https://github.com/react-hook-form/react-hook-form/discussions/7739#discussioncomment-2107024register
invoke order does matter. for custom behavior, I would recommend turning offshouldFocus
and usingsetFocus
to manually set field focus at the app level with custom logic.
簡潔に訳すと
「shouldFocusError
を使ったらできないからfalse
にして、setFocus
というプロパティを使ってフォーカスのロジックを自作したらできるで」
という感じです。
というわけでサンプルコードですが、自作しました。
import { ErrorMessage } from "@hookform/error-message";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import "./styles.css";
type Inputs = {
formA: string;
formB: string;
formC: string;
};
export default function App() {
const [isDisplay, setIsDisplay] = useState(false);
const {
register,
handleSubmit,
setFocus,
formState: { errors, submitCount }
} = useForm<Inputs>({
shouldFocusError: false
});
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
errors && errors.formA
? setFocus("formA")
: errors.formB
? setFocus("formB")
: errors.formC
? setFocus("formC")
: null;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [submitCount]);
const submitForSample = () => alert("送信成功");
return (
<div>
<button onClick={() => setIsDisplay(!isDisplay)}>
入力フォームBの表示/切り替えボタン
</button>
<br />
<br />
<form action="" onSubmit={handleSubmit(submitForSample)}>
<div>
入力フォームA:
<input
type="text"
{...register("formA", { required: "必須入力です。" })}
/>
</div>
<ErrorMessage
errors={errors}
name={"formA"}
render={({ message }) => <p style={{ color: "red" }}>{message}</p>}
/>
{isDisplay && (
<>
<div>
入力フォームB:
<input
type="text"
{...register("formB", { required: "必須入力です。" })}
/>
</div>
<ErrorMessage
errors={errors}
name={"formB"}
render={({ message }) => (
<p style={{ color: "red" }}>{message}</p>
)}
/>
</>
)}
<div>
入力フォームC:
<input
type="text"
{...register("formC", { required: "必須入力です。" })}
/>
</div>
<ErrorMessage
errors={errors}
name={"formC"}
render={({ message }) => <p style={{ color: "red" }}>{message}</p>}
/>
<br />
<div>
<input type="submit" value="送信" />
</div>
</form>
</div>
);
}
少しだけコードを解説をすると、まずはReact-Hook-Formから必要なプロパティを変数に入れ、バリデーションエラー時に1番最初の入力フォームにフォーカスするshouldFocusError
をfalse
にします。
const {
register,
handleSubmit,
setFocus,
formState: { errors, submitCount }
} = useForm<Inputs>({
shouldFocusError: false
});
各プロパティの簡単な役割は以下です。
register
:バリデーションを適用するパラメーター名(今回でいうformA, formB, formC)handleSubmit
:引数に渡した処理を実行するまでにRact-Hook-FormでバリデーションをかけるsetFocus
:引数に渡したregister
で登録したパラメーター名にフォーカスするerrors
:バリデーション情報submitCount:handleSubmit
が実行された回数(バリデーション検証回数)
自作したスクロールのロジックはuseEffect
内に記載しています。
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
errors && errors.formA
? setFocus("formA")
: errors.formB
? setFocus("formB")
: errors.formC
? setFocus("formC")
: null;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [submitCount]);
依存配列にsubmitCount
を設定することで、送信ボタンを押した段階でuseEffec内の処理を実行するようにし、バリデーション情報のうち、register
で登録したパラメータのエラーがあればそこにフォーカスするような処理にしています。
これで、以下のように入力フォームBを非表示→表示にした状態でも入力Bにエラーがあればフォーカスできるようになります。

CodeSandboxも用意していますので実際に動かすこともできます。

最後に
ライブラリ側でregister
の登録順序を変更する仕様はまだまだ実装されなさそう(今後ずっとされないかも)なので自作でもできることがわかったのが大きな収穫でした。
また、管理者がしっかりしている(?)ライブラリでは公式のGitHubで質問することでかなり解決に近づけることがあり、今後に活かせる経験でした。
React-Hook-Formの基本的な使い方はこちらにまとめています。

コメント