フォームの内容を state で制御したい時はよくあります。フォームとして扱い都度セレクタから FormData を取るのも手ですが、それ以外でも行いたいです。これを素直に実装すると次の様なコードになります。
import React, { useState } from 'react'; export default function HasFormStateComponent() { // form を state で扱う const [formState, setFormState] = useState({}); // 起きた event を元に state を変更する const onChange = (event) => setFormState({ ...formState, [event.target.name]: event.target.value }) return ( <label>名<input type="text" name="firstName" value={formState.firstName} onChange={onChange} /></label> ); }
これに state を表示する機能を加えたデモが次です。
テンプレートにできるくらい汎用的なつくりです。
またブラウザ上のバリデーションはよくある機能要求でこれを React で簡単に実現するためのライブラリが React Hook Form です。これを用いることでバリデーションを後々読みやすい形ですぐ作れます。
ホーム | React Hook Form – Simple React forms validation
【React】React Hook Form と yup でフォームのバリデーションを作る – 株式会社シーポイントラボ | 浜松のシステム・RTK-GNSS開発
この React Hook Form を次の様に先ほどのテンプレートと組み合わせるとバグが発生します。
// 主な部分のみ関数コンポーネントから抜粋 const { register } = useForm({ resolver: yupResolver(SignupSchema)// 省略部で定義したバリデーションルールをあてはめ }); const [formState, setFormState] = useState({}); const onChange = (event) => { setFormState({ ...formState, ...{ [event.target.name]: event.target.value } }); }; return ( <label>名 <input type="text" name="firstName" ref={register} value={formState.firstName} onChange={onChange} /></label> );
上記コードと同じつくりをしたバグを持つフォームのデモが次です。
一度送信ボタンを押した後、バリデーションエラーを消したり出したりする様に入力すると異常に気が付きやすいです。このエラーはバリデーションエラーの状態が変更する際、onChange がフックされていないことが原因で起きています(onInput の場合はフックしますが入力値が反映されていない値が event.target.value に入ります)。入力しながらデモ下部で表示している最新のイベント内容を見るとわかりやすいです。
これはフィールドの value の値を formState.firstName であると定義したのがバグの元です。バリデーションエラーの状態変更が起きた時、どういうわけか event.target.value には formState.firstName の値が入っており、想定される入力後の値でなく、入力前の値が formState.firstName に再代入されます。value の値には formState.firstName の値が割り当てられているので結果、変化が起きません。
この問題の解決方法の一つ(他にもあるかもしれませんが満足できる解決方法でしたので調査していません)が value={hogehoge} をやめて別の方法で同様の機能を実現することです。これは React Hook Form の提供する defaultValues オプションと setValue 関数によって達成できます。
defaultValues オプションは useForm 関数実行時に引数に与えることで設定されます。
API ドキュメント | React Hook Form – Simple React forms validation#useForm
次の様に記述することでフォームの初期値が設定できます。編集ページなどで最初の一度だけ既存データを入れておきたい、といったユースケースでは value={hogehoge} を用いず、 useForm で defaultValues オプションを設定するのみが無難です。
// 主要部を抜粋 // state の初期値を設定 const [formState, setFormState] = useState({ firstName: '既存のユーザ名' }); // defaultValues で初期値を設定。 state の初期値をそのまま反映 const { register } = useForm({ resolver: yupResolver(SignupSchema), defaultValues: formState, // 実際は {firstName: props.firstName} とかが入りやすいです }); // 初期値が state とフォームで同じなので変化も同じにすれば同期されます。 const onChange = (event) => { setFormState({ ...formState, ...{ [event.target.name]: event.target.value } }); }; // value={}を外す。 return ( <label>名 <input type="text" name="firstName" ref={register} onChange={onChange} /></label> );
setValue 関数はフォーム内のフィールドに値をセットします。直接 value={hogehoge} と定義するのでなく、必要に応じて都度 setValue 関数を実行することで動作を壊すことなくフィールドに値を渡せます。
API ドキュメント | React Hook Form – Simple React forms validation#setValue
これを用いて例えば次の記述ができます。
const [formState, setFormState] = useState({}); const { register, setValue } = useForm({ resolver: yupResolver(SignupSchema), }); const onChange = (event) => { setFormState({ ...formState, ...{ [event.target.name]: event.target.value } }); // もし firstName が変更されたら firstNameDuplicate も同じ値に変更 if (event.target.name === "firstName") { setValue("firstNameDuplicate", event.target.value); } }; return ( <div> <label>名</label> <input type="text" name="firstName" ref={register} onChange={onChange} /> <label>名(↑フィールドの変化に追従)</label> <input type="text" name="firstNameDuplicate" ref={register} onChange={onChange} /> </div> );
実際にバグを取り除き、state の活用を増やしたデモが次です。
この記事で紹介したバグを起こしてしまった場合の対象方法はありませんでしたが、React Hook Form の公式ドキュメントはよくある課題の解決方法(名前違いの props を持つコンポーネントとの接続方法)、応用例、TypeScript用の型、開発者ツールなど充実しています。自動翻訳感が否めませんが日本語もサポートしており読みやすくもありおすすめです。
ホーム | React Hook Form – Simple React forms validation