redux-form  でフォームを作成する

react + reduxでフォームを作成する際、redux-formを使用しましたが、情報が少なかったので個人的にまとめてみました。

#実装

まずreducerを作成します。

```Reducer.js
import { createStore, combineReducers } from "redux";
import { reducer as formReducer } from "redux-form";

const reducer = combineReducers({
  form: formReducer
});

const store = createStore(reducer)
export default reducer;
```

コンポーネントを作成する。

```App.js
import React from "react";
import Form from "./Form";

export default class App extends Component {
  render() {
    return (
      <div>
        <Form onSubmit={showResult} />
      </div>
    );
  }
}
```

```Form.js
import React from "react";
import { Field, reduxForm } from "redux-form";

const Form = props => {
  const { handleSubmit } = props
  return (
    <form onSubmit={handleSubmit}>
      <Field
        name="FirstName"
        type="text"
        component="input"
      />
      <Field
        name="LastName"
        type="text"
        component="input"
      />
      <Field
        name="Comment"
        component="textarea"
      />
      <button type="submit">
        Submit
      </button>
    </form>
  )
}
export default reduxForm({
  form: 'simple' // a unique identifier for this form
})(Form)
```

**reduxForm**
フォームコンポーネントをラップし、入力をRedux actionにbindします。

**Field コンポーネント**
Redux のストアに、個々の入力を接続するものです。

属性がいくつかありますが、必須なのは２つです。
１つは `name`属性。フォーム値の値に対応する文字列パスです。
もう１つは `component`属性。
指定の方法はいくつかあり、上記のように文字列指定ではテキストボックスが表示されます。type属性にはHTMLで定義されているtypeが指定可能です。
自身で作成したコンポーネントの指定もできます。

##フォームを追加可能にする
フォームを追加し、複数のデータを一括で保存できるようにしていきます。

```Form.js
import { Field, reduxForm, FieldArray } from "redux-form";

const renderMembers = ({ fields }) => (
  <ul>
    <button 
      type="button" 
      onClick={() => fields.push({})}
    >
      追加
    </button>
    {fields.map((member, index) => (
      <li key={index}>
        <Field
          name={`${member}.FirstName`}
          type="text"
          component="input"
        />
        <Field
          name={`${member}.LastName`}
          type="text"
          component="input"
        />
        <Field
          name={`${member}.Comment`}
          component="textarea"
        />
      </li>
      <button type="submit">
        Submit
      </button>
    ))}
  </ul>
);

const Form = props => {
  const { handleSubmit } = props
  return (
    <form onSubmit={handleSubmit}>
      <FieldArray name="members" component={renderMembers} />
    </form>
  )
}

// 省略
```

同一のフォームの配列は、`FieldArray` コンポーネントを使って実現できます。
`name`属性は指定されたコンポーネントで記述されるFieldコンポーネントの配列を表す名称になります。

##初期値を設定

初期値の設定とデータの送信に成功した際に初期化する処理を実装します。

```Form.js
import { Field, reduxForm, FieldArray, reset } from "redux-form";

const renderMembers = ({ fields }) => (
  <ul>
    <button 
      type="button" 
      onClick={() => fields.push({
        comment: "デフォルト値です。"
      })}
    >
      追加
    </button>
    // 省略
  </ul>
);

// 省略

const afterSubmit = (result: any, dispatch: any) =>
  dispatch(reset('Form'));

export default reduxForm({
  form: 'Form',
  initialValues: {
    members: [
      {
        comment: "デフォルト値です。"
      }
    ]
  },
  onSubmitSuccess: afterSubmit,
})(Form)
```

reduxFormのパラメータとして`initialValues`で初期値を指定します。
ただこれだと追加したフォームには初期値が反映されないので、`fields.push`でも初期値を指定します。
これで初期値設定は完了です。

初期化には、redux-formで用意されている`reset`関数を使用します。送信が成功した際の処理は、`onSubmitSuccess`を使い上記のような記述で実装できます。

##バリデーションをかける

```Form.js
const validation = values => {
  const errors = {}
  if (!values.members || !values.members.length) {
    errors.members = { _error: 'メンバーを入力してください。' }
  } else {
    const membersArrayErrors = [];
    values.members.forEach((member, memberIndex) => {
      const memberErrors = {};
      if (member && !member.name) {
        memberErrors.firstName = "苗字を入力してください。";
      }
      membersArrayErrors[memberIndex] = memberErrors
    })
  }
  return errors
}

// 省略

export default reduxForm({
  form: 'Form',
  initialValues: {
    members: [
      {
        comment: "デフォルト値です。"
      }
    ]
  },
  onSubmitSuccess: afterSubmit,
  validate: validation,
})(Form)
```

reduxFormのパラメータとして`validate`で指定します。

##TypeScriptでの記述
TepeScriptだと以下のようになる

```App.tsx
import React from "react";
import Form from "./Form";

export default class App extends Component<
  InjectedFormProps<{}, Props> & Props, State
  >  {
  render() {
    return (
      <div>
        <Form onSubmit={showResult} />
      </div>
    );
  }
}
```

```Form.tsx
const renderMembers = ({fields}: any) => (
  <ul>
    <button 
      type="button" 
      onClick={() => fields.push({})}
    >
      追加
    </button>
    {fields.map((records: any, index: number) => (
      <li key={index}>
        <Field
          name={`${member}.FirstName`}
          type="text"
          component="input"
        />
        <Field
          name={`${member}.LastName`}
          type="text"
          component="input"
        />
        <Field
          name={`${member}.Comment`}
          component="textarea"
        />
      </li>
      <button type="submit">
        Submit
      </button>
    ))}
  </ul>
);

const Form = (props: any) => {
  const { handleSubmit } = props
  return (
    <form onSubmit={handleSubmit}>
      <FieldArray name="members" component={renderMembers} />
    </form>
  )
}

const validation = (values: any) => {
  const errors = {}
  if (!values.members || !values.members.length) {
    errors.members = { _error: 'メンバーを入力してください。' }
  } else {
    const membersArrayErrors = [];
    values.members.forEach((member, memberIndex) => {
      const memberErrors = {};
      if (member && !member.name) {
        memberErrors.firstName = "苗字を入力してください。";
      }
      membersArrayErrors[memberIndex] = memberErrors
    })
  }
  return errors
}

export default reduxForm({
  form: 'Form',
  initialValues: {
    members: [
      {
        comment: "デフォルト値です。"
      }
    ]
  },
  validate: validation,
})(Form)
```