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


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

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

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

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

## formik vs react-hook-form　人気
まずは簡単に両者の人気チェックと公式の言い分を確認します。

![スクリーンショット 2020-12-18 16.51.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441432/c0d3b019-5efd-58d4-2ddb-22a1ac1cb9ec.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 通常レンダリング数
でも、自分の目でみないと信じられません。誇大広告かもしれません。
実際に同じ実装をした上で検証してみます。


```react: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: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と揃えるためです。
２つの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/>`に指定することで可能です。


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


```react:formikのgithub
export function Formik<
  Values extends FormikValues = FormikValues,
  ExtraProps = {}
>(props: FormikConfig<Values> & ExtraProps) {
  const formikbag = useFormik<Values>(props);
...
  }
  return (
    <FormikProvider value={formikbag}>
...
```
https://github.com/formium/formik/blob/master/packages/formik/src/Formik.tsx

一部省略していますが、githubからFormikの実装を抜粋したものです。
`<FormikProvider value={formikbag}>` があり、 valuesなどといった値はここから購読しています。

そして、contextに関するレンダリングは、下記の記事が非常に参考になります。
reduxのメンテナーのMarkさんの記事を翻訳されたものになります。
[Reactのレンダリングに関する完全ガイド](https://qiita.com/hellokenta/items/6b795501a0a8921bb6b5#context-%E3%81%A8%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0)

>デフォルトでは、親コンポーネントの 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
まず１つは、公式の用意している `FastField`　を利用するという解決方法です。
https://formik.org/docs/api/fastfield
これは`shouldComponentUpdate()`が内部で実装されています。以下のような性質を持ちます。

>For example, <FastField name="firstName" /> 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が変わった時(筆者コメント:そんな実装ある？）

```react: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

```

上記のように `FastField`、 `Field`それぞれレンダリングをカウントするログを仕込みました。
結果、`Field`は再レンダリング起きまくっていますが、`FastField`はブラー時や自身と関係のないフォーム入力時は
レンダリングを抑えることができています。
![スクリーンショット 2020-12-22 18.46.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441432/afb8e8c8-fa49-48c3-4604-bb059cce9927.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での操作でパフォーマンス面に違いが出るのかなどの調査も今後行なっていきたいです。