Help us understand the problem. What is going on with this article?

Reduxでのクライアントサイドvalidationをどこでやるべきか?

More than 3 years have passed since last update.

入力フォームを利用するとやっぱり大事になってくるValidationについてあれこれ悩んだ。
結論が完全に自分の中でも出てないが、とりあえず考え尽くした所まで

前提など

validation

validationとひとくちに言っても色々考える事がある

  • 出力するエラーは一つ?複数?
  • エラーが出たフォームを赤くしたいとかある?
  • 複数の値を見てvalidationしたいとかある?
  • validateするタイミングは随時?ボタン押されたら?

今回の話

  • Redux + Reactを使う
  • 簡易なTodoリストを想定する
  • Actionの形式はFlux Standad Actionを使う
  • 一旦細かいことは脇に置きつつ、下記のvalidationを想定して実装してみる
    • エラーメッセージは一つ
    • Todoのinputが空だったらエラーとする
    • Todoの追加ボタンが押されたタイミングでvalidateする
  • redux-formredux-validatorなど、外部ライブラリが存在しているが、しっくり来るのが無かったので今回は一旦考えない
    • 個人的にこれだ!というものが無かったため

Todoリスト

シンプルにこんな感じ。
TODO.png

ベースとなるコードはこちらに配置したソース

validationについて

空白が入力されたらエラーを出す、みたいなことを考える
validate自体は、一旦Redux、Reactから切り離してシンプルな関数があることを想定する

validate.js
export const isValidTodo = (task) => {
  if(task === ""){
    return false
  }
  return true
}

また、エラーメッセージはreducerとして状態を持つことにする

reducer/error-message.js
const errorMessage = (state = null, action) => {
  switch(action.type) {
  case 'ERROR_MESSAGE':
    return action.payload
  }
  return state
}

実装パターン

案1:Smart Componentで行う

propsを受けるSmart Componentでvalidateを行うやり方。
送信ボタンが行われたタイミングでvalidationをかませる。

component.js
class InputComponent extends Component{
     :
  handleClick(e){
    let currentInput = this.state.currentInput
    // handle内でvalidate。
    // ダメなら値を入れない
    if(!isValidTodo(currentInput)){
      this.props.actions.errorMsg("Input some word")
      return
    }
    this.props.actions.appendTask(currentInput)
    this.setState({
      currentInput: ""
    })
  }
  render(){
        :
      <TodoInput
        onSend={this.handleClick.bind(this)} 
        value={this.state.currentInput}
        />
        :
  }
}
class TodoInput extends Component{
  render(){
    const {onChange, onSend, value} = this.props
    return (
      <div>
        <input onChange={onChange} value={value} onKeyPress={this.handleKeydown.bind(this)}/>
        <button onClick={onSend} >Append</button>
      </div>
    )
  }
}

寸感:まあ、悪くはない。

利点

  • 簡易なvalidationであればこれで十分事足りてる感。
  • 後で「エラーが出たらそのフォームを赤くしたい」とかやりたくなったら割りとやりやすそう
  • AとBの値を見てvalidateしたい。みたいな要望がある程度かなえられるかもしれない

欠点

  • そもそもView層ってコードが肥大化しがちな所なのでそこに押し込めるのって微妙・・・
  • エラーメッセージをReducerに流したいがために複雑度が上がっている感じがある

案2:Dumb Componentで行う

今回、前提としてreducerでエラーメッセージをやりとりすることを前提としたが、stateを利用することまで想定すれば、このぐらいまで出来そう

  handleClick(e){
    let currentInput = this.state.currentInput

    // validationエラーをstateでやる
    if(!isValidTodo(currentInput)){
      this.setState({
        errorMessage: "Input some word"
      })
      return
    }
    this.props.actions.appendTask(currentInput)
    this.setState({
      currentInput: ""
    })
  }

寸感:これはナシかな・・・

利点

  • 正直あんまりない・・・
  • flux使ってないほど簡易で良いならこれでも良いかもしれない

欠点

  • そもそもstate使うのあんまりやりたくない感じ
  • AとBの値でvalidateしたい〜とかやりだすとstate肥大化の予感。

案3:Actionで行う

次にviewの一個奥のactionでやることを考える

export const appendTask = (task) => {
  // エラーかどうかで返すactionを変える
  if(!isValidTodo(task)){
    return {type: "ERROR_MESSAGE", payload: "Input Some word"}
  }
  return {type: "TODO_ADD", payload: task}
}

寸感:良さそう

利点

  • 一番まとまりが良さそう
  • viewでやるよりも責務としては正しい気がする
  • コンパクトに収まる
  • サーバーサイドvalidationとかも透過的に扱えるかも

欠点

  • task追加のactionを投げて別なactionが発火されるのは場合によっては驚きが大きいかも
  • これもAとBの値を見つつCのvalidateしたいとかなったら辛い気がする。
    • metaを利用して他のstoreの値も受け取れるようにしておけばギリ可能かもしれない・・・
  • (良し悪しだが)エラー出た表示を赤くしたい、とかなるとHOGEHOGE_VALIDATE_ERRORみたいなaction typeが膨大になってしまうかも。

欄外:Actionでリアルタイムvalidation的な事やりたかったら?

上記のやり方、当然だが「片方のactionをキャンセルする」というやり方で、即時validationみたいなことをしたかった時にこまるのでちょっと考えてみた。

component.js
class InputComponent extends Component{
    :
  handleClick(e){
       :
    this.props.actions.appendTask(currentInput)
    // validation用にもactionを飛ばす
    this.props.actions.checkTaskError(currentInput)
       :
actions.js
// 値は値でreducerに返す
export const appendTask = (task) => {
  return {type: "TODO_ADD", payload: task}
}

// 別途、エラーがあれば、そちらもactionを生成する
export const checkTaskError = (task) => {
  if(!isValidTodo(task)){
    return {type: "ERROR_MESSAGE", payload: "Input Some word"}
  }
  return {type: "ERROR_MESSAGE", payload: ""}
}

考えとして複数の作用をさせるのだから複数のactionを発火させるというのは悪く無い気がするが、多分もうちょい色々考えたほうが良さそうな感じもしている。。。

案4:Reducerで行う

reducerでやるとしたパターンを考えると、
複数のreducerで同じaction.typeを処理する必要が出そうだ。

reducer/tasks.js
const tasks = (state = [], action) => {
  switch (action.type) {
  case 'TODO_ADD':
    let task = action.payload
    // validate通った時だけpushする
    if(isValidTodo(task)){
      state.push(task)
    }
    return state.concat()
  case 'TODO_REMOVE':
     :

単に「データを入れないようにしたい」というだけであれば、taskのreducerで上記ようにpushを抑制すればいいが、エラーメッセージまで出したいなら下記まで必要だろう

error-message.js
const errorMessage = (state = null, action) => {
  switch(action.type) {
    case "TODO_ADD":
    if(!isValidTodo(action.payload)){
      return "Invalid Task"
    }
  }
  return state
}

寸感:とても微妙・・・
あとReducerでやるにしてももっと別なやり方ありそうな気がしてくる・・・

利点

  • 「値をどう扱うべきか」という事の責務をreducerが担うのはただしそう
  • エラーメッセージのことを考えなければナシとはいえない。

欠点

  • エラーメッセージのことを考えた途端結構微妙な感じになる。
  • 1action typeに対して複数箇所での処理ってどうなのか?
    • 保守性悪そう・・・
  • 多分そうでなくても、複雑なvalidationになったら途端に副作用起きるReducerが出来上がる危なさありそう

案5:Middlewareで行う

middleware.js
const doValidate = (action) => {
  let errors = []
  switch(action.type){
    case "TODO_ADD":
      if(!isValidTodo(action.payload)){
        return "Invalid Task"
      }
  }
}

export const validateMiddleware = ({ dispatch, getStore }) => next => action => {
  let error = doValidate(action)
  if(error){
    // errorがあったらerrorMsgのactionだけ発火させる
    dispatch(actions.errorMsg(error))
    return
  }
  let result = next(action)
  return result
}

寸感:アリかもしれない

利点

  • 実装としては素直な感じ
  • getStoreで全ての値が取れるのである程度複雑なvalidationでも出来そう
  • validateの処理が一箇所に収まる

欠点

  • なんかreducerでやってることをやっている感じ・・・
  • Middlewareもちょっと慣れるまで学習コスト高めかも?

案6:reselector(selector)で行う(2016/12/15追加)

詳しい実装に関しては有益な記事があったので追記

validationとはstateのある状態がvalidな状態なのかどうかを計算することに他なりません。

案3や案5の場合、冗長にstateが増えてしまう所を、上記のように「validateはあくまでstateのCompouted propsである」という発想のもと扱うことで、無駄にreducerやstateを増やすことを防止出来る。

発想としては案1のSmart Componentで行うのに近い。
Smart Componentよりもっと手前のselectorのレイヤーで行うようなイメージになるだろう。
また、案1はComponentが責務としている値でしかvalidateしづらかったのに対してstate全体の値でvalidationしやすくなることも良い部分だろうと思われる。

ただし、注意点として、この手法を取る場合、「reducerを通してstoreに入ったデータしかvalidate出来ない」という問題がある。validでないデータをstoreにも入れたくないというケースの場合、validateのロジックを切り出して、その値をComponentとselectorがどちらからも呼べるようにするなどの工夫が必要そうだ。

まとめ

  • 一定以上複雑なことになってきたら Middleware でのやり方に移植するとかは検討しても良いかもしれない
    • とはいえぶっちゃけそこまで複雑なパターンも無い(または回避出来る)気もしている。
    • 複雑なvalidationは、必要以上にクライアントサイドではやらないでサーバーサイドにまかせてしまうという割り切りも必要かも
  • Smart Componentsでやるのは簡易ならよさそうだが、ごく1部にのみ使う場合かもしれない。とはいえ使いどころはありそう
  • 案2 Dumb Component, 案4 Reducerはバッドパターンな気がする
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした