Reactフォームライブラリ、何を組み合わせればいい? | 技術ブログ
はじめにライブラリを使わないフォーム今回実装する画面実際のコードフォーム本体バリデーション定数、型、スタイルフォームの構成要素全体の把握画面UIフォームの状態管理バリデーションライブラリの使用状...
https://www.wantedly.com/companies/jointcrew/post_articles/1037611
はじめに
ライブラリにおける思想の違い
controlled(制御)かuncontrolled(非制御)か
Formikのアプローチ
React Hook Formのアプローチ
Formikを使った実装
フォーム本体
Formikの基本的な使い方
フォームの初期化
Formikの情報へのアクセス
React Hook Formを使った実装
フォーム本体
バリデーション処理
React Hook Formの基本的な使い方
フォームの初期化
React Hook Formの情報へのアクセス
まとめ
前回はライブラリを使わずにフォームを実装し、「画面UI」、「フォームの状態管理」、「バリデーション」の3つの要素から構成されることを確認しました。
本記事ではその中でも「フォームの状態管理」を扱います。
前回のように自分で状態管理を実装する場合、値やエラーの情報、フォームの編集状態などを一つ一つ関数を使用して管理していました。
この方法だとフォームの要素の分だけ作成する必要があり、かなり煩雑になります。
これらを解消してくれるのが、FormikやReact Hook Formといった状態管理のためのライブラリです。
実際の実装をそれぞれ比較しながら、考え方の違いを紹介していきます。
なお、本記事ではフォームのライブラリ以外のソースであるバリデーション(validation.ts)、型(types.ts)、定数(constants.ts)、スタイル(styles.ts)については引き続き同じものを使用します。
※UIについてはMUIを使う想定でしたが、状態管理ライブラリの差異のみにフォーカスするため、引き続き自前の実装を使うこととします。
FormikとReact Hook Formの一番の違いはcontrolled(制御)コンポーネントであるか、uncontrolled(非制御)コンポーネントであるかということです。
controlledコンポーネントとは、親のコンポーネントから子に対してpropsを受け渡すことで子の振る舞いを決めるものを指します。
uncontrolledコンポーネントとは、子のコンポーネント内に定義したstateによって振る舞いを決めるものを指します。
React公式ドキュメントの説明に
実際には、“制御された”、“非制御” は技術用語として厳密なものではありません。
とあるように明確な定義があるわけではありませんが、フォームライブラリの文脈においてはフォームの値をどのように扱うかという点で違いがあります。
具体的にはフォームの値をpropsを通じて渡すのがcontrolled、別の方法で値を管理するのがuncontrolledということになります。
それぞれのライブラリの扱いについて確認します。
Formikライブラリの公式サイトに明示的な記載があるわけではありませんが、その書き方からcontrolledであることがわかります。
/* サンプルコード */
const formik = useFormik({
initialValues: { email: '' },
onSubmit: values => { /* ... */ },
});
return (
<form onSubmit={formik.handleSubmit}>
<input
name="email"
onChange={formik.handleChange} // 入力 → stateを更新
value={formik.values.email} // state → inputに反映
/>
</form>
);Formik特有の記載については後ほど解説するので、ここではinputコンポーネントのonChange、valueを通じて値や処理を受け渡していることに着目してください。
inputコンポーネントに表示される値はpropsを通じてのみ変更できます。
フォームに値を入力するとonChangeを通じてstateが更新され、コンポーネントの再レンダリングが発生します。
Formikにおいては全フィールドの値を一つのstateで管理しているため、あるフィールドが変更されるとフォーム全体が再レンダリングされます。
React Hook Formは公式がuncontrolledであることを明示しています。
React Hook Form relies on an uncontrolled form,
コード例を見たほうが早いので確認してみましょう。
/* サンプルコード */
const { register, handleSubmit } = useForm({
defaultValues: { email: '' },
});
const emailField = register('email');
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
name={emailField.name}
onChange={emailField.onChange} // RHF内部ストアへの通知
ref={emailField.ref} // DOM参照
// value がない = DOMが値を保持
/>
</form>
);Formikとの決定的な違いはvalueを渡していないことです。
値はrefを通じてDOMで管理されます。つまりstateではなく、ブラウザのinput要素が本来持っている値の保持機能をそのまま使います。
stateを経由しないため、値を入力するたびにコンポーネントが再レンダリングされることはありません。
propsに渡しているonChangeも値の変更を管理するものではなく、バリデーションタイミングやフォームの変更判定などに使われます。
2つのライブラリの考え方の違いがわかったところで、前回の記事のフォームをそれぞれのライブラリで書くとどうなるのか見てみましょう。
まずはFormikを使った実装です。
なお、以下のコードを動かすためにはFormikライブラリのインストールが必要です。
npm install formik 少し長いですが、全体の実装がこちらです。
詳細は後ほど解説します。
/* FormikForm.tsx */
import { useFormik } from "formik";
import type { FormValues } from "./types";
import { GENDER_OPTIONS, OCCUPATION_OPTIONS, INTEREST_OPTIONS } from "./constants";
import { validate } from "./validation";
import { styles } from "./styles";
export const FormikForm = () => {
const { values, errors, touched, getFieldProps, setFieldValue, setFieldTouched, handleSubmit } =
useFormik<FormValues>({
initialValues: {
username: "",
age: "",
occupation: "",
gender: "",
interests: [],
agreement: false,
},
validate,
validateOnChange: false,
validateOnBlur: true,
onSubmit: (values) => console.log("フォームが正常に送信されました:", values),
});
return (
<form onSubmit={handleSubmit} style={styles.form}>
<h2>ユーザー登録</h2>
{/* ユーザー名 */}
<div style={styles.field}>
<label>ユーザー名:</label>
<input type="text" {...getFieldProps("username")} />
{touched.username && errors.username && <span style={styles.error}>{errors.username}</span>}
</div>
{/* 年齢 */}
<div style={styles.field}>
<label>年齢:</label>
<input type="number" {...getFieldProps("age")} />
{touched.age && errors.age && <span style={styles.error}>{errors.age}</span>}
</div>
{/* 性別 */}
<div style={styles.field}>
<label>性別:</label>
{GENDER_OPTIONS.map((option) => (
<label key={option} style={styles.radioLabel}>
<input
type="radio"
{...getFieldProps("gender")}
value={option}
checked={values.gender === option}
/>
{option}
</label>
))}
{touched.gender && errors.gender && <span style={styles.error}>{errors.gender}</span>}
</div>
{/* 職業 */}
<div style={styles.field}>
<label htmlFor="occupation">職業:</label>
<select id="occupation" {...getFieldProps("occupation")}>
<option value="">選択してください</option>
{OCCUPATION_OPTIONS.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
{touched.occupation && errors.occupation && (
<span style={styles.error}>{errors.occupation}</span>
)}
</div>
{/* 興味・関心 */}
<div style={styles.field}>
<label>興味・関心:</label>
{INTEREST_OPTIONS.map((option) => (
<label key={option} style={styles.checkboxLabel}>
<input
type="checkbox"
value={option}
checked={values.interests.includes(option)}
onChange={() => {
const next = values.interests.includes(option)
? values.interests.filter((v) => v !== option)
: [...values.interests, option];
setFieldValue("interests", next);
}}
onBlur={() => setFieldTouched("interests", true)}
/>
{option}
</label>
))}
{touched.interests && errors.interests && (
<span style={styles.error}>{errors.interests}</span>
)}
</div>
{/* 利用規約への同意 */}
<div style={styles.field}>
<label style={styles.checkboxLabel}>
<input
type="checkbox"
checked={values.agreement}
onChange={(e) => setFieldValue("agreement", e.target.checked)}
onBlur={() => setFieldTouched("agreement", true)}
/>
利用規約に同意する
</label>
{touched.agreement && errors.agreement && (
<span style={styles.error}>{errors.agreement}</span>
)}
</div>
<button type="submit" style={styles.button}>
登録
</button>
</form>
);
};Formikを利用するには2つのアプローチがあります。
まずは提示した実装のようなuseFormikというカスタムフックを使う方法と、<Formik>というコンポーネントを使う方法です。
Formikの公式が推奨するのは<Formik>コンポーネントを使った実装です。
このコンポーネント内では内部的にuseFormikとContextを組み合わせて使用しています。これにより、コンポーネントを分割してもpropsのバケツリレーなしで、Contextを使用してフォームの値やエラー情報にアクセスできます。
本記事ではコンポーネントの分割をせず、Contextを使わないことからuseFormikを使った実装としています。
useFormikフックを使用してフォームの初期化を行います。
このカスタムフックはフォームの設定情報を引数に取ります。設定できる情報が多いため、代表的なもののみ紹介します。
initialValues
フォームの初期値をオブジェクトの形式で設定します。Formikの情報にアクセスする際、このオブジェクトのプロパティ名を指定します。
validate
バリデーションの処理を設定します。ここでは前回も使用した自作のバリデーション処理を設定しています。バリデーションが発火するタイミングはsubmit時のほか、後述のオプションでコントロールすることが出来ます。
validateOnChange, validateOnBlur
バリデーションの発火タイミングを設定できます。デフォルト値はtrueです。
validateOnChangeはonChangeイベントのほか、setFieldValueの呼び出し時にもバリデーションが発火します。
validateOnBlurはonBlurイベントのほか、setFieldTouchedの呼び出し時にもバリデーションが発火します。
フィールドの値が変更されるたび発火するのを防ぐため、ここではvalidateOnChangeをfalseとしています。
onSubmit
submit時の挙動を設定します。後述のhandleSubmitで実施される処理です。
useFormikの戻り値を使うことでFormikの情報にアクセスしたり、情報を設定したりすることができます。情報の取得は基本的にinitialValuesで設定したオブジェクトのプロパティ名を指定します。
こちらも多くの値や処理があるため、代表的なものを紹介します。
values
フィールドの値がオブジェクトの形式で設定されています。initialValuesで指定した初期値から、入力に応じて更新されます。
errors
バリデーションエラーの結果がオブジェクトの形式で設定されています。エラーがあるフィールドのみプロパティが含まれます。
touched
フィールドを訪問済みかどうかがオブジェクトの形式で設定されています。
フィールドからフォーカスが外れた時点でtrueになります。
getFieldProps
value、name、onChange、onBlurなどフィールドに関わる値や処理を取得できます。
基本的なテキストフィールドの場合はスプレッド演算子を使用してgetFieldPropsの戻り値を展開することで、簡単にフィールドの状態管理を行うことが出来ます。
setFieldValue
フィールドの値を直接設定することができます。
チェックボックスのように値の加工が必要なフィールドにはこの処理を使用して対応します。
setFieldTouched
フィールドの訪問状態を直接設定することが出来ます。
チェックボックス系のフィールドではonChangeのハンドラ内でsetFieldValueを呼ぶため、getFieldPropsでまとめて展開せず、onBlurもこの処理で個別に設定しています。
handleSubmit
submit時の処理です。バリデーションエラーがない場合にのみ、useFormikで設定したonSubmitが実行されます。
なお、submit時には全フィールドのtouchedがtrueに設定されるため、未訪問のフィールドにエラーがあればこのタイミングで表示されます。
コードを動かすにはReact Hook Formライブラリのインストールが必要です。
npm install react-hook-form Formikと同様、詳細な解説は後ほど行います。
/* RHFForm.tsx */
import { useForm } from "react-hook-form";
import type { FormValues } from "./types";
import { GENDER_OPTIONS, OCCUPATION_OPTIONS, INTEREST_OPTIONS } from "./constants";
import { validationResolver } from "./validationResolver";
import { styles } from "./styles";
export const RHFForm = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
defaultValues: {
username: "",
age: "",
occupation: "",
gender: "",
interests: [],
agreement: false,
},
resolver: validationResolver,
mode: "onBlur",
reValidateMode: "onBlur",
});
const onSubmit = (values: FormValues) => console.log("フォームが正常に送信されました:", values);
return (
<form onSubmit={handleSubmit(onSubmit)} style={styles.form}>
<h2>ユーザー登録</h2>
{/* ユーザー名 */}
<div style={styles.field}>
<label>ユーザー名:</label>
<input type="text" {...register("username")} />
{errors.username && <span style={styles.error}>{errors.username.message}</span>}
</div>
{/* 年齢 */}
<div style={styles.field}>
<label>年齢:</label>
<input type="number" {...register("age")} />
{errors.age && <span style={styles.error}>{errors.age.message}</span>}
</div>
{/* 性別 */}
<div style={styles.field}>
<label>性別:</label>
{GENDER_OPTIONS.map((option) => (
<label key={option} style={styles.radioLabel}>
<input type="radio" value={option} {...register("gender")} />
{option}
</label>
))}
{errors.gender && <span style={styles.error}>{errors.gender.message}</span>}
</div>
{/* 職業 */}
<div style={styles.field}>
<label htmlFor="occupation">職業:</label>
<select id="occupation" {...register("occupation")}>
<option value="">選択してください</option>
{OCCUPATION_OPTIONS.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
{errors.occupation && <span style={styles.error}>{errors.occupation.message}</span>}
</div>
{/* 興味・関心 */}
<div style={styles.field}>
<label>興味・関心:</label>
{INTEREST_OPTIONS.map((option) => (
<label key={option} style={styles.checkboxLabel}>
<input type="checkbox" value={option} {...register("interests")} />
{option}
</label>
))}
{errors.interests && <span style={styles.error}>{errors.interests.message}</span>}
</div>
{/* 利用規約への同意 */}
<div style={styles.field}>
<label style={styles.checkboxLabel}>
<input type="checkbox" {...register("agreement")} />
利用規約に同意する
</label>
{errors.agreement && <span style={styles.error}>{errors.agreement.message}</span>}
</div>
<button type="submit" style={styles.button}>
登録
</button>
</form>
);
};React Hook Formのバリデーションを扱うために処理が1つ増えます。
こちらの処理は本筋ではないので、役割については「resolver」の項目で簡単に触れます。
/* validationResolver.ts */
import type { Resolver } from "react-hook-form";
import type { FormValues } from "./types";
import { validate } from "./validation";
export const validationResolver: Resolver<FormValues> = (values) => {
const validationErrors = validate(values);
const hasErrors = Object.keys(validationErrors).length > 0;
return {
values: hasErrors ? {} : values,
errors: hasErrors
? Object.fromEntries(
Object.entries(validationErrors).map(([key, message]) => [
key,
{ type: "validation", message },
]),
)
: {},
};
};React Hook FormはuseFormというカスタムフックを使用する方法のみが用意されています。
useFormフックを使用してフォームの初期化を行います。
Formikと同様、設定情報をオブジェクトとして渡します。
defaultValues
フォームの初期値をオブジェクトの形式で設定します。React Hook Formの情報にアクセスする際、このオブジェクトのプロパティ名を指定します。
resolver
バリデーションの処理を設定します。処理はvaluesとerrorsプロパティの両方をもつオブジェクトを返す必要があるため、前回作成した自作のバリデーションをそのまま使えません。
そこで「validationResolver.ts」という変換処理を使用することでReact Hook Formのバリデーション処理に適合するようにしています。
mode
submit前のバリデーションが発火するタイミングを設定します。デフォルトは「onSubmit」でsubmit時にのみ発火します。そのほか「onBlur」、「onChange」、「onTouched」、「all」があります。
reValidateMode
submitしたあと、エラーがあるフィールドに対して再度バリデーションが発火するタイミングを設定します。デフォルトは「onChange」で変更のたびに発火します。そのほか「onBlur」、「onSubmit」があります。
useFormの戻り値を使うことでReact Hook Formの情報にアクセスしたり、情報を設定したりすることができます。
こちらも代表的なものを紹介します。
register
フィールドの登録を行う関数です。name、ref、onChange、onBlurを返します。
ラジオボタンやチェックボックスでも特殊な書き方をする必要なく、スプレッド演算子で展開して使用します。
handleSubmit
submit時の処理です。バリデーションエラーがない場合にのみ、引数に渡したコールバックが実行されます。
submit時には全フィールドのバリデーションが実行され、エラーがあればerrorsに格納されます。
formState: { errors }
バリデーションエラーの結果がオブジェクトの形式で格納されています。エラーがあるフィールドのみプロパティが含まれます。
errors.usernameはオブジェクトであり、errors.username.messageでエラーメッセージにアクセスします。
messageのほかにtype(バリデーションルールの種別)も保持しています。
本記事ではFormikとReact Hook Formを使ったフォームの状態管理を比較しました。
Formikはcontrolledアプローチで、全フィールドの値をReactのstateとして一元管理します。シンプルに書ける反面、1つのフィールドの変更がフォーム全体の再レンダリングを引き起こします。
React Hook Formはuncontrolledアプローチで、値はDOMで保持されており、refによる参照を利用することで、Reactの再レンダリングを抑えながらフォームの状態を管理します。
このパフォーマンス面の優位性が、React Hook Formが広く採用されている理由の一つと言えます。
一方で、本記事ではバリデーションについては前回と同じく自前の処理を使いましたが、React Hook FormではResolver形式への変換が必要になるなど、ライブラリによって接続方法が異なることもわかりました。次回はバリデーション自体に焦点を当て、YupとZodを比較していきます。