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と組み合わせて使うことでシンプルで再利用可能なスキーマを記述できます。