はじめに
以前に別の記事に書いたのですが、うちの職場ではフォームの管理は 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 には validation
と validationSchema
の使い方があると思います。
ここでは、 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)
動的なハンドリングについて、当初 .when
の context
を利用できないかと画策しました。参考になりそうな issue も見つけました。
.when
は context
で渡したパラメータを $something
で参照できる機能を持ちます。
この質問者のケースでは、 Form
に context
props を渡すことで実現できたとあります。
ですが私の場合、(validationSchema
を使っているからか?) JSX に context
を渡してもうまくハンドルできませんでした。
schema.isValid
や schema.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>
);
おわりに
.when
を context
はハンドリングしようとしてドツボにはまりました。(結局、解決策を見つけられなかった)
同僚に相談したら、
「フィールドの表示制御も以ってるんでしょ?それなら genSchema
に引数渡しちゃいなよ。」
って言われて解決しました。
視野を狭めず、いろんな可能性を考えることの重要性を再認識させられました。
同じ悩みを持つ誰かの助けになれば幸いです。