redux-form 慣れてきた

React でフォームを実装するためのライブラリは、決定版と呼べるものが見当たらないので、一番スターが多い redux-form を使っている。

github.com

フォームの入力内容をストアに持つのはいかがなものかと思っていたのに、不思議と使っているうちに気にならなくなってきた。あと、v5 までは API がツラくて、何度も使うのをやめたくなったけど、v6 で API が刷新されてだいぶマシになった。

基本的にシンプルなフォームなら下記のように書ける。 onChange を書きまくらなくていいだけでも十分助かる。

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';

// カスタマーを追加するアクション
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;
};

// フォームの入力用コンポーネント。
// input と meta は redux-form が渡してくる props。
// type や label は Field に渡した props がそのまま渡ってくる。
const renderField = ({ input, type, label,meta: { touched, error } }) => (
  <div className="form-group">
    <label>{label}</label>
    <input {...input} type={type} className="form-control" />
    {touched && error && <span className="text-danger">{error}</span>}
  </div>
);

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

    return (
      <form onSubmit={handleSubmit}>
        <Field name="code"
               component={renderField}
               label="コード"/>
        <Field name="name"
               component={renderField}
               label="名前"/>
        <Field name="kana"
               component={renderField}
               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 (
      <div id="wrapper">
        <CustomerFormContainer />
        <CustomerListContainer />
      </div>
    );
  }
}

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