Reduxの公式ExampleにあるTodo Listを機能ごとに作っていくシリーズの2回目です。
1回目では、TodoをTodo Listに追加する「Add Todo」を作りました。
今回は、Todoの完了・未完了を切り替える「Toggle Todo」の機能を作っていきます。
1回目を読んでない人は、そちらを先にどうぞ。
Redux ExampleのTodo Listをはじめからていねいに(1)
1. 完了・未完了を表すcompletedによってスタイルを変える
todoにcompleted要素を追加して、とりあえず取り消し線を表示する
まず、todoごとに完了・未完了を区別するために、completedという要素を
加えます。前回つくったtodo reducerを修正します。
todo作成時は、未完了なので、デフォルトでfalseにしておきます。
デフォルトでfalseにするので、actionからなにか受け取る必要はありませんので、
addTodoのactionCreatorは変更ありません。
const todo = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        id: action.id,
        text: action.text,
        completed: false
      }
    default:
      return state
  }
}
const todos = (state = [], action) => {
  // ...
}
export default todos
completedによってviewを変えるので、Todoコンポーネントを修正します。
completedがtrueだったらtextDecorationをline-throughにします。
const Todo = ({ completed, text }) => (
  <li style={{textDecoration: completed ? 'line-through' : 'none'}}>
    {text}
  </li>
)
Todo.propTypes = {
  completed: PropTypes.bool.isRequired,
  text: PropTypes.string.isRequired
}
これで、stateで保持されるtodoのcompletedがtrueのとき取り消し線がつきます。
動作確認は、一時的にreducers/todos.jsのcompleted: falseを
trueに変えてやればちゃんと取り消し線が付いているはずです。
actionCreatorからcompleted要素を操作する
次に、action経由で取り消し線のON/OFFを行うために、actionCreatorとreducerの作成を行います。
actionCreatorで必要なのは、todoのidだけです。
export const addTodo = (text) => {
  // ...
}
export const toggleTodo = (id) => {
  return {
    type: 'TOGGLE_TODO',
    id
  }
}
reducerはtodosとtodoの両方にtoggleTodoのactionが呼び出されたときの処理が必要です。
storeにはtodos reducerが登録されており、todoはtodosが呼び出されているに過ぎません。
todos reducerでは、map関数を使って現在のtodosに格納されているすべてのtodoを
todo reducerに渡しています。
todo reducerでは、actionCreatorに渡したidと一致するtodoに対して、
completedだけを反転させています。
Object.assignで現在のstateと、completedを書き換えたstateを結合しています。
const todo = (state, action) => {
  switch (action.type) {
    // ...
    case 'TOGGLE_TODO':
      if (state.id !== action.id) {
        return state
      }
      return Object.assign({}, state, {
        completed: !state.completed
      })
    // ...
  }
}
const todos = (state = [], action) => {
  switch (action.type) {
    // ...
    case 'TOGGLE_TODO':
      return state.map((t) =>
        todo(t, action)
      )
    // ...
  }
}
export default todos
index.jsからtoggleTodoを使ってみると、
正しく取り消し線が付いていると思います。
import { addTodo, toggleTodo } from './actions'
store.dispatch(addTodo('Hello React!'))
store.dispatch(toggleTodo(0))
2. クリックしてcompletedの値を変える
それでは、クリックしたときにcompletedの値を変更する処理を書いていきます。
stateをpropsとして使えるようにしたと同じように、dispatchをpropsとして
使えるようにします。
onTodoClickという名前でdispatchをstoreに格納します。
idを渡すと、dispatch(toggleTodo(id))のようにtoggleTodo actionCreatorで
actionをつくって、dispatchによりstoreのstateを変更します。
処理の流れは今までと同じです。
import { toggleTodo } from '../actions'
// ...
const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch(toggleTodo(id))
    }
  }
}
const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)
export default VisibleTodoList
TodoListコンテナでonTodoClickが使えるようになったので、Todoコンテナを
つくるところで、他のpropsと同じようにonTodoClick(todo.id)も渡します。
const TodoList = ({ todos, onTodoClick }) => (
  <ul>
    {todos.map((todo) =>
      <Todo
        key={todo.id}
        {...todo}
        onClick={() => onTodoClick(todo.id)}
      />
    )}
  </ul>
)
TodoList.propTypes = {
  // ...
  onTodoClick: PropTypes.func.isRequired
}
export default TodoList
TodoコンテナでTodoListから渡されたonClickを使います。
const Todo = ({ onClick, completed, text }) => (
  <li
    onClick={onClick}
    style={{textDecoration: completed ? 'line-through' : 'none'}}
  >
    {text}
  </li>
)
Todo.propTypes = {
  onClick: PropTypes.func.isRequired,
  // ...
}
export default Todo
これでクリックするとcompletedの値が変更され、取り消し線がON/OFFされます。
「Toggle Todo」機能が完成しました。
ここまでのソースコードはGitHubにあげています。
続きます。。
次回、表示するTodo Listを完了または未完了のTodoだけにする「Filter Todo」機能を実装します。
2016/3/15 update
Redux ExampleのTodo Listをはじめからていねいに(3)を書きました。
