LoginSignup
51
18

More than 3 years have passed since last update.

Formikが遅すぎるので React Hook Formと比較してみた

Last updated at Posted at 2020-12-22

かれこれFormikで数十個以上フォームを作成しており、
Formikの大ファンと言っても過言ではありません。

糖衣構文でスッキリかける!
痒いところに手が届く!

ですが、最近はパフォーマンス面に少し不満を抱えていました。。
普通に書くとめちゃめちゃformikは再レンダリングされてしまうんですよね。

そんな中、react-hook-formはそんなに再レンダリングされないよ!との情報を耳にしましたので、
実際どんなもんなの?ってところと、なんとかformikもパフォーマンスアップできない?ってところを検証/確認したいと思います。

formik vs react-hook-form 人気

まずは簡単に両者の人気チェックと公式の言い分を確認します。

スクリーンショット 2020-12-18 16.51.41.png

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 通常レンダリング数

でも、自分の目でみないと信じられません。誇大広告かもしれません。
実際に同じ実装をした上で検証してみます。

formik
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
react-fook-form
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
    <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>の内部では、コンテキストを利用しています。

formikのgithub
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が追加/削除された時。
namepropsが変わった時(筆者コメント:そんな実装ある?)

FastField計測
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

上記のように FastFieldFieldそれぞれレンダリングをカウントするログを仕込みました。
結果、Fieldは再レンダリング起きまくっていますが、FastFieldはブラー時や自身と関係のないフォーム入力時は
レンダリングを抑えることができています。
スクリーンショット 2020-12-22 18.46.30.png

頑張る

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での操作でパフォーマンス面に違いが出るのかなどの調査も今後行なっていきたいです。

51
18
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
18