Reactのフォームライブラリを比較してみる

## はじめに
タイトルの通り、Reactのフォームライブラリのチュートリアルをそれぞれ触ってみて個人的に比較してみた記事です。

### 対象ライブラリ
対象は以下の3ライブラリとなっています。

* react-hook-form
* Formik
* react-final-form

## Reactでのフォーム実装

まずはじめに、Reactでフォームを実装するにはuseStateやuseRefを用いた実装方法が挙げられます。
これらはそれぞれReactに値を管理させる（制御コンポーネント）かDOMに値を管理させる（非制御コンポーネント）かといった違いがあります。
[公式doc](https://ja.reactjs.org/docs/uncontrolled-components.html)を見ていただくのが早いですが、端的に言うと以下のような違いがあります。

* 制御コンポーネント（Controlled Component）
React自身がフォームの値を管理する。
Reactの設計原則(single source of truth)に即している。
フォームへの入力毎に値を監視するため、即時バリデーション等、値の変化時に処理を行うようなフォームに向いている。

* 非制御コンポーネント（UnControlled Component）
フォームの値をDOMに持たせて管理する。
DOMに値を持たせることでReact以外のコードへ値を簡単に渡すことが出来る。
フォームへの入力毎に値を監視せず再レンダリング回数を抑えられるため、パフォーマンスに優れる。


### 実装イメージ
Submit時にコンソールへとフォームの値を出力する単純なユーザ登録フォームです。

![state_Trim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3125818/836bf4d9-ed8b-f1ea-727c-f93e8e2ae229.gif)

それぞれの方法でフォームを実装したコードが以下になります。

```StateForm.tsx
import { FormEvent, useState } from "react"

export const StateForm = () => {
    const [ name, setName ] = useState<string>("");
    const [ password, setPassword ] = useState<string>("");
    const [ checkPassword, setCheckPassword ] = useState<string>("");
    const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        console.log({
            name: name,
            password: password,
            checkPassword: checkPassword
        });
    }
    return (
        <form onSubmit={handleSubmit}>
            <h2>ユーザ登録フォーム</h2>
            <label>ユーザID
                <input value={name} onChange={(e) => setName(e.target.value)} />
            </label>
            <label>パスワード
                <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
            </label>
            <label>パスワード（再入力）
                <input type="password" value={checkPassword} onChange={(e) => setCheckPassword(e.target.value)} />
            </label>
            <button type="submit">登録</button>
        </form>
    )
}
```

```RefForm.tsx
import { FormEvent, useRef } from "react"

export const RefForm = () => {
    const nameRef = useRef<HTMLInputElement>(null);
    const passwordRef = useRef<HTMLInputElement>(null);
    const checkPasswordRef = useRef<HTMLInputElement>(null);
    const mailAddressRef = useRef<HTMLInputElement>(null);
    const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        console.log({
            name: nameRef.current?.value,
            password: passwordRef.current?.value,
            checkPassword: checkPasswordRef.current?.value,
            mailAddress: mailAddressRef.current?.value
        });
    }
    return (
        <form onSubmit={handleSubmit}>
            <h2>ユーザ登録フォーム(useRef)</h2>
            <label>ユーザID
                <input ref={nameRef} />
            </label>
            <label>パスワード
                <input type="password" ref={passwordRef} />
            </label>
            <label>パスワード（再入力）
                <input type="password" ref={checkPasswordRef} />
            </label>
            <label>メールアドレス
                <input ref={mailAddressRef} />
            </label>
            <button type="submit">登録</button>
        </form>
    )
}
```


## 数字から比較してみる
[npm trends](https://npmtrends.com/formik-vs-react-final-form-vs-react-hook-form)で比較してみた結果がこちらです。
※画像は執筆時点（2023/02/06）の情報です。

* ダウンロード数
![donwload.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3125818/59624a73-c0dc-dab6-06f1-27cc98ef5eed.png)

* その他数値
![stats.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3125818/3494f67a-ed77-a952-6380-2d51132c17d2.png)

ダウンロード数やスター数、更新頻度など、数値だけで見るとreact-hook-formが何かと強い印象です。

## 比較表
星が多いほど高評価
※筆者の主観で付けています

|                 | react-hook-form | Formik | react-final-form | 
| --------------- | --------------- | ------ | ---------------- | 
| 公式ドキュメント | ★★★ | ★★☆ | ★★☆ | 
| 日本語ドキュメント | ★★★ | ★★★ | ★☆☆ | 
| TypeScriptサポート | ★★★ | ★★★ | ★★★ | 
| ダウンロード数 | ★★★ | ★★★ | ★☆☆ | 
| アップデート | ★★★ | ★☆☆ | ★★☆ |
| パフォーマンス | ★★★ | ★☆☆ | ★★★ | 
| 記事数（Qiita） | 107 | 42 | 6 |
| 制御 | 非制御コンポーネント | 制御コンポーネント | 制御コンポーネント |
| メイン実装 | Hooks | Component | Component | 
| バリデーション | ビルトイン<br> 外部ライブラリ | 自力実装<br>外部ライブラリ | 自力実装<br>外部ライブラリ | 


## [react-hook-form](https://www.react-hook-form.com/)

react-hook-formは内部でuseRefを用いた非制御コンポーネントのフォームライブラリです。
現在も繰り返しアップデートが行われており、今回の比較対象の中では最も新しいライブラリです。

基本的にはuseForm()フックを用いてフォームを実装します。

### バリデーション付きフォーム実装例

![form_Trim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3125818/80220532-2261-639a-7c8a-fe103b89983e.gif)

```ReactHookForm.tsx
import { useForm, SubmitHandler } from 'react-hook-form';

type Form = {
    "name": string,
    "password": string,
    "checkPassword": string
}

export const ReactHookForm = () => {
    const { register, handleSubmit, formState, getValues } = useForm<Form>({
        mode: "onTouched",
        defaultValues: {
            name: "",
            password: "",
            checkPassword: ""
        },
        shouldUnregister: false
    });
    const submitForm: SubmitHandler<Form> = (value) => {
        console.log(value);
    }
    return (
        <form onSubmit={handleSubmit(submitForm)}>
            <h2>ユーザ登録フォーム(react-hook-form)</h2>
            <label>ユーザID
                <input {...register("name", {
                        required: {value: true, message: "ユーザIDは入力必須です。"},
                        minLength: {value: 4, message: "ユーザIDは4文字以上で入力してください。"},
                        maxLength: {value: 20, message: "ユーザIDは20文字以内で入力してください。"},
                    })} />
            </label>
            <span>{formState.errors.name && formState.errors.name.message}</span>
            <label>パスワード
                <input type="password" {...register("password", {
                        required: {value: true, message: "パスワードは入力必須です。"},
                        pattern: {value: /^[a-z\d]{4,20}$/i, message: "パスワードは半角英数4文字以上20文字以内で入力してください。"}
                    })} />
            </label>
            <span>{formState.errors.password && formState.errors.password.message}</span>
            <label>パスワード（確認用）
                <input type="password" {...register("checkPassword", {
                        required: {value: true, message: "確認用パスワードは入力必須です。"},
                        validate: (val) => val === getValues("password") ? true : "パスワードと確認用パスワードは同じ内容を入力してください。"
                    })} />
            </label>
            <span>{formState.errors.checkPassword && formState.errors.checkPassword.message}</span>
            <div className="action-area">
                <button type="reset" disabled={!formState.isDirty}>リセット</button>
                <button type="submit" disabled={!formState.isValid}>登録</button>
            </div>
        </form>
    )
}
```

### react-hook-formのメリット

* 公式ドキュメントの情報が豊富
各種APIのリファレンス、型情報、JavaScript,TypeScript両方のサンプルコード提供に加え、CodeSandBox、外部ライブラリ連携方法、GUIによるコード生成ツールetc...
日本語対応はしていないものの、初心者は取り合えず公式ドキュメントを見れば大抵のことは分かる！というレベルで情報が豊富です。

* DevToolをChrome拡張にて提供
ChromeのDeveloper Tool内でフォームバリデーションのデバッグを行えるDevToolを公式が提供しており、開発時にとても役に立ちます。

* 日本語のドキュメントが豊富
ダウンロード数が多く、広く使われていることから日本語の技術記事も多く存在しています。

* ビルトインバリデーションにより、簡単なバリデーションを実装できる
上記のコード例のようにバリデーション機能をビルトインで持っているため、わざわざ自前でバリデーション機能を実装する必要なく簡単にバリデーションを実装出来ます。


### react-hook-formのデメリット

* 互換性のないUIライブラリとの連携が面倒
propsに直接{...register("$formname")}として値を渡せないコンポーネントに対しては別途useController()フック、\<Controller />コンポーネントを使用する必要があります。


## [Formik](https://formik.org/docs/overview)

Formikは内部にContextを用いた制御コンポーネントのフォームライブラリです。
今回の比較対象の中では最も古いライブラリで2年前に更新が止まっていますが、未だにダウンロード数は衰えていない人気の高いライブラリです。
基本的には\<Formik />、\<Form /> 、\<Field />といったコンポーネントを用いてフォームを実装します。

### バリデーション付きフォーム実装例

```FormikForm.tsx
import { ErrorMessage, Field, Form, Formik, FormikErrors, FormikProps } from "formik";

type Form = {
    userName: string,
    password: string,
    checkPassword: string
}

export const FormikForm = () => {
    const initialValues: Form = {
        userName: "",
        password: "",
        checkPassword: ""
    }
    const validate = (values: Form) => {
        const errors: FormikErrors<Form> = {};
        if (!values.userName) {
            errors.userName = "ユーザIDは入力必須です。";
        }
        if (!errors.userName && values.userName.length < 4) {
            errors.userName = "ユーザIDは4文字以上で入力してください。";
        }
        if (!errors.userName && values.userName.length > 20) {
            errors.userName = "ユーザIDは20文字以内で入力してください。";
        }
        if (!values.password) {
            errors.password = "パスワードは入力必須です。";
        }
        if (!errors.password && !/^[a-z\d]{4,20}$/.test(values.password)){
            errors.password = "パスワードは半角英数4文字以上20文字以内で入力してください。"
        }
        if (!values.checkPassword) {
            errors.checkPassword = "確認パスワードは必須です。";
        }
        if (values.checkPassword && values.password !== values.checkPassword) {
            errors.checkPassword = "確認パスワードの値が異なります。"
        }
        return errors;
    }
    return (
        <Formik initialValues={initialValues} onSubmit={(values) => {console.log(values);}} validate={validate}
            validateOnBlur={false} validateOnChange={true} validateOnMount={false}>
            {({ errors, isValid, dirty }: FormikProps<Form>) => {
            return (
                <Form>
                    <h2>ユーザ登録フォーム(formik-Component)</h2>
                    <label>ユーザID
                        <Field name="userName" />
                    </label>
                    <ErrorMessage className="error-message" name="userName">{msg => <span>{msg}</span>}</ErrorMessage>
                    <label>パスワード
                        <Field type="password" name="password" />
                    </label>
                    <ErrorMessage name="password">{msg => <span>{msg}</span>}</ErrorMessage>
                    <label>パスワード（確認用）
                        <Field type="password" name="checkPassword" />
                    </label>
                    <ErrorMessage name="checkPassword">{msg => <span>{msg}</span>}</ErrorMessage>
                    <div className="action-area">
                        <button type="reset" disabled={!dirty}>リセット</button>
                        <button type="submit" disabled={!dirty || !isValid}>登録</button>
                    </div>
                </Form>
            )}}
        </Formik>
    )
}
```

### Formikのメリット

* 公式ドキュメントの情報が豊富
APIリファレンスをはじめ、YupやMUIといったサードパーティ製ライブラリとの連携方法、それらのCodeSandBoxの提供等、公式ドキュメントに欲しい情報がしっかりと載っています。
こちらも日本語対応はしていないものの、react-hook-formと同様、初心者ならとりあえず公式ドキュメント（＆DeepL）で問題ないレベルです。

* 日本語ドキュメントが豊富
react-hook-formと同様にダウンロード数が多いため、日本語の技術記事が多く存在し、知見を得やすいです。

### Formikのデメリット

* 再レンダリングが多く、パフォーマンスが悪い
FormikのコンポーネントはContextを用いた制御コンポーネントとして実装されているため、入力毎に再レンダリングされてしまう影響で他ライブラリに比べてパフォーマンスが悪い傾向にあります。
Contextを使わないようにFormikを使うにはuseFormik()フックを使う方法がありますが、その場合はContextを用いて実装されている\<Formik />、\<Filed />といったコンポーネントを使用できず、独自でそれらに代わるコンポーネントを実装する必要があります。

* バリデーションの実装は外部ライブラリへの依存が大きい
react-hook-formと異なりビルトインバリデーションが存在しないため、外部ライブラリを使用しない場合は上記のソースのように自力でバリデーションの実装が必要となります。
実質的に外部ライブラリが必須と言えるでしょう。公式ドキュメントではバリデーションライブラリであるYupを薦めており、連携方法も記載されています。

## [react-final-form](https://final-form.org/docs/react-final-form/getting-started)

react-final-formはfinal-formというフォームライブラリをReact向けにラップした制御コンポーネントのライブラリです。
今回の比較対象の中では2番目に古いライブラリで年に数回アップデートが行われています。
基本的には提供される\<Form />、\<Field />コンポーネントにRender Propsを渡して実装を行います。
別途提供される`react-final-form-hooks`というライブラリを使用することでHooksを用いた実装を行うことが可能ですが、再レンダリングが増えてパフォーマンスが低下する、開発者の管理対象が増えるなど使用感がかなり異なります。

### バリデーション付きフォーム実装例

```ReactFinalForm.tsx
import { FieldValidator, ValidationErrors } from "final-form";
import { Form, Field } from "react-final-form";

type Form = {
    userName: string,
    password: string,
    checkPassword: string
}

export const ReactFinalForm = () => {
    const onSubmit = (values: Form) => {
        console.log(values);
    }
    const validateForm = (values: Form) => {
        const errors: ValidationErrors = {
            userName: (() => {
                if (!values.userName) {
                    return ["ユーザIDは入力必須です。"]
                }
                if (values.userName.length < 4) {
                    return ["ユーザIDは4文字以上で入力してください。"]
                }
                if (values.userName.length > 20) {
                    return ["ユーザIDは20文字以内で入力してください。"]
                }
                return undefined;
            })(),
            password: (() => {
                if (!values.password) {
                    return ["パスワードは入力必須です。"]
                }
                if (!/^[a-z\d]{4,20}$/.test(values.password)) {
                    return ["パスワードは半角英数4文字以上20文字以内で入力してください。"]
                }
                return undefined;
            })(),
            checkPassword: (() => {
                if (values.password !== values.checkPassword) {
                    return ["パスワードと確認用パスワードは同じ内容を入力してください。"]
                }
                return undefined;
            })()
        }
        return errors;
    }
    return (
        <Form onSubmit={onSubmit} initialValues={{ userName: "", password: "", checkPassword: ""}} subscription={{validating: true, dirty: true, valid: true}}
            validate={validateForm} render={({ handleSubmit, dirty, valid }) => (
                <form onSubmit={handleSubmit}>
                    <h2>ユーザ登録フォーム(FinalForm)</h2>
                        <Field name="userName" type="text" render={({input, meta}) => (
                            <>
                                <label>ユーザID
                                    <input {...input} />
                                </label>
                                {meta.error && meta.touched &&<span className="error-message" >{meta.error}</span>}
                            </>
                        )} />
                        <Field type="password" name="password" render={({input, meta}) => (
                            <>
                                <label>パスワード
                                    <input {...input} />
                                </label>
                                {meta.error && meta.touched && <span className="error-message">{meta.error}</span>}
                            </>
                        )} />
                        <Field type="password" name="checkPassword" render={({input, meta}) => (
                            <>
                                <label>パスワード（確認用）
                                    <input {...input} />
                                </label>
                                {meta.error && meta.touched && <span className="error-message">{meta.error}</span>}
                            </>
                        )} />
                    
                    <div className="action-area">
                        <button type="reset" disabled={!dirty}>リセット</button>
                        <button type="submit" disabled={!valid}>登録</button>
                    </div>
                </form>
            )}
        />
    )
}
```

### react-final-formのメリット

* パフォーマンスに優れる
Observerパターンを用いた変更検知によって必要な箇所のみの再レンダリングに抑えられるため、高いパフォーマンスを誇っています。
また、バリデーションのタイミング（デフォルトだと全ての変更を検知）を変更することによって更にレンダリング回数を減らすことも可能です。

* 公式提供のSandBoxが豊富
基本的なパターンに加えてサードパーティのバリデーション、UIライブラリ連携、クレジットカードのようなユースケースといった様々なパターンのCodeSandBox（またはGitHubリポジトリ）が用意されています。

* 他フォームライブラリからの移行ガイドが存在
Formik、同じ作者のフォームライブラリであるRedux-Formを使用している場合からの移行ガイドが存在しています。

### react-final-formのデメリット

* 他比較対象に比べて公式ドキュメントが弱い
CodeSandBoxは様々なパターンが用意されていますが、APIのリファレンスとしてみると他2つのライブラリよりも情報量として少ない印象です。初心者はドキュメントで理解するよりCodeSandBoxのコードを見て理解するという形になりそうです。
また、TypeScriptを使用する場合のサンプルコードが少ないのも個人的にはマイナスです。

* 日本語のドキュメントが少ない
他の2ライブラリに比べてダウンロード数が少ない影響か、日本語のドキュメントがかなり少ないです。

* バリデーションは外部への依存が大きい
Formikと同様にビルトインバリデーションが存在しないため、外部ライブラリを使わない場合は自前でのバリデーション実装が必要となります。

## まとめ

さて、簡単にReactのフォームライブラリを比較してみましたがいかがでしたでしょうか？
個人的にはどのライブラリも学習コストやコードの記述しやすさに明確に違いがあるとは（少なくともチュートリアルレベルでは）感じないので
1. パフォーマンスが高い
1. 日本語のドキュメントが豊富
1. ライブラリの更新頻度が高い

の3点でreact-hook-formをおススメしたいですね。