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

# はじめに

以前に[別の記事](https://qiita.com/dhythm/items/7404bf586e0a4493ccdd)に書いたのですが、うちの職場ではフォームの管理は [Formik](https://jaredpalmer.com/formik/)、バリデーションチェックは [yup](https://github.com/jquense/yup) を使っています。

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

# Formik の利用パターン
私の環境で使用してる Formik の使い方は以下の通りです。

```typescript
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 には `validation` と `validationSchema` の使い方があると思います。
ここでは、 yup で定義したスキーマを直接渡す形を取っています。
（そのせいで(?)、詰まった部分もあります。。。）

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

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

```typescript
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`を忘れると、選択しなくても通るので注意してください。

```typescript
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 を選択したケースだと必須入力の詳細内容フォームが現れる、といったケースです。

```typescript
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` の引数を利用します。

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

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

#### ちょこっとコラム（その１）
動的なハンドリングについて、当初 `.when` の `context` を利用できないかと画策しました。参考になりそうな [issue](https://github.com/jquense/yup/issues/74) も見つけました。

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

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

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

#### ちょこっとコラム（その２）
`Formik` には `<Form />` や `<Field />` が用意されています。
`<Form />` は `<form>`、 `<Field />` は `<input>` のラッパーとして定義されているので、React Native でそのままは利用できません。

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

```typescript
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>
);
```

# おわりに
`.when` を `context` はハンドリングしようとしてドツボにはまりました。（結局、解決策を見つけられなかった）

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

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