LoginSignup
39
17

More than 5 years have passed since last update.

Formik で使う yup のスキーマパターン

Last updated at Posted at 2019-03-30

はじめに

以前に別の記事に書いたのですが、うちの職場ではフォームの管理は Formik、バリデーションチェックは yup を使っています。

いくつかのやりたいことは無理やり実現してる感がありますが、使っていて便利だなーと思うのでいくつかのパターンをシェアします。
GitHub の issue も漁りましたが会心の解決策に出会えてないものもあり…、もっと良いパターンがあったらぜひとも知りたい。(切望

Formik の利用パターン

私の環境で使用してる Formik の使い方は以下の通りです。

const FormView: React.SFC<Props> = ({ ...props }) => {
  const schema = genSchema(something);

  return 
    <Formik
      initialValues={initialValues}
      validationSchema={schema.validations}
      onSubmit={(values, { setSubmitting }) => {
        // something...
        setSubmitting(false);
      }}
      render={(props: FormikProps<Values>) => (
        <>
          <CustomField item={schema.somethingSchema} />

          <Button
            title="Submit"
            onPress={props.handleSubmit}
            disabled={props.isSubmitting}
          />
        </>
      )}
    />
  );
}

Formik には validationvalidationSchema の使い方があると思います。
ここでは、 yup で定義したスキーマを直接渡す形を取っています。
(そのせいで(?)、詰まった部分もあります。。。)

ポイントは、 genSchema という処理を作って、そこでスキーマを取得していることです。(詳細は後述)

yup のスキーマ

getSchema

getSchema は以下のような形で定義します。
引数 arg を渡しているのは、このほうがダイナミックなスキーマの利用ができるからです。

export const genSchema = (arg: any) => {
  const somethingSchema = {
    fieldValidations: Yup.string().required(),
    // something...
  };

  const validations = Yup.object().shape({
    something: somethingSchema.fieldValidations,
    // others...
  });

  return {
    somethingSchema,
    // others...
    validations,
  };
}

スキーマのパターン

いくつかの(よく使う)スキーマパターンを紹介します。
なお、 .email.required みたいなベーシックなものは省略します。

公式ドキュメントの内容を自分で試してみて、コメント(感想?)を載せているようなものですので、どうぞ温かい目で見守ってください。

mixed.oneOf(...)

ラジオボタンや yes / no のチェックなど、複数の中から一つを選択させるフォームで活用します。
選択肢のリストを変数として渡せば、何かのはずみで別の値が渡されたとしてもエラーとして弾いてくれます。

yup.ref と組み合わせて、パスワード(確認用)のフォームを作ることも簡単です。
.requiredを忘れると、選択しなくても通るので注意してください。

const radioButtonSchema = {
  fieldValidations: Yup.string().oneOf(optionsList).required(),
  // ...
}

const passwordSchema = {
  fieldValidations: Yup.string().required(),
  // ...
}
const confirmPasswordSchema = {
  fieldValidations: Yup.string().oneOf([Yup.ref('password')]).required(),
  // ...
};

mixed.when

公式に以下のような記載があります。

Adjust the schema based on a sibling or sibling children fields.

別のスキーマ(おそらく同じ Yup.object().shape({ ... }) で定義されたもの)を参照することができます。
自分自身の値を参照しようとすると、循環参照エラー(?)になりました。

活用シーンは、 yes / no ボタンを用意し、 no を選択したケースだと必須入力の詳細内容フォームが現れる、といったケースです。

const detailSchema = {
  fieldValidations: Yup.string().when('yesNoCheck',
    (value, schema) => value === 'no'
    ? schema.required()
    : schema,
  // ...
}

このパターンでは、他の yesNoCheck スキーマの値を参照し、No の場合は必須入力に、Yes の場合はパスさせる、という処理をしています。
detailSchema の初期値が null になる場合は、 schema.nullable() にしてあげる必要があります。

mixed.test

.test は第三引数で渡すテスト関数の返り値で検証します。

All tests must provide a name, an error message and a validation function that must return true or false or a ValidationError. To make a test async return a promise that resolves true or false or a ValidationError.

テスト関数は true / false / ValidationError を返す必要があります。
また、async も使えるみたいです。

.test では、自分自身の値と、thisが使えるのが特徴です。
前回の記事にも書きましたが、 arrow 関数を使ってしまうと this が束縛されてしまうので、 function(value) { ... } の形で定義する必要があります。

Note that to use the this context the test function must be a function expression (function test(value) {}), not an arrow function, since arrow functions have lexical context.

動的なハンドリング

この記事を書こうと決意した理由がこの動的なハンドリングです。
私が求めていた動的なハンドリングが発生するケースとは、フォームの値以外の条件(たとえばバックエンドから返ってきたステータスなど)によって、フォームの表示 / 非表示が変化する場合です。

ここでは、 genSchema の引数を利用します。

export const genSchema = (arg: any) => {
  const somethingSchema = {
    fieldValidations: arg === 'something'
    ? Yup.string().required()
    : Yup.string(),
    // something...
  };
  // ...
}

この方法を使うことで、フォーム以外の値でもフォームのバリデーションをコントロールすることができます。

ちょこっとコラム(その1)

動的なハンドリングについて、当初 .whencontext を利用できないかと画策しました。参考になりそうな issue も見つけました。

.whencontext で渡したパラメータを $something で参照できる機能を持ちます。
この質問者のケースでは、 Formcontext props を渡すことで実現できたとあります。

ですが私の場合、(validationSchema を使っているからか?) JSX に context を渡してもうまくハンドルできませんでした。

schema.isValidschema.validation を直接コールするとバリデーションチェックはできているものの、Formik を介してのチェックが走りません。
そのため、やむ終えず上記の手法にチェンジしました。

ちょこっとコラム(その2)

Formik には <Form /><Field /> が用意されています。
<Form /><form><Field /><input> のラッパーとして定義されているので、React Native でそのままは利用できません。

そのため CustomField Component を用意することで、フォームの入力フィールドを作りました。
TextInput 以外にも、ラジオボタンなども定義可能です。

const CustomField: React.SFC<Props> = ({ item }) => (
  <Field name={item.fieldName}>
    {({ form, field }: FieldProps) => {
      const hasError = !!form.errors[field.name] && !!form.touched[field.name];

      return (
        <View style={{ ... }}>
          <TextInput
            style={{ ... }}
            value={field.value}
            placeholder={item.defaultLabel}
            onChangeText={(v) => form.setFieldValue(field.name, v)}
            onBlur={() => form.setFieldTouched(field.name)}
          />
          <View style={{ ... }}>
            {hasError && (
              <Text style={{ ... }}>
                {form.errors[field.name]}
              </Text>
            )}
          </View>
        </View>
      );
    }}
  </Field>
);

おわりに

.whencontext はハンドリングしようとしてドツボにはまりました。(結局、解決策を見つけられなかった)

同僚に相談したら、
「フィールドの表示制御も以ってるんでしょ?それなら genSchema に引数渡しちゃいなよ。」
って言われて解決しました。
視野を狭めず、いろんな可能性を考えることの重要性を再認識させられました。

同じ悩みを持つ誰かの助けになれば幸いです。

39
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
39
17