⌨️
Reactのフォームライブラリの「入力していない」状態の扱いについて
Reactでフォームを扱う時、シンプルであればライブラリを使わなくても書けるものの、なんだかんだでフォームライブラリを選ぶことは多いのではと思います。各ライブラリのAPIは、最適化の手段や設計思想が違うことで似ているようで似ていないのが実体です。今回は中でも「入力していない」状態の扱いが各ライブラリでかなり異なることがわかったのでまとめてみたいと思います。
比較するライブラリはよく使う3つ
- react-hook-form
- react-final-form
- formik
実装は各公式サンプルの最もシンプルなものをベースにしています。
比較サンプル
シンプルな実装で比較するとわかりやすいので全て同じ仕様とします
#フォーム
①フォームは2つのフィールドを持つ
* FieldA(必須)
* FieldB(任意)
②submitボタンをクリックでsubmitされ、エラーが無い場合はconsole.logにvalueを表示する
③フォームの値は常に<pre>タグ内に表示する
#カウンター
フォーム外での再レンダリングの影響調査用
比較パターン
「入力していない」といっても色々な状態があります。今回は下記の5パターンをそれぞれ行い、フォームの値がどうなるかを見てみます。
- 初期レンダリング時
- 再レンダリング時(form外のstate変更。今回はcounterを+1)
- 1フィールドのみ入力した時(FieldAのみ入力)
- 入力後空にした時
- 1フィールドのみ入力してsubmitした時
結論
結構違う。
react-hook-form | formik | react-final-form | |
---|---|---|---|
初期レンダリング | {} | {} | {} |
再レンダリング(counter +1) | {"filedA":"","fieldB":""} | {} | {} |
FieldAのみ入力 | {"filedA":"あいうえお","fieldB":""} | {"fieldA":"あいうえお"} | {"fieldA":"あいうえお"} |
入力後空にする | {"filedA":"","fieldB":""} | {"fieldA":""} | {} |
FieldAのみ入力してsubmit | {filedA: "あいうえお", fieldB: ""} | {"fieldA":"あいうえお", "fieldB": undefined} | {"fieldA":"あいうえお"} |
※実装にも影響を受けるので必ずしもこうなるとは限りません(formikはinitialValuesが必須で、今回はundefinedを指定している等)。
思ったこと
- react-hook-formは入力していない状態は基本empty stringとする方針。これはJS的なstateよりもhtml的な状態を重視しているためだが、初期レンダリングとそれ以降の値に一貫性がない。
- react-final-formはempty stringを扱わない方針。htmlよりもJS的なstateを正としているため一貫性は高い。
- formikは双方の中間的な立ち位置に見える。一度onChangeが走ればhtmlを正とする挙動
- こうした挙動を念頭にvalidationやsubmit時のロジックを書く必要がありそう
codesandbox
react-hook-form
function App() {
const {
register,
handleSubmit,
watch,
formState: { errors }
} = useForm();
const onSubmit = (data) => {
console.log("result", data);
};
const [count, setCount] = useState(0);
return (
<section>
{/* Form */}
<div>
<h2>Form</h2>
<form onSubmit={handleSubmit(onSubmit)}>
<label>FieldA</label>
<input {...register("filedA", { required: true })} />
{errors.fieldA && <p>required</p>}
<label>FieldB</label>
<input {...register("fieldB")} />
<input type="submit" />
<pre>{JSON.stringify(watch())}</pre>
</form>
</div>
<hr />
{/* Counter */}
<div>
<h2>Counter</h2>
<button
onClick={() => {
setCount((prev) => prev + 1);
}}
>
+
</button>
<pre>{count}</pre>
</div>
</section>
);
}
formik
function App() {
const [count, setCount] = useState(0);
const onSubmit = (data) => {
console.log("result", data);
};
return (
<section>
{/* Form */}
<div>
<h2>Form</h2>
<Formik
initialValues={{
fieldA: undefined,
fieldB: undefined
}}
onSubmit={onSubmit}
>
{(props) => (
<Form>
<label>FieldA</label>
<Field
name="fieldA"
validate={(value) => (!value ? "required" : undefined)}
/>
{props.errors.fieldA && <p>{props.errors.fieldA}</p>}
<label>FieldB</label>
<Field name="fieldB" />
<button type="submit">Submit</button>
<pre>{JSON.stringify(props.values)}</pre>
</Form>
)}
</Formik>
</div>
{/* Counter */}
<hr />
<div>
<h2>Counter</h2>
<button
onClick={() => {
setCount((prev) => prev + 1);
}}
>
+
</button>
<pre>{count}</pre>
</div>
</section>
);
}
react-final-form
function App() {
const [count, setCount] = useState(0);
const onSubmit = (data) => {
console.log("result", data);
};
return (
<section>
{/* Form */}
<div>
<h2>Form</h2>
<Form
onSubmit={onSubmit}
render={({ handleSubmit, submitting, pristine, values, errors }) => (
<form onSubmit={handleSubmit}>
<label>FieldA</label>
<Field
name="fieldA"
component="input"
validate={(value) => (!value ? "required" : undefined)}
/>
{errors.fieldA && <p>{errors.fieldA}</p>}
<label>FieldB</label>
<Field name="fieldB" component="input" />
<input type="submit" />
<pre>{JSON.stringify(values)}</pre>
</form>
)}
/>
</div>
{/* Counter */}
<hr />
<div>
<h2>Counter</h2>
<button
onClick={() => {
setCount((prev) => prev + 1);
}}
>
+
</button>
<pre>{count}</pre>
</div>
</section>
);
}
Discussion
graphqlを使っているとこの使用が不便で配列や、number型でもvalueが空だったときに空文字列が送られてしまっているのですが、みなさんはどうやって対応しているのでしょうか。。。
自分は
こちらを見てhandleSubmitごとにこの関数を書いて対応しているのですが、handleSubmitの度に書くのがのめんどくさいです。。