React UI library の antd について (3) - redux-form

[React UI library の antd について (1) - Button](https://qiita.com/sand/items/35422a669e6021cccb4f)
[React UI library の antd について (2) - Layout](https://qiita.com/sand/items/dd420202e6c7daaea667)
[React UI library の antd について (3) - redux-form](https://qiita.com/sand/items/a0cf3fa3a1a17741c06b)

---



React のUI libraryであるantdは使い方はとても簡単でcomponentsも種類が多いのを見てきました。Layout componentも比較的簡単に使えました。


今回はFormとInput componentを見ていきます。

validateの方法は2つ示唆されています。最初はantd Formで標準の**Form.create**を使うことです。2番目はForm.createを使わずに行うことです。

2番目の方法としては、まったく手動で行うとかいろいろあるでしょうが、今回は**[redux-form](https://qiita.com/sand/items/5ef1564546c130080398)**を利用します。


Form.createの使い勝手は少し複雑な気がします。比べてredux-formを使う方法はシンプルでわかりやすいです。



# Form.createを使った場合 - Antd標準
[Antd Form component](https://ant.design/components/form/)

antdのForm component とInput componentの使い方を見ていきます。ここではform の validateは、antd Formの標準のやり方ですが、**Form.create**でdecorateすること行います。

以下のコードは、HorizontalLoginForm という component を **Form.create** で decorate しています。

```
const WrappedHorizontalLoginForm = 
    Form.create({ name: 'horizontal_login' })(HorizontalLoginForm);
```


Form.create で decorate された component には this.props.form property が渡されます。this.props.formはいくつかの属性を持ちます。以下にその中のいくつかを示します。（[==> 全リストはこちら](https://ant.design/components/form/)）

* validateFields
* getFieldDecorator
* getFieldError
* getFieldsError
* isFieldTouched


\<Form.Item />の中では、getFieldDecoratorでinput controlをラップします。その中でvalidate ruleをまとめて記述していくことになります。一か所で全ての情報をかけるので、ある意味見やすいと言えます。


```JSX:src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import 'antd/dist/antd.css';
import { Form, Icon, Input, Button } from 'antd';

function hasErrors(fieldsError) {
  return Object.keys(fieldsError).some(field => fieldsError[field]);
}

class HorizontalLoginForm extends React.Component {
  componentDidMount() {
    // To disabled submit button at the beginning.
    this.props.form.validateFields();
  }

  handleSubmit = e => {
    e.preventDefault();
    this.props.form.validateFields((err, values) => {
      if (!err) {
        console.log('Received values of form: ', values);
      }
    });
  };

  render() {
    const { getFieldDecorator, getFieldsError, getFieldError, isFieldTouched } = this.props.form;

    // Only show error after a field is touched.
    const usernameError = isFieldTouched('username') && getFieldError('username');
    const passwordError = isFieldTouched('password') && getFieldError('password');
    return (
      <Form layout="inline" onSubmit={this.handleSubmit}>
        <Form.Item validateStatus={usernameError ? 'error' : ''} help={usernameError || ''}>
          {getFieldDecorator('username', {
            rules: [{ required: true, message: 'Please input your username!' }],
          })(
            <Input
              prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
              placeholder="Username"
            />,
          )}
        </Form.Item>
        <Form.Item validateStatus={passwordError ? 'error' : ''} help={passwordError || ''}>
          {getFieldDecorator('password', {
            rules: [{ required: true, message: 'Please input your Password!' }],
          })(
            <Input
              prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
              type="password"
              placeholder="Password"
            />,
          )}
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit" disabled={hasErrors(getFieldsError())}>
            Log in
          </Button>
        </Form.Item>
      </Form>
    );
  }
}

const WrappedHorizontalLoginForm = Form.create({ name: 'horizontal_login' })(HorizontalLoginForm);

ReactDOM.render(<WrappedHorizontalLoginForm />, document.getElementById('root'));
```

初期画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/9f5c07b7-8f56-95c8-c27c-3d0878292843.png)


一度入力後、削除するとエラーとなる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/a6a45705-625b-6629-b1e4-de9e27dc046c.png)


正しく入力後、ログインボタンを押す
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/99a979bc-d1be-8e3f-c114-c507874f419f.png)


コンソールにログが出力されている
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/32a98d1e-80cc-3d09-0e40-e3d219542b48.png)


# redux-form を使った場合

### 【注意】(2019/07/13)

**いろいろ試した結果、redux-formとreact-router v4の組み合わせがうまく動作しないという結論に達しました。reduxForm()とconnect()という2つのHOCで2重にラップした時に、propsが、最終的なコンポーネントにうまく伝わっていかない感じです。ラップする順番を変えてもだめでした。しかしreduxForm()の代わりに、antdのForm.create()でラップすればうまくいきました。何か私の見落としがあるのかもしれませんが、当面はForm.create()でいろいろ試していきたいと思います。**



[Form component](https://ant.design/components/form/)
マニュアルには次のような記述があります。**Form.create** はformデータを集めvalidateします。しかしForm.createによるやり方が気に入らない場合は、使う必要はありません。手動でvalidateを行っても構いません。

ここではvalidateを**redux-form**で行いたいと思います。個人的にはこちらの方がシンプルで柔軟なような気がします。
[redux-form (1) - Simple Form Example](https://qiita.com/sand/items/5ef1564546c130080398)


つまりantdとredux-formを組み合わせて使うわけです。

必要なライブラリをインストールします。

```
yarn add redux react-redux redux-form
```



以下はantdとredux-formを同時に使用しているコードです。**Form.createは使わず、代わりにreduxFormを使っていることに注意してください。**

* \<Form />はantdです。
* \<Field />はredux-formです。

\<Field /> の　component={renderInput} に注目してください。renderInput は antd のcomponentで構成されたcomponentになります。renderInput には prefix等のpropを引き渡しています。


```JSX:src/HorizontalLoginForm.js
import React from 'react';
import 'antd/dist/antd.css';
import { Form, Icon, Button } from 'antd';
import { Field, reduxForm } from 'redux-form';
import renderInput from './input';


const HorizontalLoginForm = props => {

  const { handleSubmit, pristine, submitting, message } = props

  return (
      <Form layout="inline" onSubmit={handleSubmit}>
        <Field
          hasFeedback
          type="text"
          name="name"
          component={renderInput}
          disabled={submitting}
          label="Name"
          placeholder="Full Name"
          prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
        />
        <Field
          hasFeedback
          type="password"
          name="password"
          component={renderInput}
          disabled={submitting}
          label="Password"
          placeholder="Password"
          prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
        />
        <Form.Item>
          <Button type="primary" htmlType="submit" className="btn-submit" disabled={pristine || submitting}>
            Log in
          </Button>
        </Form.Item>
        {!!message && <p className="caption-invalid">{message}</p>}
      </Form>
  );
}

const validate = values => {
  const errors = {};

  if (!values.name) {
    errors.name = "Name can't be blank";
  }

  if (!values.password) {
    errors.password = "Password can't be blank";
  }

  return errors;
};


export default reduxForm({
  form: 'login-form',
  validate,
})(HorizontalLoginForm);
```


以下がrenderInput componentのソースです。propsを展開して、inputやrestを取り出していることに注目してください。そのままInput componentに展開されます。特に**(1)input object (redux-form) はvalueを含んでおり、(2)Input (antd component) のvalue属性の値としてそのまま適用されます。このvalueはactionによりRedux stateにbindされています。**またrestにはprefixが含まれており、そのままアイコン情報が展開されます。
[(1) Redux-form Fieldのinput object](https://qiita.com/sand/items/5ef1564546c130080398#%EF%BC%93-field-%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)
[(2) Antd Input component](https://ant.design/components/input/)



```JSX:src/input.js
import React from 'react';
import { Form, Input } from 'antd';

const newComponent = props => {
  const { input, meta, hasFeedback, label, ...rest } = props;
  const hasError = meta.touched && meta.invalid;

  return (
    <Form.Item
      label={label}
      help={hasError && meta.error}
      hasFeedback={hasFeedback && hasError}
      validateStatus={hasError ? 'error' : 'success'}
    >
      <Input {...input} {...rest} />
    </Form.Item>
  );
};

export default newComponent;
```

ここで[**分割代入** (Destructuring assignment) 構文](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring)を思い出しましょう。

```
({a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40});
console.log(a); // 10
console.log(b); // 20
console.log(rest); // {c: 30, d: 40}
```



以下のindex.jsでは主にredux-formの設定を行っています。Redux storeの設定や、onSubmit関数の定義を行っています。


```JSX:src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { createStore, combineReducers } from 'redux'
import { reducer as reduxFormReducer } from 'redux-form'
import HorizontalLoginForm from './HorizontalLoginForm'

const dest = document.getElementById('root')
const reducer = combineReducers({
  form: reduxFormReducer // mounted under "form"
})
const store = createStore(reducer)

const showResults = values =>
  new Promise(resolve => {
    setTimeout(() => {
      // simulate server latency
      window.alert(`You submitted:\n\n${JSON.stringify(values, null, 2)}`)
      resolve()
    }, 500)
  })

let render = () => {
  // const HorizontalLoginForm = require('./HorizontalLoginForm').default
  ReactDOM.hydrate(
    <Provider store={store}>
      <h2>Form</h2>
      <HorizontalLoginForm onSubmit={showResults} />
    </Provider>,
    dest
  )
}

render()
```



初期画面。antdのアイコンが表示されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/8274a1c6-ba87-ea1a-7979-fb94e1d115b6.png)


入力を消すとエラーが表示されます。ボタンは無効化されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/21af1ff4-d912-8057-5185-6079bc80f6fb.png)


入力を正しく行うと、ボタンも有効化されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/a4dcdfd9-805f-18b1-f46f-a199248caee0.png)


ボタンを押すと、onSubmit関数が起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/3b12e539-dbea-80b0-4d39-c7ff0e41b49c.png)



今回は以上です。