React + ReduxのプロジェクトでRedux Formを使ったので使い方のまとめと注意点

react

React + Reduxのプロジェクトにジョインしているのですが、Form周りに「Redux Form」に使っているのでざっくりですが使い方の紹介と使ってみて思ったことをまとめてみようかなと思います。

こちらから完成例ダウンロードできます

Redux Formとは

Form周りのライブラリです。これを使用することでValidateを簡単に実装できます。現在(2016/10/07)の最新バージョンは6.0.5です。

Redux Form
上記のexmapleを見ればRedux + Reactやったことある人なら作れるはず。

GitHubのStarやForkもたくさんついていますし、何より開発が活発です!友人曰くとりあえず「これ使っておけばOKっしょ!」とのこと。
ちなみに、開発が活発すぎてバージョンアップ時にプロジェクトがてんやわんやになったのは別の記事にしようかなと。

とりあえずForm作ってみる

まずは簡単なFormを作ってみます。下記の記事でReact + Redux の環境構築と簡単なチュートリアルがあるのでよかったらみてみてください。

今回は、簡単なFormを作って、Validateしてサーバーに送信できる手前までやります。サーバーに送信のところはAPIとの繋ぎこみが必要になってくるので割愛。

package.json

まずはインストールしないと話にならないので。

{
  ・・・

  "dependencies": {
    "react": "^15.3.0",
    "react-dom": "^15.3.0",
    "react-redux": "^4.4.5",
    "redux": "^3.5.2",
    "redux-form": "6.0.5" ←これを追加
  }
}
npm update
npm start

RootReducer変更

ReducerにRedux Formを登録します。

import {combineReducers} from "redux"
import { reducer as formReducer } from 'redux-form'
import Modal from "./Modal"

const App = combineReducers({
  Modal,
  form: formReducer
})

export default App

FormのContainersを作る

containers/Form.jsを作ります。

import React, { Component } from 'react'
import { Field, reduxForm } from 'redux-form'

import FieldInput from '../components/FieldInput'


class SubmitValidationForm extends Component {
  render() {
    const { error, handleSubmit } = this.props
    return (
      <form className="c-form" onSubmit={handleSubmit}>
        <Field name="email" type="email" component={FieldInput} label="email"/>
        <Field name="password" type="password" component={FieldInput} label="Password"/>
        {error && <strong>{error}</strong>}
        <div>
          <button className="c-btn c-btn-primary--flat" type="submit">Submit</button>
        </div>
      </form>
    )
  }
}

export default reduxForm({
  form: 'submitValidation'
})(SubmitValidationForm)


Input用のコンポーネントを作る

component/FieldInput.jsを作ります。今回は、textで入力できるコンポーネントです。業務であればRadioボタンやCheckboxなんかのコンポーネントも必要になってくるので都度作る感じです。

import React from'react'

export default class FieldInput extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    const {input, label, type, meta, meta: { touched, error }} = this.props
    return (
      <div className="c-container--full">
        <label>{label}</label>
        <div>
          <input {...input} placeholder={label} type={type}/>
          {touched && error && <span className="c-text--highlight">{error}</span>}
        </div>
      </div>
    )
  }
}

errorについて

component/FieldInput.jserrorと書かれている部分がありますね。これはValidateをするときに返ってきます。そのときにエラー文言を含めておく感じですね。この辺は後ほどやってみます。まずはvalidateなしで送信できる状態になるようにしましょう。

App.js

以前の記事でModalが出るようになっています。今回作ったForm.jsが出るようにします。

import { bindActionCreators } from 'redux'
import React, { Component } from 'react'
import { connect } from 'react-redux'
import Modal from '../components/Modal'
import {modalOpen} from '../actions/Modal'
import DevTools from './DevTools'
import Form from './Form'

class App extends Component {
  componentWillMount() {}

  handleModalOpen(){
    this.props.modalOpen(true)
  }

  handleModalClose(){
    this.props.modalOpen(false)
  }

  onSubmitEvent(values){
    console.log('APIに送信したり色々するとこ')
    console.log(values)
  }

  render() {
    const {show} = this.props
    return (
      <div>
        <div className="l-wrapper">
          <div className="c-container">
            <h1 className="c-title c-title--primary">Modal</h1>
            <button className="c-btn c-btn-primary--flat" onClick={this.handleModalOpen.bind(this)}>Modal Open</button>
            <Modal
              handleModalOpen={this.handleModalOpen.bind(this)}
              handleModalClose={this.handleModalClose.bind(this)}
              show={show}
              title='modalテスト'
            >
              モーダル内容
            </Modal>
          </div>
          <Form onSubmit={this.onSubmitEvent.bind(this)} />
        </div>
        <DevTools />
      </div>
    )
  }
}

function mapStateToProps(state) {
  return {
    show: state.Modal.show
  }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(Object.assign({}, {modalOpen}), dispatch)
}

export default connect(mapStateToProps, mapDispatchToProps)(App)
onSubmitEvent(values){
  console.log('APIに送信したり色々するとこ')
  console.log(values)
}

この部分でSubmitしています。本来であればAPIを叩きに入ってサーバに送信するところですね。

試す

ここまでコードがかけたら、formに入力して、Submitしてみる

SET_SUBMIT_SUCCEEDED

これが出ていればちゃんと送信できる状態になっています!

validateする

util/Validate.jsなどを作ってそこで一括でvalidateを行えるようにします。

公式にあるValidateのサンプルについて

公式にもValidateのサンプルがあるのですが、まっっっったく汎用的に使えない代物なんですよねー

例えば、必須項目チェックの場合だと

if(!values.email){
  // 何か処理
}

って感じです。これだとemail以外の例えばpasswordとかuserNameとかは都度作らなくちゃいけないですよね。できれば汎用的に作りたい!!って誰しも思うはずです。実は今回プロジェクトでもRedux Form使ったんですが、このValiddate周りが一番苦戦したとこでした。

とりあえず作ってみる

util/Validate.jsを作る

utilディレクトリを作成して、その中にValidate.jsを作ります。
かなりカオスってますが、この辺が一番苦労したところ。もしバグ等あったら教えてください。

export const validate = config => (values, props) => {
  var errors = {}
  for (const configKey in config) {
    let keys = configKey.split('.')
    // 横断的にバリデーションを行う
    traverse(values, keys, config[configKey], errors)
  }
  return errors
}

/**
 * 横断バリデーション
 */
function traverse(values, keys, validations, errors = {}) {
  // バリデーション設定キーが存在する場合(root node / inner node)
  if (keys.length > 0) {
    // 配列識別文字列[]を削除
    const _key = keys[0].replace(/\[]/g, '')
    // まだ対象キーの階層にエラーが存在しない場合、初期化
    if (errors[_key] === undefined) {
      errors[_key] = {}
    }

    // バリデーション対象のフィールドに値が存在しない場合(フォーム初期化直後等)
    if (values === undefined || values[_key] === undefined) {

      // フィールドキーから判断して、対象のフィールドが配列となりうる場合
      if (keys[0].indexOf('[') > 0) {
        // インデックス0でバリデーションエラーを登録する
        errors[_key] = [{}]
        // 再帰的にバリデーション
        errors[_key][0] = traverse(undefined, keys.slice(1), validations, errors[_key][0])
        // エラーが無かった場合は、エラーオブジェクトのkeyごと削除
        // 残ってるとエラーがあるとredux-formに判断される
        if (errors[_key][0] && Object.keys(errors[_key][0]).length < 1) delete errors[_key][0]
      } else {
        // 再帰的にバリデーション
        errors[_key] = traverse(undefined, keys.slice(1), validations, errors[_key])
        // エラーが無かった場合は、エラーオブジェクトのkeyごと削除
        if (errors[_key] && Object.keys(errors[_key]).length < 1) delete errors[_key]
      }
    } else {
      // 値が存在する場合
      if (values[_key] instanceof Array) {
        errors[_key] = []
        // 値が配列の場合、各インデックスをバリデーション
        for (let [index, value] of values[_key].entries()) {
          errors[_key].push({})
          // 再帰的にバリデーション
          errors[_key][index] = traverse(value, keys.slice(1), validations, errors[_key][index])
          // エラーが無かった場合は、エラーオブジェクトのkeyごと削除
          if (errors[_key][index] && Object.keys(errors[_key][index]).length < 1) delete errors[_key][index]
        }
      } else if (values[_key] instanceof Object) {
        // 値がオブジェクトの場合
        // 子要素を再帰的にバリデーション
        errors[_key] = traverse(values[_key], keys.slice(1), validations, errors[_key])
        // エラーが無かった場合は、エラーオブジェクトのkeyごと削除
        if (errors[_key] && Object.keys(errors[_key]).length < 1) delete errors[_key]
      } else {
        // 値がプリミティブの場合(入力値 / 未入力でオブジェクトが存在していない)
        errors[_key] = traverse(values[_key], keys.slice(1), validations, errors[_key])
        // エラーが無かった場合は、エラーオブジェクトのkeyごと削除
        if (errors[_key] && Object.keys(errors[_key]).length < 1) delete errors[_key]
      }
    }
  } else {
    // leaf nodeの場合
    errors = []
    // 対象フィールドのバリデーション設定に基づきバリデーションを行う
    for (const type in validations) {
      if (Validates[type] && !Validates[type](values, validations[type])) {
        const msg = ErrorMessages[type]
        if (msg) errors.push(msg)
      }
    }
  }

  return errors
}

export const ErrorMessages = {
  required: "必須項目です。",
  email: "Emailの形式が正しくありません。",
  password: "英字、数字を組み合わせた8文字以上で入力してください。",
}

export const Validates = {
  required: (value, prop) => {
    return prop ? value !== undefined && value !== null && value.toString().length > 0 : true
  },
  email: (value, prop) => {
    return /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(value)
  },
  password: (value, prop) => {
    return /^(?=.*?[a-zA-Z])(?=.*?\d)[a-zA-Z\d]{8,}$/.test(value)
  }
}

Form.jsを編集

validate.jsをimport

import {validate} from '../utils/Validate'
import React, { Component } from 'react'
import { Field, reduxForm } from 'redux-form'

import FieldInput from '../components/FieldInput'
import {validate} from '../utils/Validate'


class SubmitValidationForm extends Component {
  render() {
    const { error, handleSubmit } = this.props
    return (
      <form className="c-form" onSubmit={handleSubmit}>
        <Field name="email" type="email" component={FieldInput} label="email"/>
        <Field name="password" type="password" component={FieldInput} label="Password"/>
        {error && <strong>{error}</strong>}
        <div>
          <button className="c-btn c-btn-primary--flat" type="submit">Submit</button>
        </div>
      </form>
    )
  }
}

export const validations = {
  'email': {required: true, email: true},
  'password': {required: true, password: true}
}

export default reduxForm({
  form: 'submitValidation',
  validate: validate(validations)
})(SubmitValidationForm)

validationsについて

<Field name="email" type="email" component={FieldInput} label="email"/>
<Field name="password" type="password" component={FieldInput} label="Password"/>

Fieldのnameのところが下記のプロパティになっています。

export const validations = {
  'email': {required: true, email: true},
  'password': {required: true, password: true}
}

この部分でformのnameに合わせたプロパティ名でvalidateする内容を書きます。ちなみに、この辺も汎用的になるようにしていて例えば、

product: {
  name: 'hoehgoe',
  category: 'fuga',
  images: [
    {"filename":"1234.png"},
    {"filename":"2234.png"}
  ]
}

とかみたいに階層が深かったり、配列が混じっていてもvalidateできるようにしてあるはず・・・
なのでValidate.jsでは再帰的にvalidateしているのがわかるかと思います。

requiredのValidateするときの問題点

基本的にはForm要素に入力されてものがValidate.jsに渡ってくるようになっています。が、実は6系になってからなんですが、入力されていないと、nameすらわからないんです。なので、入力されるまで、emailpasswordの項目がわからないんですね。

なので、Submitボタン押されたときにrequiredのチェックができないんですよね。ここで使うしかないのが、validationsの項目です。

これね。

export const validations = {
  'email': {required: true, email: true},
  'password': {required: true, password: true}
}

この部分のプロパティでvalidationします。これでなんとかvalidateは無事解決!!
本当この辺はリリース前まで試行錯誤しましたねー。大変だったー

initialValuesについて

基本的にForm作る場合、特に管理画面系ですかね。その場合って新規作成画面と編集画面が必要だったりします。
新規作成画面であれば特に問題ないんですが、編集画面の場合は、新規作成時に入力された内容が要素に表示されていないとダメですよね。

業務でいえば、基本的にはAPIから受け取ったデータを表示させるって感じだと思います。その場合はinitialValuesを使うんですが、若干気をつけないといけないことがあります。

基本的にはFormを使うところで下記のような感じで書くとstateにあるデータがinitialValuesとして使うことができます。

let ProductGroupEditForm = reduxForm({
    form: 'ProductGroupEditForm',
    enableReinitialize: true,
    touchOnChange: true,
    validate: validate(validations),
  }
)(EditForm)

ProductGroupEditForm = connect(
  (state) => {
    return {
      initialValues: state.productGroups.productGroup.fields
    }
  }
)(ProductGroupEditForm)

export default connect(mapStateToProps, mapDispatchToProps)(ProductGroupEditForm)

ですが、ここで大事なのが

enableReinitialize: true,

これです。これがないと実は最初のrender時にstateが取れなくてうまくinitialValuesが表示されないんです。上記を記述することで再度取りに行ってくれます。

まとめ

・汎用的にvalidateしようとすると結構大変
・validateのrequiredは注意が必要
・initialValues周り

だいたいこの辺がはまりポイントかなと思っています。その点以外はバージョンアップされてからは特にデータの受け渡しとか、FieldのComponent周りとかすごい綺麗になったなという印象です。

こちらから完成例ダウンロードできます

オススメの本



WebデベロッパーのためのReact開発入門 JavaScript UIライブラリの基本と活用

イチからわかる!Reactの仕組みと使い方UIコードの再利用化と速度向上を図る!Reactのコンセプト、コンポーネント、JSX、活用テクニック、一歩進んだ使い方を解説!

イチマルニデザインブログをフォローしよう

イチマルニデザインブログではTwitterアカウントでWebに関する情報をつぶやいています。フォローすることで最新情報をすぐに受け取ることができます

同じカテゴリーの記事

ページの先頭へ