ReactNative+TypeScript+Formikでフォーム画面を作る
はじめに
ReactNative上でログイン機能を実装するために、フォームを実装する必要があったのですが、Formikが想像以上に良かったので書き残します。 説明雑なので、コードを参考にする程度で読んでいただければと思います。
環境は、
- ReactNative 0.55.3
- Formik 1.0.0-alpha.6
- Yup 0.24.1
- TypeScript 2.7.2
Formikとは
Formikとは、ReactやReactNativeで使用できるフォーム管理のためのライブラリで、以下のような特徴があります
- バリデーションが簡単にできる
- 送信ボタンの制御も簡単にできる
- フォームのStateを簡単に受け取れる
- State管理のライブラリに依存しない
使い方
普通にComponentとして書く場合と、HOCとして自分で定義したFormのViewを包んでやる方法があります。
今回は、HOCを利用する方法で書きました。こちらのほうがViewとフォームの制御に関しての記述が分かれてわかりやすいと思いました。
実装は、Using Formik with TypeScriptを参考にしました。
以下が実装したコード全体です。
import { FormikProps, withFormik } from "formik" import * as React from "react" import { StyleSheet, View, ViewStyle } from "react-native" import { Button, FormInput, FormLabel, FormValidationMessage, } from "react-native-elements" import Yup from "yup" import { login } from "../actions/auth" interface FormValues { email: string password: string url: string } interface OtherProps { message: string } const InnerForm = (props: OtherProps & FormikProps<FormValues>) => ( <View> <FormLabel>SiteURL</FormLabel> <FormInput onChangeText={text => props.setFieldValue("url", text)} value={props.values.url} /> <SwitchFormValidationMessage text={props.errors.url} submitCount={props.submitCount} /> <FormLabel>Email</FormLabel> <FormInput onChangeText={text => props.setFieldValue("email", text)} value={props.values.email} /> <SwitchFormValidationMessage text={props.errors.email} submitCount={props.submitCount} /> <FormLabel>Password</FormLabel> <FormInput onChangeText={text => props.setFieldValue("password", text)} value={props.values.password} secureTextEntry={true} /> <SwitchFormValidationMessage text={props.errors.password} submitCount={props.submitCount} /> <Button onPress={props.submitForm} title={props.message} buttonStyle={styles.loginButton} /> </View> ) interface SwitchFormProps { text: any submitCount: number } const SwitchFormValidationMessage = (props: SwitchFormProps) => { if (props.submitCount > 0) { return <FormValidationMessage>{props.text}</FormValidationMessage> } return <FormValidationMessage /> } interface LoginFromProps { message: string } const LoginForm = withFormik<LoginFromProps, FormValues>({ mapPropsToValues: () => { return { email: "", password: "", url: "", } }, validationSchema: Yup.object().shape({ email: Yup.string() .email("Please input Email Address") .required("Email Address is required!"), password: Yup.string() .max(36, "Please enter password in 36 characters or less") .required("Password is required!"), url: Yup.string() .url("Please input your site url") .required("URL is required!"), }), handleSubmit: values => { login(values.email, values.password, values.url) .then(token => console.log(token)) .catch(error => console.log(error)) }, })(InnerForm) interface Style { loginButton: ViewStyle } const styles = StyleSheet.create<Style>({ loginButton: { backgroundColor: "#0086F6", borderRadius: 2, marginTop: 20, }, }) export default LoginForm
コードの説明
まず、withFormikで包むためのフォームのViewを定義したコンポーネントを作ります。今回で言えば、以下の部分です。
ここでは、Form画面をどのように構成するかを記述します。 今回はReactNativeElementsというUIコンポーネントを利用して、画面を構成しています。
const InnerForm = (props: OtherProps & FormikProps<FormValues>) => ( <View> <FormLabel>SiteURL</FormLabel> <FormInput onChangeText={text => props.setFieldValue("url", text)} value={props.values.url} /> <SwitchFormValidationMessage text={props.errors.url} submitCount={props.submitCount} /> <FormLabel>Email</FormLabel> <FormInput onChangeText={text => props.setFieldValue("email", text)} value={props.values.email} /> <SwitchFormValidationMessage text={props.errors.email} submitCount={props.submitCount} /> <FormLabel>Password</FormLabel> <FormInput onChangeText={text => props.setFieldValue("password", text)} value={props.values.password} secureTextEntry={true} /> <SwitchFormValidationMessage text={props.errors.password} submitCount={props.submitCount} /> <Button onPress={props.submitForm} title={props.message} buttonStyle={styles.loginButton} /> </View> )
また、TypeScriptを使用しているので予めFormValuesというinterfaceを定義して、その中にFormで入力する要素を定義しています。
その他、渡したいデータに関してはOtherPropsに定義します。
interface FormValues { email: string password: string url: string } interface OtherProps { message: string }
ここで、一点注意が必要なのが、送信ボタンのonPressに渡しているメソッドについてです。
Formik公式のドキュメントを見ると、props.handleSubmitを渡していますが、ReactNativeの場合props.handleSubmitではonPressに渡すのに型が合わないので、今回はprops.submitFormを渡します。
こちらに関しては、公式でIssueが立てられており、そこの回答を見るとひとまずsubmitFormで対応してくれとのことでした。
次に、ロジックの部分に関して説明します。 こちらは、withFormikで自分で定義したFormコンポーネントを包み、そのFormコンポーネントに対して振る舞いを追加する感じで実装をします。 具体的なコードは以下の部分です。
const LoginForm = withFormik<LoginFromProps, FormValues>({ mapPropsToValues: () => { return { email: "", password: "", url: "", } }, validationSchema: Yup.object().shape({ email: Yup.string() .email("Please input Email Address") .required("Email Address is required!"), password: Yup.string() .max(36, "Please enter password in 36 characters or less") .required("Password is required!"), url: Yup.string() .url("Please input your site url") .required("URL is required!"), }), handleSubmit: values => { login(values.email, values.password, values.url) .then(token => console.log(token)) .catch(error => console.log(error)) }, })(InnerForm)
mapPropsToValuesで、各フォームの値の初期値を設定しています。
validationSchemaでは、Yupというバリデーションライブラリを用いて、バリデーションを行っています。
FormikとYupの相性は非常によく、公式でもYupを使うことが推奨されており、コードを見ていただくとわかるように非常に直感的にバリデーションが書けます。
validationSchemeでバリデーションを定義しておくことで、バリデーションエラーの場合はsubmitが発火しないようになります。
最後に、handleSubmitに送信ボタンが押されたときの挙動を書きます。
まとめ
Formikは公式ドキュメントもしっかりしており、便利でしたが意外と記事が少なかったので書いてみました。 TypeScriptでの実装例として参考にしていただければと思います。