formik step by step (React Native)

## 注意!!

本サンプルにはreact-native-elementsを利用していますが、旧執筆時はver0.xでした。が、最新は1.2.xであり、書き方が変わっています（Formが廃止されInputのみに）。最新環境でも動くコードを最後に追記してあります。

## 本文

React (Native)でのForm操作は悩ましいところです。
Redux-Formが有名みたいですが、Redux必須というのがうーんって感じです。

そこで[formik](https://github.com/jaredpalmer/formik)を試してみたいと思います。

formikを利用することで値のハンドリング、バリデーション、Submitボタンの操作などいろいろできます。

書き方としては\<Formik>タグを利用する方法とwithFormik()を利用する方法があるようですが、とりあえず\<Formik>タグを利用してみます。

また、バリデーションはカスタムで書くか、[Yup](https://github.com/jquense/yup)を利用するか（他のライブラリとも連携可能なようです）ですが、ここではYupを利用してみます。

React系のコーディングは完成形だけみても？？？って感じになるので冗長ですがSTEP BY STEPで進めていきます。

## ゴール

最終的には下記のようなバリデーション付きのフォームを作ります。

![スクリーンショット 2018-11-10 7.52.09.png](https://qiita-image-store.s3.amazonaws.com/0/55188/aafb2565-9646-1e83-21a4-bed95eb5d116.png)


## モジュールのインストール

必要なモジュールをインストールします。
UIのパーツとしてはreact-native-elementsを利用します。

```bash
npm install --save formik yup react-native-elements
npm install
```

## STEP1：最低限の雛形を記述する

まず、項目としてNameを表示し、Submitでstateを表示する雛形を作ります。

```App.js
import React from 'react';
import { StyleSheet, Text, View, Alert } from 'react-native';
import { Formik } from 'formik';
import { Button, FormLabel, FormInput, FormValidationMessage } from 'react-native-elements';

export default class App extends React.Component {
  render() {
    return (
      <View style={{flex:1, alignItems:'center', justifyContent:'center'}}>
        <Formik
          initialValues={{ name: ''}}
          onSubmit={values => Alert.alert(JSON.stringify(values))}
        >
          {
            ({handleChange, handleSubmit, values}) => (
              <View style={{width:'80%'}}>

                <FormLabel>Name</FormLabel>
                <FormInput
                  onChangeText={handleChange('name')}
                  value={values.name}
                />
                <FormValidationMessage>error</FormValidationMessage>

                <Button
                  onPress={handleSubmit}
                  title='Submit'
                  buttonStyle={{marginTop:30}}
                />
              </View>
            )
          }
        </Formik>
      </View>
    );
  }
}
```

onChangeText={handleChange('name')とすることで、nameに変更を反映してくれます。
また、value={values.name}として、値を表示します。

## STEP2：項目を追加してみる

続いてEmailを追加してみます。特に難しいことはありません。

```diff:App.js
import React from 'react';
import { StyleSheet, Text, View, Alert } from 'react-native';
import { Formik } from 'formik';
import { Button, FormLabel, FormInput, FormValidationMessage } from 'react-native-elements';

export default class App extends React.Component {
  render() {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <Formik
+          initialValues={{ name: '', email: '' }}
          onSubmit={values => Alert.alert(JSON.stringify(values))}
        >
          {
            ({ handleChange, handleSubmit, values }) => (
              <View style={{ width: '80%' }}>

                <FormLabel>Name</FormLabel>
                <FormInput
                  onChangeText={handleChange('name')}
                  value={values.name}
                />
                <FormValidationMessage>error</FormValidationMessage>

+                <FormLabel>Email</FormLabel>
+                <FormInput
+                  onChangeText={handleChange('email')}
+                  value={values.email}
+                />
+                <FormValidationMessage>error</FormValidationMessage>

                <Button
                  onPress={handleSubmit}
                  title='Submit'
                  buttonStyle={{ marginTop: 30 }}
                />
              </View>
            )
          }
        </Formik>
      </View>
    );
  }
}
```

## STEP３：Validationの実装１：最低限

ではValidationを実装します。
下記のように記述することで、書くフォームのエラーを制御できるようになります。

```diff:App.js
import React from 'react';
import { StyleSheet, Text, View, Alert } from 'react-native';
import { Formik } from 'formik';
import { Button, FormLabel, FormInput, FormValidationMessage } from 'react-native-elements';
import * as Yup from 'yup';

export default class App extends React.Component {
  render() {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <Formik
          initialValues={{ name: '', email: '' }}
          onSubmit={values => Alert.alert(JSON.stringify(values))}
+          validationSchema={Yup.object().shape({
+            name: Yup.string().required(),
+            email: Yup.string().email().required(),
+          })}
        >
          {
+            ({ handleChange, handleSubmit, values, errors }) => (
              <View style={{ width: '80%' }}>

                <FormLabel>Name</FormLabel>
                <FormInput
                  onChangeText={handleChange('name')}
                  value={values.name}
                />
+                <FormValidationMessage>{errors.name}</FormValidationMessage>

                <FormLabel>Email</FormLabel>
                <FormInput
                  onChangeText={handleChange('email')}
                  value={values.email}
                />
+                <FormValidationMessage>{errors.email}</FormValidationMessage>

                <Button
                  onPress={handleSubmit}
                  title='Submit'
                  buttonStyle={{ marginTop: 30 }}
                />
              </View>
            )
          }
        </Formik>
      </View>
    );
  }
}
```

下記のようにすることでエラーメッセージをカスタマイズできます。

```js
name: Yup
        .string()
        .required('名前の入力は必須です。'),
```

## STEP4：Validationの実装２：対象の項目を操作するまでエラーを出さない

先述の状態では、どこか１つの項目をいじると、全ての項目についてバリデーションが実行され、エラーが表示されてしまいます。また編集（touch）していない項目についてはエラーを表示しないようにします。

{ (条件) && 表示内容}とすることで、条件により表示の切り替えが可能です。これを利用して、書く項目が編集されたかどうか（touched）で表示を制御します。

```diff
import React from 'react';
import { StyleSheet, Text, View, Alert } from 'react-native';
import { Formik } from 'formik';
import { Button, FormLabel, FormInput, FormValidationMessage } from 'react-native-elements';
import * as Yup from 'yup';

export default class App extends React.Component {
  render() {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <Formik
          initialValues={{ name: '', email: '' }}
          onSubmit={values => Alert.alert(JSON.stringify(values))}
          validationSchema={Yup.object().shape({
            name: Yup.string().required(),
            email: Yup.string().email().required(),
          })}
        >
          {
+            ({ handleChange, handleSubmit, values, errors, touched }) => (
              <View style={{ width: '80%' }}>

                <FormLabel>Name</FormLabel>
                <FormInput
                  onChangeText={handleChange('name')}
                  value={values.name}
                />
+                {touched.name && <FormValidationMessage>{errors.name}</FormValidationMessage>}

                <FormLabel>Email</FormLabel>
                <FormInput
                  onChangeText={handleChange('email')}
                  value={values.email}
                />
+                {touched.email && <FormValidationMessage>{errors.email}</FormValidationMessage>}

                <Button
                  onPress={handleSubmit}
                  title='Submit'
                  buttonStyle={{ marginTop: 30 }}
                />
              </View>
            )
          }
        </Formik>
      </View>
    );
  }
}
```

## STEP5：Validationの実装３：項目からの移動時（onBlur）にバリデーションする

Submitボタンを押すまでエラーがわからないのは不親切です。
編集項目から移動した際(onBlur)にエラーがあれば表示するようにしてみます。

```diff
import React from 'react';
import { StyleSheet, Text, View, Alert } from 'react-native';
import { Formik } from 'formik';
import { Button, FormLabel, FormInput, FormValidationMessage } from 'react-native-elements';
import * as Yup from 'yup';

export default class App extends React.Component {
  render() {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <Formik
          initialValues={{ name: '', email: '' }}
          onSubmit={values => Alert.alert(JSON.stringify(values))}
          validationSchema={Yup.object().shape({
            name: Yup.string().required(),
            email: Yup.string().email().required(),
          })}
        >
          {
+            ({ handleChange, handleSubmit, values, errors, touched, handleBlur }) => (
              <View style={{ width: '80%' }}>

                <FormLabel>Name</FormLabel>
                <FormInput
                  onChangeText={handleChange('name')}
+                  onBlur={handleBlur('name')}
                  value={values.name}
                />
                {touched.name && <FormValidationMessage>{errors.name}</FormValidationMessage>}

                <FormLabel>Email</FormLabel>
                <FormInput
                  onChangeText={handleChange('email')}
+                  onBlur={handleBlur('email')}
                  value={values.email}
                />
                {touched.email && <FormValidationMessage>{errors.email}</FormValidationMessage>}

                <Button
                  onPress={handleSubmit}
                  title='Submit'
                  buttonStyle={{ marginTop: 30 }}
                />
              </View>
            )
          }
        </Formik>
      </View>
    );
  }
}
```

## STEP６：Validationの実装４：全ての項目がOKになるまでSubmitボタンをアクティブにしない

場合により、全ての項目のチェックがOKになるまでSubmitボタンをアクティブにしたくない場合もあります。
それを試します。isValidを利用すればバリデーションがOKかNGかが取れるようです。

まあ、実際に適用するかどうかはケースバイケースかなと思います。

```diff
import React from 'react';
import { StyleSheet, Text, View, Alert } from 'react-native';
import { Formik } from 'formik';
import { Button, FormLabel, FormInput, FormValidationMessage } from 'react-native-elements';
import * as Yup from 'yup';

export default class App extends React.Component {
  render() {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <Formik
          initialValues={{ name: '', email: '' }}
          onSubmit={values => Alert.alert(JSON.stringify(values))}
          validationSchema={Yup.object().shape({
            name: Yup.string().required(),
            email: Yup.string().email().required(),
          })}
        >
          {
+            ({ handleChange, handleSubmit, values, errors, touched, handleBlur, isValid }) => (
              <View style={{ width: '80%' }}>

                <FormLabel>Name</FormLabel>
                <FormInput
                  onChangeText={handleChange('name')}
                  onBlur={handleBlur('name')}
                  value={values.name}
                />
                {touched.name && <FormValidationMessage>{errors.name}</FormValidationMessage>}

                <FormLabel>Email</FormLabel>
                <FormInput
                  onChangeText={handleChange('email')}
                  onBlur={handleBlur('email')}
                  value={values.email}
                />
                {touched.email && <FormValidationMessage>{errors.email}</FormValidationMessage>}

                <Button
                  onPress={handleSubmit}
                  title='Submit'
                  buttonStyle={{ marginTop: 30 }}
+                  disabled={!isValid}
                />
              </View>
            )
          }
        </Formik>
      </View>
    );
  }
}
```

## STEP7：Validationの実装５：２度押し禁止

状況によりボタンの2通しを禁止したい場合もあります。それを実装してみます。isSubmittingでSubmitされたかどうかが取得できるようです。

```diff
import React from 'react';
import { StyleSheet, Text, View, Alert } from 'react-native';
import { Formik } from 'formik';
import { Button, FormLabel, FormInput, FormValidationMessage } from 'react-native-elements';
import * as Yup from 'yup';

export default class App extends React.Component {
  render() {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <Formik
          initialValues={{ name: '', email: '' }}
          onSubmit={values => Alert.alert(JSON.stringify(values))}
          validationSchema={Yup.object().shape({
            name: Yup.string().required(),
            email: Yup.string().email().required(),
          })}
        >
          {
+            ({ handleChange, handleSubmit, values, errors, touched, handleBlur, isValid, isSubmitting }) => (
              <View style={{ width: '80%' }}>

                <FormLabel>Name</FormLabel>
                <FormInput
                  onChangeText={handleChange('name')}
                  onBlur={handleBlur('name')}
                  value={values.name}
                />
                {touched.name && <FormValidationMessage>{errors.name}</FormValidationMessage>}

                <FormLabel>Email</FormLabel>
                <FormInput
                  onChangeText={handleChange('email')}
                  onBlur={handleBlur('email')}
                  value={values.email}
                />
                {touched.email && <FormValidationMessage>{errors.email}</FormValidationMessage>}

                <Button
                  onPress={handleSubmit}
                  title='Submit'
                  buttonStyle={{ marginTop: 30 }}
+                  disabled={!isValid || isSubmitting}
                />
              </View>
            )
          }
        </Formik>
      </View>
    );
  }
}
```

## STEP８：Validationの実装６：Checkboxの追加

Checkboxの制御ができるか試してみます。
下記のようにすることで値のコントロール、バリデーションができましたが、ベストな記述かどうかわ微妙です。

```diff
import React from 'react';
import { StyleSheet, Text, View, Alert } from 'react-native';
import { Formik } from 'formik';
+import { Button, FormLabel, FormInput, FormValidationMessage, CheckBox } from 'react-native-elements';
import * as Yup from 'yup';

export default class App extends React.Component {
  render() {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <Formik
+          initialValues={{ name: '', email: '', check: false}}
          onSubmit={values => Alert.alert(JSON.stringify(values))}
          validationSchema={Yup.object().shape({
            name: Yup.string().required(),
            email: Yup.string().email().required(),
+            check: Yup.boolean().oneOf([true],'同意にチェックして下さい。')
          })}
        >
          {
+            ({ handleChange, handleSubmit, values, errors, touched, handleBlur, isValid, isSubmitting, setFieldValue }) => (
              <View style={{ width: '80%' }}>

                <FormLabel>Name</FormLabel>
                <FormInput
                  onChangeText={handleChange('name')}
                  onBlur={handleBlur('name')}
                  value={values.name}
                />
                {touched.name && <FormValidationMessage>{errors.name}</FormValidationMessage>}

                <FormLabel>Email</FormLabel>
                <FormInput
                  onChangeText={handleChange('email')}
                  onBlur={handleBlur('email')}
                  value={values.email}
                />
                {touched.email && <FormValidationMessage>{errors.email}</FormValidationMessage>}

+                <CheckBox
+                  center
+                  title='同意'
+                  checked={values.check}
+                  onPress={() => setFieldValue('check',!values.check)}
+                />
+                {touched.check && <FormValidationMessage>{errors.check}</FormValidationMessage>}

                <Button
                  onPress={handleSubmit}
                  title='Submit'
                  buttonStyle={{ marginTop: 30 }}
                  disabled={!isValid || isSubmitting}
                />
              </View>
            )
          }
        </Formik>
      </View>
    );
  }
}
```

## おまけ：loading

最後に、formikとは関係ありませんが、Buttonにloadingを設定することでスピナーを表示できるようです。
外部API連携等の際は利用するといいかもしれません。

```diff
import React from 'react';
import { StyleSheet, Text, View, Alert } from 'react-native';
import { Formik } from 'formik';
import { Button, FormLabel, FormInput, FormValidationMessage, CheckBox } from 'react-native-elements';
import * as Yup from 'yup';

export default class App extends React.Component {
  render() {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <Formik
          initialValues={{ name: '', email: '', check: false}}
          onSubmit={values => Alert.alert(JSON.stringify(values))}
          validationSchema={Yup.object().shape({
            name: Yup.string().required(),
            email: Yup.string().email().required(),
            check: Yup.boolean().oneOf([true],'同意にチェックして下さい。')
          })}
        >
          {
            ({ handleChange, handleSubmit, values, errors, touched, handleBlur, isValid, isSubmitting, setFieldValue }) => (
              <View style={{ width: '80%' }}>

                <FormLabel>Name</FormLabel>
                <FormInput
                  onChangeText={handleChange('name')}
                  onBlur={handleBlur('name')}
                  value={values.name}
                />
                {touched.name && <FormValidationMessage>{errors.name}</FormValidationMessage>}

                <FormLabel>Email</FormLabel>
                <FormInput
                  onChangeText={handleChange('email')}
                  onBlur={handleBlur('email')}
                  value={values.email}
                />
                {touched.email && <FormValidationMessage>{errors.email}</FormValidationMessage>}

                <CheckBox
                  center
                  title='同意'
                  checked={values.check}
                  onPress={() => setFieldValue('check',!values.check)}
                />
                {touched.check && <FormValidationMessage>{errors.check}</FormValidationMessage>}

                <Button
                  onPress={handleSubmit}
                  title='Submit'
                  buttonStyle={{ marginTop: 30 }}
                  disabled={!isValid || isSubmitting}
+                  loading={true}
                />
              </View>
            )
          }
        </Formik>
      </View>
    );
  }
}
```

## 追記（2019年8月30日）

react-native-elementsが1.xとなり、Formが廃止され、Inputになりました。それに伴い上記のコードは動きません。下記のような感じにする必要があります。

```js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Card, Input, Button, CheckBox } from 'react-native-elements';
import { Formik } from 'formik';
import * as Yup from 'yup';

export default class App extends React.Component {

    state = {
        email: '',
        password: ''
    }

    render() {
        return (
            <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
                <Formik
                    initialValues={{ email: '', password: '', check: false }}
                    onSubmit={values => alert(JSON.stringify(values))}
                    validationSchema={Yup.object().shape({
                        email: Yup.string().email('Emailの形式ではないようです。').required('Emailは必須です。'),
                        password: Yup.string().min(4, '4文字以上。').required('パスワードは必須です。'),
                        check: Yup.boolean().oneOf([true], '進むには同意してください。'),
                    })}
                >
                    {
                        ({ handleChange, handleSubmit, values, errors, touched, handleBlur, isValid, isSubmitting, setFieldValue }) => (
                            <View style={{ alignSelf: 'stretch', justifyContent: 'center', alignItems: 'center' }}>
                                <Input
                                    label="Email"
                                    placeholder="Input email address"
                                    containerStyle={{ marginTop: 20, width: '80%' }}
                                    autoCapitalize="none"
                                    errorMessage={errors.email && touched.email ? errors.email : null}
                                    value={values.email}
                                    onChangeText={handleChange('email')}
                                    onBlur={handleBlur('email')}
                                />
                                <Input
                                    placeholder="Input password."
                                    label="Password"
                                    containerStyle={{ marginTop: 20, width: '80%' }}
                                    errorMessage={errors.password && touched.password ? errors.password : null}
                                    secureTextEntry={true}
                                    value={values.password}
                                    onChangeText={handleChange('password')}
                                    onBlur={handleBlur('password')}
                                />
                                <CheckBox
                                    center
                                    title="同意する"
                                    containerStyle={{ width: '75%', borderColor: "#fff", backgroundColor: "#fff", marginTop: 30 }}
                                    checked={values.check}
                                    onPress={() => setFieldValue('check', !values.check)}
                                />
                                <Text style={{ fontSize: 12, color: 'red' }}>{errors.check && touched.check ? errors.check : null}</Text>
                                <Button
                                    title="Entry"
                                    buttonStyle={{ marginTop: 30, width: 200 }}
                                    onPress={handleSubmit}
                                    disabled={!isValid || isSubmitting}
                                />
                            </View>
                        )
                    }
                </Formik>
            </View>
        );
    }
}
```

## 参考

* https://www.youtube.com/watch?v=EO08ydCCsOQ
* https://snack.expo.io/Bk9pPK87X