react + reduxでフォームを作成する際、redux-formを使用しましたが、情報が少なかったので個人的にまとめてみました。
実装
まずreducerを作成します。
import { createStore, combineReducers } from "redux";
import { reducer as formReducer } from "redux-form";
const reducer = combineReducers({
  form: formReducer
});
const store = createStore(reducer)
export default reducer;
コンポーネントを作成する。
import React from "react";
import Form from "./Form";
export default class App extends Component {
  render() {
    return (
      <div>
        <Form onSubmit={showResult} />
      </div>
    );
  }
}
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 のストアに、個々の入力を接続するものです。
属性がいくつかありますが、必須なのは2つです。
1つは name属性。フォーム値の値に対応する文字列パスです。
もう1つは component属性。
指定の方法はいくつかあり、上記のように文字列指定ではテキストボックスが表示されます。type属性にはHTMLで定義されているtypeが指定可能です。
自身で作成したコンポーネントの指定もできます。
フォームを追加可能にする
フォームを追加し、複数のデータを一括で保存できるようにしていきます。
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コンポーネントの配列を表す名称になります。
初期値を設定
初期値の設定とデータの送信に成功した際に初期化する処理を実装します。
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を使い上記のような記述で実装できます。
バリデーションをかける
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だと以下のようになる
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>
    );
  }
}
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)
