かれこれFormikで数十個以上フォームを作成しており、
Formikの大ファンと言っても過言ではありません。
糖衣構文でスッキリかける!
痒いところに手が届く!
ですが、最近はパフォーマンス面に少し不満を抱えていました。。
普通に書くとめちゃめちゃformikは再レンダリングされてしまうんですよね。
そんな中、**react-hook-formはそんなに再レンダリングされないよ!**との情報を耳にしましたので、
実際どんなもんなの?ってところと、なんとかformikもパフォーマンスアップできない?ってところを検証/確認したいと思います。
formik vs react-hook-form 人気
まずは簡単に両者の人気チェックと公式の言い分を確認します。
formikがまだまだ人気のようです。
しかし、両者ともシェアを伸ばしていっている感じがします。
https://formik.org/
formik公式サイトトップの謳い文句を見ると、 **Declarative(宣言的), Intuitive(直感的), Adoptable(採用しやすさ)、そしてLess Code(コード量が少なくかける)**とのことです。
使用してきた私としても、上記の宣伝には頷けます。
では、react-hook-formはどうでしょうか?
https://react-hook-form.com/jp/
売り文句が多いのですが、中でも目につくのは、 ライブラリのコード比較ですね。後発者の強みです。
そしてFormikと比べ、レンダリング数が少なく、高速なマウントであることを挙げています。
ページ内のサンプルを見ると圧倒的ですね。あと、ドキュメントが日本語化されてて読みやすいです。
formik vs react-hook-form 通常レンダリング数
でも、自分の目でみないと信じられません。誇大広告かもしれません。
実際に同じ実装をした上で検証してみます。
import React from 'react'
import { Formik, Form, Field } from 'formik'
import { initials } from '../../../utils/initials'
const FormikComp = () => {
return (
<Formik
initialValues={initials}
onSubmit={values => alert(values)}
>
{() => {
console.count('formik呼び出し回数')
return (
<Form>
<h2>Formik</h2>
<Field name='username' />
<Field name='kana' />
</Form>
)
}}
</Formik>
)
}
export default FormikComp
import React from 'react'
import { useForm } from 'react-hook-form'
import { initials } from '../../../utils/initials'
const HookForm = () => {
const { register, handleSubmit } = useForm({
defaultValues: initials
})
const onSubmit = data => alert(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<HookFormInner register={register} />
</form>
)
}
export default HookForm
const HookFormInner = ({ register }) => {
console.count('react-hook-form呼び出し回数')
return (
<>
<h2>react hook form</h2>
<input name="username" ref={register} />
<input name="kana" ref={register} />
</>
)
}
両者全く同じ実装です。
react-fook-formの中をわざわざ抜き出しているのは、レンダリング発生測定の場所をFormikと揃えるためです。
2つのinputを持っており、検証方法は下記の手順で行います。
・1つ目(username)を 名前 と入力
・2つ目(kana)を ナマエ と入力
・2つ目のインプットからフォーカスを外す
もちろんタイプ数も揃えます。
【結果】
・formik呼び出し回数: 27
・react-hook-form呼び出し回数: 1
react-hook-formの圧勝でしたね、では解散!
...と思う前に、ちょっと下記のreact-fook-formの設定をみて欲しいです。
https://react-hook-form.com/jp/api/
mode: onChange | onBlur | onSubmit | onTouched | all = 'onSubmit'
デフォルトではmodeはonSubmitです。
このモードは、 submit イベントからバリデーションがトリガーされ、 無効な入力は onChange イベントリスナーをアタッチして再度バリデーションを行います。 とのことです。
ちなみにformikは入力毎かつブラー時にもバリデーションが発火します。
つまりデフォルトでは、formik/react-fook-formそれぞれバリデーションが走る頻度が揃っていないということになります。
ふう、騙されるところでした。
バリデーション条件を揃える
では、formikでも mode:onSubmit つまり、送信時のみにバリデーションが走るように設定してみましょう。
https://formik.org/docs/api/formik
formikのバリデーションの細かい条件は、<Formik/>
に指定することで可能です。
<Formik
initialValues={initials}
onSubmit={values => alert(values)}
+ validateOnBlur={false}
+ validateOnChange={false}
+ validateOnMount={false}
>
上記でブラー時/値変更時/マウント時のバリデーションを無効にしました。
この上で再度検証してみましょう。
【結果】
・formik呼び出し回数: 14
・react-hook-form呼び出し回数: 1
formikのレンダリング数は 27 -> 14に削減しました!
しかし、react-hook-formには遠く及びません....
なぜformikはめちゃめちゃレンダリングしてしまうのか
一体14回もレンダリングしてformikは何をしているのでしょう。
<Formik>
の内部では、コンテキストを利用しています。
export function Formik<
Values extends FormikValues = FormikValues,
ExtraProps = {}
>(props: FormikConfig<Values> & ExtraProps) {
const formikbag = useFormik<Values>(props);
...
}
return (
<FormikProvider value={formikbag}>
...
一部省略していますが、githubからFormikの実装を抜粋したものです。
<FormikProvider value={formikbag}>
があり、 valuesなどといった値はここから購読しています。
そして、contextに関するレンダリングは、下記の記事が非常に参考になります。
reduxのメンテナーのMarkさんの記事を翻訳されたものになります。
Reactのレンダリングに関する完全ガイド
デフォルトでは、親コンポーネントの State が更新されると、コンテキスト値を読み込んでいるかどうかに関わらず、その子孫のすべてが再レンダリングされるのです!
これがformikがレンダリングされまくる原因のようです。
現在親元しかレンダリングの観測をしていませんが、子コンポーネントもバリバリレンダリングされています。
では、react-hook-formではどのようにしてレンダリングを回避しつつFormの値を取得/管理しているのでしょうか?
なぜreact-hook-formはレンダリングが少ないのか
react-hook-formでは、useFormから作成したregister
を各コンポーネントのref
にわたしています。このregisterを通して、formの管理を行なっているようです。
ref
の更新は再レンダリングを伴わないのでパフォーマンスが向上するということです。
こちらの公式FAQsにもこのような回答があります。
https://react-hook-form.com/jp/faqs/
React Hook Formでは非制御コンポーネントによってregister関数をrefで実行しています。 このアプローチにより、ユーザーからの入力や値の変更により発生する再レンダリングの量を削減しています。 コンポーネントのページへのマウントも制御されていないことによりはるかに高速になります。こちらの他のライブラリとのマウントスピードの簡単な比較をご覧下さい。
formikのレンダリング地獄はどうしたらいい?
FastField
まず1つは、公式の用意している FastField
を利用するという解決方法です。
https://formik.org/docs/api/fastfield
これはshouldComponentUpdate()
が内部で実装されています。以下のような性質を持ちます。
For example, will only re-render when there are:
Changes to values.firstName, errors.firstName, touched.firstName, or isSubmitting. This is determined by shallow >comparison. Note: dotpaths are supported.
A prop is added/removed to the<FastField name="firstName" />
The name prop changes
適当訳
以下、レンダリングが起こる例です
・values.firstName
,errors.firstName
,touched.firstName
,isSubmitting
が更新された時。ちなみに浅い比較。 *ドットパスはサポートされてます。
・<FastField name="firstName" />
にpropが追加/削除された時。
・name
propsが変わった時(筆者コメント:そんな実装ある?)
import React from 'react'
import { Formik, Form, FastField, Field } from 'formik'
import { initials } from '../../../utils/initials'
const FormikComp = () => {
return (
<Formik
initialValues={initials}
onSubmit={values => alert(values)}
validateOnBlur={false}
validateOnChange={false}
validateOnMount={false}
>
{() => {
return (
<Form>
<h2>Formik</h2>
<FastField name='username'>
{({ field, form, meta }) => {
console.count('FastField')
return (
<input {...field} />
)
}}
</FastField>
<Field name='kana'>
{({ field, form, meta }) => {
console.count('Field')
return (
<input {...field} />
)
}}
</Field>
</Form>
)
}}
</Formik>
)
}
export default FormikComp
上記のように FastField
、 Field
それぞれレンダリングをカウントするログを仕込みました。
結果、Field
は再レンダリング起きまくっていますが、FastField
はブラー時や自身と関係のないフォーム入力時は
レンダリングを抑えることができています。
頑張る
FastFieldでは対応できないコンポーネントやその他の重いコンポーネントは、
各コンポーネントに地道にReact.memoなりshouldComponentUpdateなりを実装して手作業で最適化していくしかないです。
がっくり。
https://ja.reactjs.org/docs/react-api.html#reactmemo
https://ja.reactjs.org/docs/optimizing-performance.html#shouldcomponentupdate-in-action
まとめと感想
最初にreact-hook-form
のdemoをみたときは なぜref??なんだかスッキリしない書き方でちょっと苦手かも...と思っていましたが、かなり理にかなっていることがわかりました。
今後大きいフォームを作成する場合はreact-hook-form
を選定すると思います。
また、refであるが故の弊害などないのかが気になっているので、手が空いたときにそちらも調査してみようと思います。
追記
react-hook-form
ではcontrollerを使用することで、refを使わなくても操作可能になります。
https://react-hook-form.com/jp/api/#Controller
コントローラーでの操作とrefでの操作でパフォーマンス面に違いが出るのかなどの調査も今後行なっていきたいです。