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