React Hook From + yupを使う
Table of Contents
tldr
最近 フロントエンドで Nx + React + Redux のフォームに React Hook Form + yup を導入したので、使い方や詰まった点などを説明します。
React Hook Form とは
React Hook Form は React のフォームライブラリとしては最も有名な Formik と比べて、軽くて速いライブラリとして登場しました。
React Hook Formが速い理由はUncontrolled Componentを使用しているからです。そのため、ユーザーが入力した際のレンダリング回数が少なくてすみます。
(Controled componentとUncontrolled componentの違いはこの記事がわかりやすくまとまっています。)
また、依存パッケージがなく、その点でもFormikに比べ勝っている点です。
yup と組み合わせる
React Hook Form にもバリデーション機能はあり、私も最初はビルトインでいけるかなーと希望を持っていたのですが、複雑なフォームバリデーションをやるとなるとやはり機能不足を感じました。 ということで、バリデーションライブラリである yup を組み合わせることにしました。
やり方は簡単でyupResolverにスキーマを渡して、返却値をresolver
にセットするだけです。
import { yupResolver } from '@hookform/resolvers/yup';
...
const methods = useForm({
resolver: yupResolver(schema),
defaultValues: defaultValues,
mode: 'onChange',
});
React Hook Formを子コンポーネントで使用する
フォームが複雑でReact Hook Formを子コンポーネントで渡したいときは FormProvider
を使います。
...
const methods = useForm({
resolver: yupResolver(schema),
defaultValues: defaultValues,
mode: 'onChange',
});
return (
<FormProvider {...methods}>
<HogeForm
onSubmit={handleSubmit(handleUpdate)}
/>
</FormProvider>)
...
そして、子コンポーネントで useFormContext()
を使います。
const methods = useFormContext();
公式でこのようなAPIが用意されているので、正統なやりかただとは思います。
ただ、useFormContext()
から返ってくる method
がどこから来たのかが親のコンポーネントを見に行かなければならず、エディタでジャンプも使えないのであまり気に入っていません。が、propsで渡していくのもちょっとなー。
Material UIやReact-SelectなどのControlled componentと一緒に使う
Controlled componentのライブラリと一緒に使うには Controller
コンポーネントを使用します。
<Controller
name={name}
control={control}
render={({ field }) => (
<ReactSelect
onChange={field.onChange}
options={options}
defaultValue={rule.opsType}
/>
)}
/>
yupスキーマの再利用
あるオブジェクトの作成画面と更新画面を別々に作成する場合、バリデーションを再利用したいときがあるはずです。 そのような場合は各フィールドのスキーマを独立で変数に入れておき、別々のフォームスキーマから使うのがおすすめです。 場合によっては少し見にくくはなりますが、同じフィールドに異なるバリデーションを設定してしまうミスを防げます。
const nameSchema = yup.string().max(20);
// 作成フォーム
export const addFormSchema = yup.object().shape({
name: nameSchema,
});
// 更新フォーム
export const updateFormSchema = yup.object().shape({
name: nameSchema,
});
yupで親の親のフィールドを参照したい
ここは結構ハマりました。
たとえば、以下のようなスキーマがあり、
level2
のtest関数からtarget1とtarget2を参照してかったとします。
yup.object().shape({
target1: yup.string(),
level1: yup.object().shape({
target2: yup.string(),
level2: yup
.string()
.test(
'custom',
'No good',
(value, context) => {
// target1, target2を参照したい
// context.parent.target2はできる
}
),
}),
})
この場合、context.parent.target2
で値は参照できますが、target1は参照することはできません。
こういったときは test
関数に function(value){hoge}
を渡し、そのなかで const {from} = this;
と書くことで親の値を参照することが可能です。
yup.object().shape({
target1: yup.string(),
level1: yup.object().shape({
target2: yup.string(),
level2: yup
.string()
.test(
'custom',
'No good',
.test('name', 'errorMessage', function(value) {
const { from } = this;
// from[1].value.target2
// from[2].value.target1
})
),
}),
})
参考URL: https://github.com/jquense/yup/issues/735#issuecomment-873828710
エラー文言のintl対応
yup は多言語でエラー文言を表示できます。
Locale を設定するには、デフォルトの各バリデーションのエラー文言を定義して、setLocale()
に引数として渡してあげるだけです。
export const localJp = {
mixed: {
default: '入力エラーです',
required: '必須入力項目です',
oneOf: ({ values }) => `次の値のいずれかでなければなりません: ${values}`,
notOneOf: ({ values }) => `次の値のいずれかであってはなりません: ${values}`,
isNumber: '形式が違います',
},
...
};
yup.setLocale(localJp);
まとめ
- 複雑なフォームはライブラリを使うことである程度わかりやすく記述できます。
- React Hook FormはFormikと並んで、有力な候補です。
- React Hook FormはFormikと比べて速くて軽量です。
- 簡単なバリデーションであればビルトインで問題ないと思います。
- 複雑なバリデーションはyupと組み合わせて使うことでシンプルで再利用可能なスキーマを記述できます。