NextでのFormのライブラリ選定をするためにFormikとReact Hook Formを実装してみて
概要
個人的には下記の観点から React Hook Form を推す方向で進めようと思った
- React Hook Formはドキュメントがおしゃんで、さらに日本語対応してる
- useFormContext や FormProvider など、既存の React Context と同じような使用感でコードをわかりやすくかけた(とっつきやすかった)
-
ref={register}
がめちゃめちゃ楽
比較
input 入力時のレンダリング回数等のパフォーマンスの計測はしてませぬ
あくまで実装の違いのみ
ただ React Hook Form のドキュメントの中でわかりやすい比較表を作ってくれてるのでそれが参考になるかと(いや、ほんとは自分で調べないといけないと思ってるんですよ・・わかってるんですよ・・)
環境
{
"dependencies": {
"@hookform/resolvers": "1.3.2",
"formik": "2.2.6",
"next": "10.0.3",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-hook-form": "6.14.0",
"yup": "0.32.8"
}
}
全コード
Formik
インストール
yarn add formik yup
yarn add -D @types/yup
基本
useFormik
を使った hook での書き方と<Formik />
や<Field />
を使ったコンポーネントでの書き方二つある
hook を使った書き方はチュートリアルのコードを参考に。
今回は慣れ親しんだコンポーネントでの書き方を記載する
一つの form を presenter と container と<Formik />
コンポーネントを定義する部分の三つに分ける
それぞれざっくり下記の役割
- presenter→form の ui
- container→useEffect とか副作用
<Formik />
presenter
チュートリアルから抜粋
Form や Field は formik から import したもの
export type ExampleFormValues = {
firstName: string;
lastName: string;
email: string;
};
export const ExampleFormPresenter = (): JSX.Element => (
<Form>
<label htmlFor="firstName">First Name</label>
<Field name="firstName" type="text" />
<ErrorMessage name="firstName" />
<label htmlFor="lastName">Last Name</label>
<Field name="lastName" type="text" />
<ErrorMessage name="lastName" />
<label htmlFor="email">Email Address</label>
<Field name="email" type="email" />
<ErrorMessage name="email" />
<button type="submit">Submit</button>
</Form>
);
container
例)親コンポーネント側で props を通じて form の値を変えたい時
export type ExampleFormContainerProps = {
formValues?: ExampleFormValues;
};
export const ExampleFormContainer = ({
formValues,
}: ExampleFormContainerProps): JSX.Element => {
const { setValues } = useFormikContext<ExampleFormValues>();
useEffect(() => {
setValues(formValues);
}, [formValues]);
return <ExampleFormPresenter />;
};
<Formik />
container を<Formik />
でラップする
export type ExampleFormProps = {
handleSubmit: (values: ExampleFormValues) => void;
formValues?: ExampleFormValues;
};
export const ExampleForm = ({
handleSubmit,
formValues,
}: ExampleFormProps): JSX.Element => {
const onSubmit = (values: ExampleFormValues) => {
handleSubmit(values);
};
const initialValues: ExampleFormValues = formValues || {
firstName: '';
lastName: '';
email: '';
};
return (
<Formik
initialValues={initialValues}
onSubmit={onSubmit}
>
<ExampleFormContainer formValues={formValues} />
</Formik>
);
};
バリデーション
独自にバリデーションのルールを定義できる
が、ある程度汎用的なルールはあるものを使いたいので Yup が使えるvalidationSchemaの実装例を記載する
Formik の validationSchema(prop) に yup で定義したバリデーションルールを入れる感じ
import * as Yup from "yup";
export const validationSchema = Yup.object().shape({
firstName: Yup.string().required(),
lastName: Yup.string().required(),
email: Yup.string().email(),
});
export const ExampleForm = ({
// ....省略
}: ExampleFormProps): JSX.Element => {
// ....省略
return (
<Formik
validationSchema={validationSchema}
>
// ....省略
);
};
エラーメッセージの日本語化
このままだとバリデーションに引っ掛かった時のエラーメッセージを日本語化する
setLocal の中のオブジェクトのプロパティはここを参照
import { setLocale } from "yup";
setLocale({
mixed: {
required: () => `入力必須項目です。`,
},
string: {
min: ({ min }) => `${min}文字以上で入力して下さい。`,
},
});
Yup でカスタムバリデーションの定義
test('testの名前', 'エラーメッセージ', (value): boolean => {カスタムバリデーションの定義})
value は直前の型に影響される(string()なら string になる)
例)文字列がYYYY/MM/DD
になっているかどうか
const validationSchema = Yup.object().shape({
date: Yup.string()
.required()
.test("checkDateFormat", "日付の形式が間違ってます", (value): boolean => {
if (!dayjs(value).isValid()) return false;
const format = "YYYY/MM/DD";
return dayjs(value, format).format(format) === value;
}),
});
FieldArray
同一 name の input に配列入れたい時に使うやつ
formik ドキュメントの該当箇所
実装例
type Friend = {
name: string;
email: string;
};
export type ExampleFormValues = {
friends: Friend[];
};
// ...省略
<Field name="friends">
{({ field }: FieldProps<ExampleFormValues["friends"]>) => (
<FieldArray name={field.name}>
{({ remove, push }: ArrayHelpers) => (
<div>
{field.value.map((f, i) => (
<div key={i}>
<div>
<InputText
name={`friends.${i}.name`}
value={f.name}
onChange={field.onChange}
/>
<ErrorMessage name={`friends.${i}.name`} />
</div>
<div>
<InputText
name={`friends.${i}.email`}
value={f.email}
onChange={field.onChange}
/>
<ErrorMessage name={`friends.${i}.email`} />
</div>
<button
type="button"
onClick={() => {
remove(i);
}}
>
削除
</button>
</div>
))}
<button
type="button"
onClick={() => {
push({ name: "", email: "" });
}}
>
友達追加
</button>
</div>
)}
</FieldArray>
)}
</Field>;
ArrayHelpers が list を扱う上で便利な helper 用意してくれてる
React Hook Form
インストール
yarn add react-hook-form @hookform/resolvers
基本
比較しやすいように formik でした実装と同じ感じ(presenter, container, <FormProvider />)で実装してみる
presenter
presenter, container, <FormProvider />で分ける場合は子コンポーネントは useFormContext で context を引き回す感じだと思われた
export type ExampleFormValues = {
firstName: string;
lastName: string;
email: string;
};
export type ExampleFormPresenterProps = {
onSubmit: (values: ExampleFormValues) => void;
};
export const ExampleFormPresenter = ({
onSubmit,
}: ExampleFormPresenterProps): JSX.Element => {
const {
register,
handleSubmit,
errors,
} = useFormContext<ExampleFormValues>();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="firstName">firstName</label>
<input name="firstName" ref={register} />
{errors.firstName && <p>{errors.firstName.message}</p>}
</div>
<div>
<label htmlFor="lastName">lastName</label>
<input name="lastName" ref={register} />
{errors.lastName && <p>{errors.lastName.message}</p>}
</div>
<div>
<label htmlFor="email">email</label>
<input name="email" ref={register} />
{errors.email && <p>{errors.email.message}</p>}
</div>
<button type="submit">Submit</button>
</form>
);
};
container
例)親コンポーネント側で props を通じて form の値を変えたい時
export type ExampleFormContainerProps = {
onSubmit: (values: ExampleFormValues) => void;
formValues?: ExampleFormValues;
};
export const ExampleFormContainer = ({
onSubmit,
formValues,
}: ExampleFormContainerProps): JSX.Element => {
const { reset } = useFormContext<ExampleFormValues>();
useEffect(() => {
reset(formValues);
}, [formValues]);
return <ExampleFormPresenter onSubmit={onSubmit} />;
};
<FormProvider />
hook が主としての使い方だから form が 1 コンポーネントで完結する場合は使わないけど子コンポーネントに派生する場合に<FormProvider />
使う
export type ExampleFormProps = {
onSubmit: (values: ExampleFormValues) => void;
formValues?: ExampleFormValues;
};
export const ExampleForm = ({
onSubmit,
formValues,
}: ExampleFormProps): JSX.Element => {
const methods = useForm<ExampleFormValues>();
return (
<FormProvider {...methods}>
<ExampleFormContainer onSubmit={onSubmit} formValues={formValues} />
</FormProvider>
);
};
<FormProvider>
の子コンポーネントたちには methods が渡されるのでuseFormContext
で useForm で使うメソッドが使える
バリデーション
ビルトインのバリデーションがいくつかあるが今回は Yup を使う前提なので割愛
ドキュメントがわかりやすいので見ればイメージは掴めるはず
Yup を使う場合、yupResolver
を useForm の引数で resolver として渡してやれば良い
import { yupResolver } from "@hookform/resolvers/yup";
// 前に記載したExampleFormと値が変わってることには触れないで
export const validationSchema = Yup.object().shape({
name: Yup.string().required(),
age: Yup.number().required(),
fruit: Yup.mixed().oneOf(["apple", "banana", "lemon"]).required(),
category: Yup.mixed().oneOf(["dog", "cat", "rabbit"]).required(),
});
export const ExampleForm = ({
onSubmit,
formValues,
}: ExampleFormProps): JSX.Element => {
const methods = useForm<ExampleFormValues>({
resolver: yupResolver(validationSchema),
});
return (
<FormProvider {...methods}>
<ExampleFormContainer onSubmit={onSubmit} formValues={formValues} />
</FormProvider>
);
};
useFieldArray
Formik の<FieldArray>
の hook 版
これも丁寧にドキュメントに実装方法が記載されているので、ドキュメントを読めば実装イメージが掴めるはず
type Friend = {
name: string;
email: string;
};
export type HookFormValues = {
friends: Friend[];
};
export const HookFormPresenter = ({
onSubmit,
}: HookFormPresenterProps): JSX.Element => {
const {
register,
handleSubmit,
errors,
control,
} = useFormContext<HookFormValues>();
const { fields, append, remove } = useFieldArray<Friend>({
control,
name: "friends",
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
{fields.map((f, i) => (
<div key={f.id}>
<input name={`friends[${i}].name`} ref={register} />
<input name={`friends[${i}].email`} ref={register} />
</div>
))}
</div>
<button
onClick={() => {
append({ name: "", email: "" });
}}
>
友達追加
</button>
<button
onClick={() => {
remove();
}}
>
全削除
</button>
<button type="submit">Submit</button>
</form>
);
};
append
やremove
の他にも配列で扱うために便利そうなメソッドが一通り用意されてた
Discussion