redux-form でも react-bootstrap のフォーム関連コンポーネントを使う

試行錯誤の末に、 react-bootstrap が提供する FormGroupFormControl といったフォーム関連コンポーネントを、 redux-form と一緒に使うことができた。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { createStore, combineReducers } from 'redux';
import { Provider, connect } from 'react-redux';
import {
  Field,
  reduxForm,
  reducer as formReducer
} from 'redux-form';
import {
  Grid,
  Row,
  Col,
  Table,
  FormGroup,
  ControlLabel,
  FormControl
} from 'react-bootstrap';

// bootstrap の CSS を読み込む。
// webpack で CSS Module を使っていることが前提。
import '../node_modules/bootstrap/dist/css/bootstrap.css';

// カスタマーを追加するアクション
const ADD_CUSTOMER = "ADD_CUSTOMER";

// カスタマーを追加するときに使う Action Creator
function addCustomer(customer) {
  return { type: ADD_CUSTOMER, customer };
}

// 初期状態
const initialState = {
  customers: [
    { code: "10", name: "香川", kana: "カガワ" },
    { code: "4", name: "本田", kana: "ホンダ" },
    { code: "5", name: "長友", kana: "ナガトモ" },
    { code: "9", name: "岡崎", kana: "オカザキ" }
  ]
};

// カスタマーを操作する Reducer
const customersReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_CUSTOMER:
      return Object.assign({}, state, {
        customers: [...state.customers, action.customer]
      });
    default:
      return state;
  }
};

// redux-form が提供する reducer も使う
const rootReducer = combineReducers({
  customers: customersReducer,
  form: formReducer
});

// アプリ唯一のストアを作成
const store = createStore(rootReducer);

// redux-form 用バリデーション関数
const validate = (values) => {
  // 検証に失敗したら values と同じキーで
  // エラーメッセージをセットする。
  const errors = {};
  if (!values.code) {
    errors.code = "コードを入力してください。";
  }
  if (!values.name) {
    errors.name = "名前を入力してください。";
  }
  if (!values.kana) {
    errors.kana = "カナを入力してください。";
  }
  return errors;
};

// フォームの入力用コンポーネント。
// react-bootstrap のフォーム関連コンポーネントを使う。
class FormField extends Component {
  render() {
    // input と meta は redux-form が渡してくる props。
    // type や label は Field に渡した props がそのまま渡ってくる。
    // select や textarea に対応するために componentClass を受け取る。
    // select の option に対応するために children も受け取る。
    const {
      input,
      label,
      type,
      componentClass,
      children,
      meta: {
        touched,
        error
      }
    } = this.props;

    return (
      <FormGroup>
        <ControlLabel>
          {label}
        </ControlLabel>

        <FormControl
          {...input}
          type={type || "text"}
          componentClass={componentClass || "input"}
          >
          {children}
        </FormControl>

        {touched && error && <span className="text-danger">{error}</span>}
      </FormGroup>
    );
  }
}

// カスタマー登録フォーム本体
class _CustomerForm extends Component {
  render() {
    const {
      handleSubmit,
      pristine,
      reset,
      submitting
    } = this.props;

    return (
      <form onSubmit={handleSubmit}>
        <Field name="code"
               component={FormField}
               type="text"
               label="コード"/>
        <Field name="name"
               component={FormField}
               type="text"
               label="名前"/>
        <Field name="kana"
               component={FormField}
               type="text"
               label="カナ"/>

        <div className="form-group">
          <button className="btn btn-primary"
                  type="submit"
                  disabled={submitting}>
            追加
          </button> 
          <button className="btn btn-default"
                  type="button"
                  onClick={reset}
                  disabled={submitting || pristine}>
            クリア
          </button>
        </div>
      </form>
    );
  }
}

// カスタマー登録フォームを redux-form 化。
// form にはユニークな文字列を指定する。
const CustomerForm = reduxForm({
  form: "customerForm",
  validate
})(_CustomerForm);

// さらに connect でコンテナコンポーネント化。
// redux-form でラップしたコンポーネントには onSubmit の props が
// 生えている。
// submit 時の処理はコンテナコンポーネントで実装する。
const CustomerFormContainer = connect(
  state => {
    return {};
  },
  dispatch => {
    return {
      onSubmit: (values) => {
        dispatch(addCustomer(values));
      }
    };
  }
)(CustomerForm);

// カスタマー一覧を表示するコンポーネント
class CustomerList extends Component {
  renderCustomers() {
    return this.props.customers.map(c =>
      <tr key={c.code}>
        <td>{c.code}</td>
        <td>{c.name}</td>
        <td>{c.kana}</td>
      </tr>
    );
  }

  render() {
    return (
      <Table>
        <thead>
          <tr>
            <th>コード</th>
            <th>名前</th>
            <th>カナ</th>
          </tr>
        </thead>
        <tbody>
          {this.renderCustomers()}
        </tbody>
      </Table>
    );
  }
}

const CustomerListContainer = connect(
  state => {
    return { customers: state.customers.customers };
  }
)(CustomerList);

class App extends Component {
  render() {
    return (
      <Grid>
        <Row>
          <Col md={12}>
            <CustomerFormContainer />
            <CustomerListContainer />
          </Col>
        </Row>
      </Grid>
    );
  }
}

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("app")
);

redux-form の Field は自身に渡された props を、component に指定したコンポーネントにほとんど渡してくれる。なので FormControl が必要とするものを Field に渡せばいいって寸法。

ライブラリのソースコードを読む習慣は大事だね。