Redux Formを業務で使っていてわかった!使えるTips

このエントリーをはてなブックマークに追加

Redux Formを業務で使っていてわかった!使えるTips

業務でReactのサービスを運営・更新しているのですが、Form周り全般はRedux formを使っています。かなりの頻度でバージョンアップがされていて活気がある感じがします。しかも、6.0.*系の時よりも現在(2017/2/2)のバージョン6.5.0ではvalidationが使いやすくなりとってもいい感じです。今回はそんなRedux formのちょっとしたTipsをご紹介します。

Ads

基本的な使い方

以前にもRedux Formの記事を書いているので基本的な使い方はそちらでご確認ください。

validationのやり方

Redux Formのバージョンがアップされてvalidationのやり方が変わったようですね。上記にある記事の時にはFieldごとのvalidationができなかったのですが、
最新バージョンv6.5.0(おそらくもうちょっと前のバージョン)からViewのタグに対して個別にvalidateの設定ができるようになったようです。

JSX側

import * as validate from 'utils/Validate'

export class Form extends React.Component {

  // 省略

  render() {
    <form className="c-form" onSubmit={handleSubmit}>
    <div>
      <Field
        type="text"
        component={FieldInput}
        name="buyer.tel"
        errors={errors}
        placeholder="090-XXXX-XXXX"
        validate={[ validate.required, validate.tel ]}
      />
    </div>
    <div>
      <Field
        type="text"
        component={FieldInput}
        name="buyer.url"
        errors={errors}
        placeholder="URL"
        validate={[ validate.url, validate.maxLength(200) ]}
      />
    </div>
    </form>
  )
}

export function mapStateToProps(state) {
  // 省略
}

export function mapDispatchToProps(dispatch) {
  return bindActionCreators({
    ...{}, ...routeActions
  }, dispatch)
}

const SampleForm = reduxForm({
  form: 'form'
})(Form)

export default connect(mapStateToProps, mapDispatchToProps)(SampleForm)

utils/Validate.js

export const validate = () => {
  let errors = {}
  return errors
}

const ErrorMessages = {
  required: "必須項目です。",
  email: "Emailの形式が正しくありません。",
  num: "半角数字(小数不可)で入力して下さい。",
  password: "英字、数字を組み合わせた8文字以上、16文字以内で入力してください。",
  date: "2000-01-30の形式で入力してください。",
  minNumber: "数値が少なすぎます。",
  maxNumber: "数値が多すぎます。",
  decimal: "半角数字で入力して下さい。",
  tel: "電話番号は半角数字(-)で入力してください。",
  zip: "郵便番号は半角数字(-)で入力してください。",
  url: "URLの形式が間違っています。",
  zero: '発送しない場合は0を入力してください。'
}

const Regex = {
  email: /^[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])?)*$/,
  num: /^[0-9]+$/,
  password: /^(?=.*?[a-zA-Z])(?=.*?\d)[a-zA-Z\d]{8,}$/,
  date: /(\d{4}).?(\d{2}).?(\d{2}).*/,
  decimal: /^[0-9]+(\.[0-9]*)?$/,
  tel: /^\d{1,4}-\d{4}$|^\d{2,5}-\d{1,4}-\d{4}$/,
  zip: /^\d{3}[-]\d{4}$/,
  url: /^(https?)(:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)$/
}


export const required = value =>
  value ? undefined : ErrorMessages.required

export const zero = text => value => value !== '' ? undefined : `${text}しない場合は0を入力してください。`

export const email = value =>
  value && !Regex.email.test(value) ? ErrorMessages.email : undefined

export const password = value =>
  value && !Regex.password.test(value) ? ErrorMessages.password : undefined

export const url = value =>
  value && !Regex.url.test(value) ? ErrorMessages.url : undefined

export const zip = value =>
  value && !Regex.zip.test(value) ? ErrorMessages.zip : undefined

export const tel = value =>
  value && !Regex.tel.test(value) ? ErrorMessages.tel : undefined

export const decimal = value =>
  value && !Regex.decimal.test(value) ? ErrorMessages.decimal : undefined

export const date = value =>
  value && !Regex.date.test(value) ? ErrorMessages.date : undefined

export const num = value =>
  value && !Regex.num.test(value) ? ErrorMessages.num : undefined

export const minNumber = min => value =>
  value && value < min ? `${min}以上で入力してください。` : undefined

export const maxNumber = max => value =>
  value && value > max ? `${max}以下で入力してください。` : undefined

export const minLength = min => value =>
  value && value.length < min ? `${min}文字以上で入力してください。` : undefined

export const maxLength = max => value =>
  value && value.length > max ? `${max}文字以内で入力してください。` : undefined

validate={[ validate.required, validate.tel ]}

のようにvalidateする項目をFieldごとに呼び出して適用することができます。同じファイル内でvalidateの項目を作ってもいいのですが、共用的に使えるようにutilsなどのディレクトリに作ると良さそうです。ちなみに、引数で文字や数字などを渡すこともできるので何文字以上で入力なども汎用的に使えますね。以前よりもかなり使い勝手が良いvalidateになりました!!

normalizeのやり方

normalizeすることで、例えば入力するだけで電話番号の「-」を自動でつけることができたり、何文字以上は入力できないように制御したりと色々な使い方ができます。
Fieldに対してnormalize={条件}を記述するだけです。

<Field
  type="text"
  component={FieldInput}
  name="defaultDiscountRate"
  errors={errors}
  placeholder="7掛けだと0.7と入力してください"
  normalize={secondDecimal}
  validate={[ validate.required, validate.decimal, validate.minNumber(0.01), validate.maxNumber(1.0) ]}
/>

例えば以下の場合だと4文字以上入力しようとすると4文字目が消えるようにしたりできます。

export function secondDecimal(value){
  if (!value) {
    return value
  }
  if(value.length > 4){
    return value.slice(0, 3)
  }else{
    return value
  }
}

FieldArrayで要素を増やさずにvalueを作る方法

FieldArrayを使うと、

{
  "members": [
    {
      "firstName": "hoge1",
      "lastName": "fuga1"
    },
    {
      "firstName": "hoge2",
      "lastName": "fuga2"
    }
  ]
}

というように配列で要素を増やしていくことができます。ただ、Redux Form Exampleでもあるように入力欄がどんどん増えていってしまいますね。ただ、配列として送信するデータを作りたいけどUI的には入力欄を増やしたくない場合があったりします。

例えば、入力するごとにハッシュタグが増えていくようなUIの場合です。

実際のコード

renderHashTags({fields, errors}) {

  if(fields && fields.length === 0) {
    fields.push({tag: ''})
  }

  return (
    <tbody>
      <TableRow title="タグ付け(キーワード)">

        {fields.map((item, index) => (
          <Field
            key={index}
            component={Tag}
            name={`${item}.tag`}
            handleClick={() => {fields.remove(index); fields.push({tag: ''})}}
            classes="u-mb-16"
          />
        ))
        }
        <div className="c-grid c-grid--middle c-grid--gutters">
          <div className="c-grid__col c-grid__col--7of12">
            <div className="u-pos-r">
              <Field
                type="text"
                component={FieldSuggestInput}
                errors={errors}
                placeholder="タグを入力"
                name={`tagBrands[${fields.length - 1}].tag`}
              />
            </div>
          </div>
        <div className="c-grid__col c-grid__col--5of12">
          <button type="button" onClick={() => {
            fields.push({tag: ''})
          }} className='c-btn c-btn-default--flat'>さらに追加
          </button>
        </div>
      </div>
    </TableRow>
    </tbody>
  )
}

fieldsのlengthから配列の順番を割り出してあげればできちゃます。
うん、結構簡単!

`name={`tagBrands[${fields.length - 1}].tag`}`

dispatchとchangeについて

ある入力要素が入力されたら他の入力要素にも自動で何かが入力されるようにしたい時に便利です。
例えば契約開始年が入力されたら契約終了年はデフォルトで開始年の一年後になるようにしたい時とかですね。

export class Form extends React.Component {

  constructor(props) {
    super(props)
  }

  componentWillUpdate(nextProps) {
    const {dispatch, change, formValue} = this.props

    if (nextProps && nextProps.startYear !== formValue.values.startYear) {
      let nextYear = formValue.values.startYear + 1
      dispatch(change('endYear', nextYear))
    }
  }


  render() {

    // 省略

    return (
      <form className="c-form" onSubmit={handleSubmit(handleModalSubmit)}>
        <div className="c-grid c-grid--gutters">
          <div className="c-grid__col c-grid__col--6of12">
            <Field
              name="startYear"
              component={FieldSelect}
              data={selectYearArray()}
              errors={errors}
            />
          </div>
          <div className="c-grid__col c-grid__col--6of12">
            <Field
              name="endYear"
              component={FieldSelect}
              data={selectYearArray()}
              errors={errors}
            />
          </div>
        </div>
      </form>
    )
  }
}

// 省略

Reactのコンポーネントライフサイクルうまく使うことでいい感じにできます。
componentWillUpdateではPropsが更新されたら発動できるので、今回の場合ならstartYearが更新されたら発動ですね。
nextPropsで次のPropsが取得できるので、現在のPropsと比較をすることで条件に応じて何かすることができるようになります。

dispatch(change('endYear', nextYear))とすることでendYearonChangeされます。この手法は結構使えるのでおすすめです。郵便番号が入力されたら住所が自動で入るとかもこれの応用でできちゃいます。

componentWillUpdate(nextProps) {
  const {dispatch, change, formValue} = this.props

  if (nextProps && nextProps.startYear !== formValue.values.startYear) {
    let nextYear = formValue.values.startYear + 1
    dispatch(change('endYear', nextYear))
  }
}

同じformを登録と編集で使いわける方法

業務系の管理画面とか作っているとよくあるのが登録ページと編集ページです。Formの項目は全く同じなのに2ページ作るのってめんどくさいですよね。なので、登録ページを編集ページで同じformの要素を使い分ける方法です。

構成

ファイル自体は結構多いのですが、Form.jsを共有することができます。

- containers
|- Edit.js
|- Add.js
|- parts/
    |- Form.js
    |- AddForm.js
    |- EditFom.js

Edit.js
これはただ普通のViewです。EditFormを呼び出します。

class Edit extends React.Component {

  constructor(props) {
    super(props)
  }

  handleSubmit(data) {
    // 省略
  }

  render() {
    return (
      <div>
        <EditForm onSubmit={this.handleSubmit.bind(this)}/>
      </div>

    )
  }
}

function mapStateToProps(state) {
  return {
    // 省略
  }
}

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

export default connect(mapStateToProps, mapDispatchToProps)(Edit)

Add.js
これはただ普通のViewです。AddFormを呼び出します。

class Add extends React.Component {

  constructor(props) {
    super(props)
  }

  handleSubmit(data) {
    // 省略
  }

  render() {
    return (
      <div>
        <AddForm onSubmit={this.handleSubmit.bind(this)}/>
      </div>

    )
  }
}

function mapStateToProps(state) {
  return {
    // 省略
  }
}

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

export default connect(mapStateToProps, mapDispatchToProps)(Add)

parts/Form.js
これが共通で使えるFormです。

export class Form extends React.Component {

  constructor(props) {
    super(props)
  }

  render() {
    const {handleSubmit} = this.props

    return (
      <form className="c-form" onSubmit={handleSubmit}>
        <Field
          type="number"
          component={FieldInput}
          errors={errors}
          placeholder="価格"
          name="product.price"
          validate={[ validate.required ]}
        />

        // 他にもForm

      </form>
    )
  }
}

export function mapStateToProps(state) {
  return {
    // 省略
  }
}

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

handleSubmitはAdd.js、Edit.jsから渡されるメソッドです。

んで、次のとこが肝なんですが、編集と登録の大きな違いって編集の場合はすでに項目が入力されている状態じゃないとダメですよね。なので、EditFormではinitialValuesでstateから項目を呼び出すようにします。この辺に関してはAPI叩いてjson形式とかで受け取る感じになりますね。

あとはimport {Form, mapDispatchToProps, mapStateToProps} from './Form'とすることで共通のForm.jsのForm, mapDispatchToProps, mapStateToPropsconnectできます。

EditForm.js

import {reduxForm} from 'redux-form'
import {connect} from 'react-redux'
import {Form, mapDispatchToProps, mapStateToProps} from './Form'

class EditForm extends Form {
}

let SampleEditForm = reduxForm({
  form: 'sampleEditForm',
  enableReinitialize: true
})(EditForm)

SampleEditForm = connect(
  (state) => {
    return {
      initialValues: // stateから呼び出す
    }
  }
)(ProductEditForm)

export default connect(mapStateToProps, mapDispatchToProps)(SampleEditForm)

AddForm.js

import {reduxForm} from 'redux-form'
import {connect} from 'react-redux'
import {Form, mapDispatchToProps, mapStateToProps} from './Form'

class AddForm extends Form {
}


const SampleAddForm = reduxForm({
  form: 'sampleAddForm'
})(AddForm)

export default connect(mapStateToProps, mapDispatchToProps)(SampleAddForm)

これで同じFormを使いながらもinitialValuesの受け取りによって登録と編集ができるようになりますね!!

まとめ

いかがでしたでしょうか。もっと便利な使い方やテクニックがありそうですが、うちのサービスではこんな感じで使っています。こんな使い方おすすめだよ〜とかありましたら是非是非教えていただければと思います!!よろしくお願いします。

オススメの本



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

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

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

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

いいなと思ったらシェアお願いします

このエントリーをはてなブックマークに追加

同じカテゴリーの記事

Ads
ページの先頭へ