Redux モデル層でのフォームバリデーション

Reduxを使った実装で悩むのがフォームのバリデーションをどの層で行うかというところだと思います。
[Redux Form](https://redux-form.com/7.1.1/)を使えばバリデーションもついてきますが今回はこちらは使わない前提で考えていきます。

# 考えられるパターン
以下の4つの層のどこかで行うことが考えられます。

1. View
2. Action
3. Middleware
4. Model

それぞれ利点欠点あると思いますが今回は`4. Model` + `3. Middleware`での実装をやってみました。ミドルウェアには [redux-saga](https://github.com/redux-saga/redux-saga)を利用しました。 
そもそもReduxにモデルなんて出てこないわけですが、ここでいうモデルとは `Immutable.Record` を使った実装パターンでのモデルで、詳細は[こちら](https://tonyhb.gitbooks.io/redux-without-profanity/content/using_immutablejs_records.html)や[こちら](http://post.simplie.jp/posts/81)などを参照してください。


# モデル側の実装

まずモデルの実装を見ていきます。
通常の`Immutable.Record`を使ったモデルの実装はこのようになります。

```Post.js
import { Record, Map } from 'immutable';

const Post = new Record({
  id: undefined,
  title: '',
  content: '',
  author: undefined
  comments: new Map()
});

const post = new Post({ id: 1, title: 'title' });
```

この実装を少し拡張してバリデーション機能付きのクラスを返す`Entity`関数を実装します。

```Entity.js
import { Record } from 'immutable';

export default (defaultValue = {}, validators = {}) => {
  return class extends Record(defaultValue) {
    get validators() {
      return validators;
    }

    validate(key) {
      const validator = validators[key];
      if (!validator) {
        return null;
      }
      if (validator.isInvalid(this[key])) {
        return validator.message;
      }
      return null;
    }
  }
}
```

```Post.js
import { Map } from 'immutable';
import Entity from './Entity.js';
import StrLengthValidator from './StrLengthValidator.js';

const validators = {
  title: new StrLengthValidator(100),
  content: new StrLengthValidator(400)
};

const Post = Entity({
  id: undefined,
  title: '',
  content: '',
  author: undefined
  comments: new Map(),
  errorMessages: new Map()
}, validators);
```

`Entity`の第一引数は`Immutable.Record`に渡す引数と同様で、モデルの初期化パラメータです。
第二引数の`validators`はバリデーション対象のパラメータをKeyに実際にバリデーションを行うクラスのインスタンスをオブジェクト化したものです。

新たに追加したパラメータ`errorMessages`はエラーメッセージを保存しておくためのフィールドです。

バリデーションクラスは`isInvalid(value)`と`message`関数に応答する必要があります。
今回実装したのは文字数をチェックする`StrLengthValidator.js`ですが、前述の関数に応答さえすればどんなものでも利用できます。

```StrLengthValidator.js
export default class {
  constructor(limit) {
    this.limit = limit
  }

  isInvalid(value) {
    return value.length > this.limit
  }

  get message() {
    return '文字数オーバー'
  }
}

```
`post.title`は100文字、`post.content`は400文字を超えた状態で`post.validate(key)`関数を呼び出すと`文字数オーバー`というエラーメッセージが返ります。

# Reduxアプリケーションに組み込む
このモデルをReduxアプリケーションで利用します。
実際にバリデーションを行うのは、ユーザーがフォームへの入力を終えたという`action`が発行されたタイミングとなります。
このアクションを`Middleware`で拾いバリデーションを行い、更に新たなアクションを発行することになります。
詳しく見ていきましょう。

まずは`Reducer`です。

```reducer.js
import Post from '../models/Post.js';

export default (state = new Post(), action) => {
  switch (action.type) {
  case 'CHANGE_TITLE': {
    return state.set('title', action.value);
  }
  case 'CHANGE_CONTENT': {
    return state.set('content', action.value);
  }
  case 'ADD_ERROR': {
    return state.setIn(['errorMessages', action.key], action.error);
  }
  case 'DELETE_ERROR': {
    return state.deleteIn(['errorMessages', action.key]);
  }
  default:
    return state
  }
}
```
`CHANGE_TITLE`、`CHANGE_CONTENT`はごく一般的なもので、受け取った値でそのまま更新しています。
`ADD_ERROR`では`errorMessage`に受け取ったキーに値を追加し、`DELETE_ERROR`はその逆で`errorMessage`から受け取ったキーを削除しています。
`View`ではこの`errorMessage`にアクセスし、キーが存在する場合にメッセージを表示します。
`ADD_ERROR`と`DELETE_ERROR`はミドルウェアである`sagas`から発行されます。

```sagas.js
import { put, takeEvery, select } from 'redux-saga/effects'

export function* validateField(action) {
  const { key } = action;
  const post = yield select(state => state.post);

  const error = post.validate(key);

  if (error) {
    yield put({ type: 'ADD_ERROR', key, error });
  } else {
    yield put({ type: 'DELETE_ERROR', key });
  }
}
export default function* rootSaga() {
  yield takeEvery('VALIDATE_FIELD', validateField);
}
```
`VALIDATE_FIELD`アクションを全てキャッチしてバリデーションを実行し、エラーがあった場合は`ADD_ERROR`を、なかった場合は`DELETE_ERROR`を発行します。
※ `put`はアクションの発行、 `takeEvery`は指定したアクションが発行されると関数を別のsagaを実行します。 
また、`select`では`state`を取得できます。
`redux-saga`について詳しくは[こちら](https://qiita.com/kuy/items/716affc808ebb3e1e8ac)。

```action.js
export const changeTitle = value => ({ type: 'CHANGE_TITLE', value });

export const changeContent = value ({ type: 'CHANGE_CONTENT', value });

export const validateField = key => ({ type: 'VALIDATE_FIELD', key });
```

アクションクリエーターはシンプルな実装です。
フォームの`onChange`イベントで`changeTitle`や`changeContent`、`onBlur`で`validateField`を発火します。

# 所感
`Immutable.Record`を使いモデル層を実装することでビジネスロジックをモデルに集約することができます。
その利点を更に活用し、モデル層にバリデーションロジックも集約してしまおうというのが今回のアイディアです。
また、`StrLengthValidator`以外にも様々なバリデーターを個別に実装して組み込めるので柔軟なモデルを作成出来るようになりました。

悩ましいバリデーション実装の参考になれば幸いです。