げぇむぷろぐらみんぐ

日々の生活で得た知識、経験を書きます

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での実装例として参考にしていただければと思います。