redux-form の validation を関数合成使って見やすくしてみる
Tags: redux, javascript
Published: 2020-03-26 00:07:29

私は React + redux-form でフォーム画面を作ることが多いのですが、redux-form の validation がやや冗長になりやすい傾向にあります。

これを関数合成使って、見やすくメンテナンスしやすいコードに出来ないかなと思って試してみました。

※ 基本的な redux-form の知識が必要です。

# どのあたりが冗長なの？

例えば、このようなフォームを実装するとします。

<img width="616" alt="SnapCrab_NoName_2019-6-27_11-50-56_No-00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44638/743f49b8-b24b-b6da-8e75-fb6e2ba9e05a.png">

redux-form で validate 関数を定義していくと、こんな風に書くと思います。
(共通化などは一旦考えないで書いた場合)

```js
const validate = (values, props) => {
  const errors = {};
  if (!values.option_id) {
    errors.option_id = '必須入力です';
  }
  if (!_.trim(values.name)) {
    errors.name = '必須入力です';
  }
  if (!_.trim(values.email)) {
    errors.email = '必須入力です';
  } else if (!EMAIL_VALIDATION.test(values.email)) {
    errors.email = '正しいメールアドレスを入力して下さい';
  }
  if (
    values.phone_number &&
    !_.inRange(
      values.phone_number.length,
      PHONE_NUMBER_MIN_LENGTH,
      PHONE_NUMBER_MAX_LENGTH
    )
  ) {
    errors.phone_number = '携帯電話は10桁以上20桁以下で入力できます。';
  }
  if (values.birth_day && !BIRTHDAY_PATTERN.test(values.birth_day)) {
    errors.birth_day = '不正な値です';
  }
  if (calcAge(values.birth_day) < ADULT_AGE && !values.aggreement) {
    errors.aggreement = '未成年者は保護者の同意が必要です';
  }
  return errors;
};
```

綺麗なコードには見えませんが、これぐらいなら許容範囲かもしれません。
しかし、例えば性別や生年月日といった項目が追加されるとどうでしょうか。かなり長くなってしまいます。

また、名前やメールアドレスといった必須入力かどうかを判定するロジックは同じ仕組みなので共通化したいですね。

# どんな風に書けると嬉しい？

共通化するにあたって、どういう書き方ができると綺麗かなと考えてみました。
ここは個人的な思想なども入ると思うので一概に正しいとも言えませんが、今回は下記のように書けると良いかなと思います。

```js
const validate = (values, props) => {
  return validates(
    validateRequired('option_id'),
    validateRequired('name'),
    validateRequired('email'),
    validateEmail('email'),
    validatePhoneNumber('phone_number'),
    validateBirthday('birth_day'),
    validateUnderCondition(
      calcAge(values.birth_day) < ADULT_AGE,
      validateRequired('aggreement')
    ),
  )(values, {});
};
```

どうでしょうか？
それなりに見やすくなったと思います。

validates 関数は高階関数で、values と errors の初期値(ここでは空の Hash) をとり、列挙された関数を順次実行してくれる関数です。

validateRequired など関数は、redux-form の values が hash であることから、指定された key に対して validate を行う関数です。
validateRequired であれば、その key が存在するかどうかといったチェックですね。

validateUnderCondition のみ特別で引数を複数取ります。第二引数に validateRequired といった validation 関数を指定するのですが、第一引数にそれを実行する条件を指定できるようにしてます。
今回では、未成年の場合のみ同意が必要といった validation を表現するために使います。

# validates 関数

では、validates 関数を組んでいきます。

```js
export const validates = (...fns) => (...args) => {
  if (fns.length > 1) {
    return fns.reduce((prevFn, nextFn, index) => {
      return nextFn.apply(
        null,
        typeof prevFn === 'function' ? prevFn.apply(null, args) : prevFn
      );
    })[1];
  } else if (fns.length === 1) {
    return fns[0].apply(null, args)[1];
  } else {
    return args[1];
  }
};
```

高階関数にしつつ、どちらの引数もリストで取るようにしました。
fns は関数のリストで、args は values や errors の初期値といった前提条件となる値のリストです。

reduce を使って関数合成しつつも、apply で動的引数である args を適用しています。
また、fns が一つの場合でも実行できるようにしています。
複数の関数を合成するだけであれば不要ですが、項目が 1 つだけの form もあり得ると思うので条件分岐しています。

残念なところは、6 行目あたりで prevFn が function かどうかの判定が必要になってしまっているところです。
後述しますが、apply を使っているためか、prevFn が関数ではなく prefFn の返り値そのものになってしまってうまく関数合成ができませんでした。

redux-form 以外でも利用できるようにするために apply 使っていたのですが、redux-form の validation に限れば、args を動的引数で取る必要性はないので、apply を辞めるという手もあります。
もし、もっと良い方法ご存知の方は教えて欲しいです。

# 各 validation 関数

各々の validation 関数は特別なことはしていません。
全て列挙すると長くなるので代表的なものを載せます。

```js
export const validateRequired = key => (values, errors) => {
  if (!_.trim(values[key])) {
    errors[key] = '必須入力です';
  }
  return [values, errors];
};

export const validateUnderCondition = (condition, validateFunc) => (
  values,
  errors
) => {
  if (condition) {
    return validateFunc(values, errors);
  }
  return [values, errors];
};
```

特に難しいことはしていません。
redux-form の values が hash なので、高階関数の一つ目の引数に key を取って、それを使って値を割り出しています。
validation 関数に関しては、redux-form の構造にどっぷりっといった感じですが、values も errors もただの hash なので redux-form 以外でも使い回すことは出来そうです。

# まとめ

この記事ですが、実はあまり redux-form には関係なかったりします。
元は関数合成使って、もう少しうまく書けないかなぁと思ったのが、たまたま redux-form の validate だっただけです。
記事にするにあたって、もっと汎用的に直してから書こうかと思いましたが、むしろイメージしづらいのではと思って、そのまま redux-form の validate を題材にしました。
もちろん、ここにあげているバリデーション以外にも redux-form の FieldArray を使った場合の validation 関数等々、色々と定義はしていますが、validates 関数のおかげで集約しつつ簡単にフォーム側の validate を書けるので重用してます。

また、redux-form 以外でも各 validation 関数はもちろん利用しています。
ただ共通化しただけの関数ですからね。

残念な部分もあるので、もっと改善していきたいと思います。

(書いた後に思いましたが、記事的に validates 関数と validation 関数があって、何が何やらって感じですね。。。表現力磨きたい。