React
redux
es2015
react-bootstrap
redux-form

Redux-Form の v7 を React-Bootstrap と併せて使う

ただいま Fields と FieldArray を使った複雑な構成についての記事も執筆中です、執筆出来次第こちらにリンクさせます。

はじめに

2017年の8月、使用ライブラリのバージョンを一斉にアップデートする、という作業を行った。
こと Redux-Form を v5 から v7 に上げる作業が困難を極め、全ての画面の Form まわりを書き換えるという精神衛生上よろしくない苦行のような改修を行った1
そのおかげというわけではないが、多少なりとも Redux-Form に明るくなったため、記事にまとめてみようと思う。

Redux-Form

https://redux-form.com/7.3.0
https://github.com/erikras/redux-form

React-Bootstrap

https://react-bootstrap.github.io

Redux-Form って?

A Higher Order Component using react-redux to keep form state in a Redux store.
The best way to manage your form state in Redux.

・・・らしい。
つまりは、HTML の Form の入力値や状態を Redux に流し込んで管理するためのコンポーネントを提供するライブラリ、ということだ2

簡単な例

詳しい仕組みや仕様、説明は公式ページに譲るとして、まずは ログインID のテキストボックスと ログインボタン のみが存在する画面を例とする。
まずは Redux-Form と Redux の橋渡しが必要となる、Form コンポーネントを Store で管理できるようにしよう。

configureStore.js
import { createStore, combineReducers } from 'redux'
import { reducer as formReducer } from 'redux-form'

const rootReducer = combineReducers({
  // ...your other reducers here
  // you have to pass formReducer under 'form' key,
  // for custom keys look up the docs for 'getFormState'
  form: formReducer
})

const store = createStore(rootReducer)

次にログイン画面の React コンポーネントと Form コンポーネントだ。

Login.js
import React from 'react';
import { reduxForm, Field } from 'redux-form';
import { Button, Col, ControlLabel, Form, FormControl, FormGroup, Grid, Glyphicon, HelpBlock, Jumbotron }  from 'react-bootstrap';
import PropTypes from 'prop-types';

class Login extends React.Component {
  static propTypes = {
    handleSubmit: PropTypes.func.isRequired,
    submitting: PropTypes.bool.isRequired
  };

  render() {
    const { handleSubmit, submitting } = this.props;
    return (
      <section>
        <section className={'container'}>
          <Grid>
            <Form horizontal onSubmit={handleSubmit}>
              <Jumbotron>
                <h2><Glyphicon glyph={'log-in'} />ログイン</h2>
                <Field name={'loginId'} validate={require} component={loginField} />
                <FormGroup>
                  <Col smOffset={2} sm={6}>
                    <Button type={'submit'} bsStyle={'primary'} disabled={submitting}>
                      <Glyphicon glyph={'log-in'} />ログイン
                    </Button>
                  </Col>
                </FormGroup>
              </Jumbotron>
            </Form>
          </Grid>
        </section>
      </section>
    );
  }
}

export default reduxForm(
  {
    form: 'LoginPageForm',
    initialValues: {
      loginId: ''
    },
    onSubmit: authenticate
  }
)(Login);

const authenticate = (form, dispatch) => {
  // ...authentication process
};

const loginField = field => {
  return (
    <FormGroup controlId={field.input.name} validationState={bsStyle(field)}>
      <Col componentClass={ControlLabel} sm={2}>
        ログインID
      </Col>
      <Col sm={6}>
        <FormControl {...field.input} type={'text'} />
        <FormControl.Feedback />
        <HelpBlock>{help(field)}</HelpBlock>
      </Col>
    </FormGroup>
  );
};

const require = value => {
  return value ? undefined: '必須です。';
};

const bsStyle = field => {
  if (!field.meta.touched) {
    return null;
  } else if (field.meta.error) {
    return 'error';
  } else {
    return 'success';
  }
};

const help = field => {
  if (field.meta.touched && field.meta.error) {
    return field.meta.error;
  } else {
    return null;
  }
};

各ライブラリの設定( package.json 他 )とデザインの説明は割愛する。

ログイン画面としてのコンポーネントとして Login というクラスを定義し、その内部に React コンポーネントを作成する。
Login の Form コンポーネントと Store を紐付けるために、 reduxForm() で Form をラップし、Login クラスそのものを渡す。
onSubmit には authenticate 関数を指定しており、これが Redux-Form の handleSubmit 関数を介して実行されることでフォームの入力値へのアクセスが可能になる。

export default reduxForm(
  {
    form: 'LoginPageForm',
    initialValues: {
      loginId: ''
    },
    onSubmit: authenticate
  }
)(Login);

const authenticate = (form, dispatch) => {
  // ...authentication process
};

reduxForm() で使用可能なプロパティは次のとおりだ。
https://redux-form.com/7.3.0/docs/api/reduxform.md

これで Redux-Form を使う準備は完了、実際に Form コンポーネントを使って画面の入力フォームを作ろう。
入力フォームと Store を紐付けるために Redux-Form には Field コンポーネントが準備されている。

<Field name={'loginId'} validate={require} component={loginField} />

Field コンポーネントの説明は後程。
Field コンポーネントは入力フォームと Store との紐付けであって、実際の入力フォームは component 属性に指定する。
component に指定した loginField 関数について説明しよう。

const loginField = field => {
  return (
    <FormGroup controlId={field.input.name} validationState={bsStyle(field)}>
      <Col componentClass={ControlLabel} sm={2}>
        ログインID
      </Col>
      <Col sm={6}>
        <FormControl {...field.input} type={'text'} />
        <FormControl.Feedback />
        <HelpBlock>{help(field)}</HelpBlock>
      </Col>
    </FormGroup>
  );
};

loginField 関数が引数として受け取る field オブジェクトは、 Redux-Form が提供するプロパティと関数を持った、Field コンポーネントの実体である。
field オブジェクトには大きく input と meta のプロパティが存在する。

以下、簡単に説明する。

  • FormGroup の controlId に指定している field.input.name には <Field> タグで指定した name が渡され、これで Form には loginId というキーで入力値が格納されることになる
  • FormControl には field.input そのものを渡し、type として text を指定する、これでテキストボックスが描画される
  • FormGroup の validationState には bsStyle 関数の結果が渡される、これで validation の結果に応じてフォーム枠の色が変更される
  • 同じように、FormControl.Feedback で validation の結果に応じて レ点 や バツ印 がフォームに表示されるようになる
  • そして HelpBlock の help 関数でエラーメッセージを表示している

この例ではデザインに該当する定義も loginField として切り出しているが、この辺りのコンポーネント化の単位は、プロダクトの実装方針に依るところだろう。
また、 require、help、bsStyle のような関数と併せて、loginField 関数などの入力フォームの実体はまとめて1つのファイルに切り出して管理するべきだろう。

この一連の流れは次のページに詳細に記載されている。
https://redux-form.com/7.3.0/docs/gettingstarted.md

Field コンポーネントの component 属性について

ここまで Field コンポーネントの component 属性には loginField のように field を引数とする関数を指定してきた。
ただし、component 属性には他にも使い方が存在する、簡単に説明しよう。

React コンポーネントのクラス

component に React コンポーネントを渡すことが出来る。
1つの <Field> タグが入力フォームの1項目となるため、使う場面は限られるだろう。
計算機のようなコンポーネントが良い例だろうか、演算用のボタンを押した後、計算結果が最終的に入力フォームとして設定されることになる。

ステートレス関数

上記ログインのテキストボックスで示した方法だが、これが最も柔軟で共通化可能な方法だろう。
render() メソッドの外に component を関数として定義し、<Field> タグに渡してあげれば良い。
一旦関数を定義しているが、無名関数としてcomponent 属性に直接関数を定義することも可能だ。
ただし、この方法は致命的な不具合を引き起こしてしまう。
なぜなら render() メソッド内部に関数を定義してしまうことになるため、キーが押されて値が入力される度に React のライフサイクルイベントが発生してしまうためだ。
そしてテキストボックスがその都度フォーカスを失い、まともに文字の入力が出来なくなってしまう。
なので render() メソッドの外に関数を定義して使用する方法をお勧めする。

文字列

こちらは単純だ、以下のような文字列指定でシンプルなテキストボックスが表示される。
component 属性には input を指定、type 属性には HTML で定義されている type が指定可能だ3

<Field component="input" type="text"/>

今回のテーマのように React-Bootstrap に Redux-Form を組み合わせる方法であれば、上で述べた2つの方法を取る必要がある。

詳細な仕様やコード例はこちらを参照のこと。
https://redux-form.com/7.3.0/docs/api/field.md

様々な入力フォーム

上ではシンプルなテキストボックスを例に説明したが、他の入力フォームについてもいくつか簡単に説明しよう。
placeholder 属性や disabled 属性が所々出て来るが、こちらは汎用的に指定可能な属性のため各フォームで使用可能だ。

テキストエリア

テキストエリアについてはテキストボックスと殆ど変わらないが、rows 属性によって初期表示字の行数の高さを指定可能となる。

export const textAreaField = field => {
  const { label, input, rows, placeholder } = field;
  return (
    <FormGroup controlId={input.name} validationState={bsStyle(field)}>
      <Col componentClass={ControlLabel} sm={2}>
        {label}
      </Col>
      <Col sm={6}>
        <FormControl {...input} rows={rows} placeholder={placeholder} componentClass={'textarea'} />
        <FormControl.Feedback />
        <HelpBlock>{help(field)}</HelpBlock>
      </Col>
    </FormGroup>
  );
};

このテキストエリアを使用する <Field> タグはこちら。
先に述べた通り、この例ではデザイン的な要素も含んだ汎用的な関数としており、そのため label や rows、placeholder といった属性を <Field> タグ経由で渡して使用している。

<Field
  name={'name'}
  label={'氏名'}
  validate={[require, minLength2]}
  rows={5}
  placeholder={'必須入力です。'}
  component={textAreaField}
/>

validate に複数の入力チェック用の関数を渡す際には配列として渡してやれば良い。
ここで minLength(2) のように引数を適用させた状態では入力チェックが効かない。
一旦変数を宣言してから渡す必要がある(GitHub の方でも issues として挙げられていた)

参考までに minLength の実装は次の通り。

const minLength = min => value => {
  return value && value.length < min ? `${min}文字以上です。` : undefined;
};

セレクトボックス

今回割愛、Select2 コンポーネント化も含め、改めて説明する予定。

チェックボックス

トグル的なチェックボックスの例は置いておいて、複数チェック可能なチェックボックスについて説明しよう。

export const multiCheckField = field => {
  const { label, input, options } = field;
  return (
    <FormGroup controlId={input.name} validationState={bsStyle(field)}>
      <Col componentClass={ControlLabel} sm={2}>
        <span onClick={() => {
          const values = (input.value.length == options.length) ? []: options.map(o => o.code);
          input.onChange(values);
        }}>
          {label} <Glyphicon glyph={(input.value.length == options.length) ? 'unchecked': 'check'}/>
        </span>
      </Col>
      <Col sm={9}>
        {options.map((o, i) => (
          <Checkbox
            key={`${input.name}_${i}`}
            name={`${input.name}[${i}]`}
            value={o.code}
            checked={input.value.indexOf(o.code) > -1}
            onChange={e => {
              const values = Object.assign([], input.value) || [];
              const value = e.target.value;
              const checked = e.target.checked;
              const index = values.indexOf(value);
              const exists = index > -1;
              if (checked && !exists) {
                values.push(value);
              }
              if (!checked && exists) {
                values.splice(index, 1);
              }
              input.onChange(values);
            }}
            inline
            >{o.name}</Checkbox>
        ))}
        <HelpBlock>{help(field)}</HelpBlock>
      </Col>
    </FormGroup>
  );
};

簡単に説明する。

  • genders は code と name がペアになったオブジェクトの配列、これが options に渡ってくるものとする
  • Checkbox コンポーネントの key 属性はユニークな値を設定する、これは React の VirtualDOM 管理上とても重要な属性、DOM 内でユニークになるような値を設定する
  • name 属性は input.name に [] を付けた形でインデックスを指定する、これで Redux-Form はこの input.name の名前で入力フォームに配列を設定する
  • onChange ハンドラには関数を指定する、イベント e を引数に、チェックされたコード値を配列に保持する関数となる
  • input.onChange に値を設定することで、直接フォームの値を置き換えていることが分かる
  • これを踏まえ、ラベル部分にラベル文字列とグリフアイコンを <span> タグで囲んでonClick ハンドラを指定することで、全てチェック / 全て外すの制御を実現している

このチェックボックス群を使用する <Field> タグはこちら。

<Field name={'gender'} label={'対象性別'} options={genders} component={multiCheckField} />

ラジオボタン

ラジオボタンについてはチェックボックスと性質が似ている、基本的には上記のチェックボックスの例と同じ。
Checkbox コンポーネントを 次のような Radio コンポーネントに置き換えれば良い。

<Radio
  key={`${input.name}_${i}`}
  name={`${input.name}[${i}]`}
  value={o.code}
  checked={input.value.indexOf(o.code) > -1}
  onChange={e => input.onChange(e.target.value)}
  inline
  >{o.name}</Radio>

React-Bootstrap の Forms には他にもいくつか入力フォーム用のコンポーネントが準備されている。
必要に応じて Redux-Form を組み合わせて使うことが出来る。
https://react-bootstrap.github.io/components/forms

入力値のハンドリングと React コンポーネント内からの変更

さて、ここまで入力フォームのコンポーネントについて説明してきたが、ではこのフォームの入力値へどのようにアクセスすればよいか。
サブミット時には handleSubmit 関数を介して入力値へのアクセスは可能になるが、サブミット時以外ではどうか。
Redux-Form ではフォームに入力された値は隠蔽されているため、直接操作したり参照することができない4
もちろん Fieldコンポーネントの引数、ステートレス関数内の field であれば操作可能だが5、React コンポーネント内からは参照できない6

formValueSelector 関数

Redux-Form には formValueSelector 関数が用意されており、フォームの任意項目の値を保持したオブジェクトを state 内に作成することが出来る。
以下はいくつかのフォームの中に name というテキストボックスと noName というチェックボックスが存在する React コンポーネントである。

FooBarInputPage.js
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { formValueSelector, Field, reduxForm } from 'redux-form';

class FooBarInputPage extends React.Component {
  componentWillUpdate(nextProps) {
    changeField(nextProps);
  }

  render() {
    // ...some input fields
  }
}

let FooBarInputPageForm = reduxForm({
  form: 'FooBarInputPageForm'
})(FooBarInputPage);

export default FooBarInputPageForm = connect(
  state => ({
    initialValues: {
      // ...some input fields
      name: '',
      noName: false
    },
    formValues: formValueSelector('FooBarInputPageForm')(state, ['name', 'noName'])
  }),
  dispatch => ({
    // ... bindActionCreators
  })
)(FooBarInputPageForm);

export const changeField = nextProps => {
  if (nextProps.formValues) {
    const { dispatch, change, formValues } = nextProps;
    if (formValues.noName) {
      dispatch(change('name', ''));
    }
  }
};

細かい部分は省いているが、formValueSelector 関数にフォーム名と参照したいフィールドを指定した formValues という state を持っている。
この formValues は props 内に保持されるため React コンポーネント内から参照することが可能だ。
props には Redux の dispatch 関数、Redux-Form の change 関数が存在しており、これらを使って入力フォームの任意の項目を変更することが出来る。
つまり componentWillUpdate 関数から呼び出している changeField 関数にて、noName のチェックがオンになっていれば、入力フォームの name の値を空文字に変更していることがわかる。

ここまで

Redux-Form と React-Bootstrap を使った基本的な機能について説明してきた。
ここまでの説明だけでシンプルな画面は作成できると思うが、相関関係のある入力フォームや配列的な入力フォームは別のコンポーネントが必要になる。
次は Fields や FieldArray について説明したい。


  1. ES2015、React、Redux 他を触り始めて3ヶ月の人間がやることではない 

  2. という認識であってるはず 

  3. 全て試したわけではないが 

  4. Redux-Form の v5 までは直接操作が出来たはず 

  5. getFormValues 関数のような Selectors が準備されているが、あまり使う場面はないだろう 

  6. Gets the form values. Shocking, right?