LoginSignup
29
16

More than 5 years have passed since last update.

Redux ExampleのTodo Listをはじめからていねいに(3)

Posted at

Reduxの公式ExampleにあるTodo Listを機能ごとに作っていくシリーズ最終回です。

1回目では、TodoをTodo Listに追加する「Add Todo」を作りました。
2回目では、Todoの完了・未完了を切り替える「Toggle Todo」の機能を作りました。
3回目の今回は、表示するTodo Listを完了または未完了のTodoだけにする「Filter Todo」機能を作りたいと思います。

1,2回目を読んでない人は、そちらを先にどうぞ。

機能開発の際、どこから手をつけるかは悩ましいところですが、私はactionから作ることが多いです。
だいたいactionCreatorとreducerの関数を作って、手動で(index.jsから)動いているの確認してから、
stateもしくはdispatchをpropとしてラップするcomponents(containers)を作るって感じにしています。

また、「Filter Todo」では、以下の3つのフィルターによって表示を変更する機能です。

  • SHOW_ALL: 全部表示
  • SHOW_COMPLETED: 完了しているtodoのみ
  • SHOW_ACTIVE: 完了していないtodoのみ

なので、

  1. actionCreatorとreducerでフィルターの値をstore(state)に格納
  2. フィルターの値によってviewを変更(手動でフィルターを操作して動作確認)
  3. リンクをクリックしてフィルターを操作してviewを変更

の順番に開発していきます。

1. actionCreatorとreducerでフィルターの値をstore(state)に格納

actionCreatorの作成

actionCreatorはフィルター(SHOW_ALLとか)を受け取ってそれを返すだけです。

actions/index.js
export const setVisibilityFilter = (filter) => {
  return {
    type: 'SET_VISIBILITY_FILTER',
    filter
  }
}

reducersの作成

reducersも単純で受け取ったactionのfilterを新しいstateとして
返します。なので、stateとしてfilterの値が格納されます。

todosとは別の変数として格納するので、新しくreducersを作っています。
初期値にはSHOW_ALLを与えています。

reducers/visibilityFilter.js
const visibilityFilter = (state = 'SHOW_ALL', action) => {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }
}

export default visibilityFilter

ファイルも分けていますので、reducers/index.jsに登録が必要です。
最終的なstoreの値は{todos: [todo1, todo2], visibilityFilter: 'SHOW_ALL'}みたいな感じになります。

reducers/index.js
import visibilityFilter from './visibilityFilter'
const todoApp = combineReducers({
  todos,
  visibilityFilter
})

手動で動作確認

一応、正しく格納できるか確認します。

index.js
import { setVisibilityFilter } from './actions'

console.log(store.getState()) // => Object {todos: Array[0], visibilityFilter: "SHOW_ALL"}
store.dispatch(setVisibilityFilter('SHOW_COMPLETED'))
console.log(store.getState()) // => Object {todos: Array[0], visibilityFilter: "SHOW_COMPLETED"}

storeの初期値としてSHOW_ALLが格納されており、setVisibilityFilterによって、
値が変更されていることが分かります。

2. フィルターの値によってviewを変更(手動でフィルターを操作して動作確認)

actionとreducerが完成し、storeにフィルターの値を格納できることができたので、
その値でviewを変更してみます。

VisibleTodoListコンテナの修正

todoリストを表示しているのはTodoListコンポーネントですが、表示するtodoリストを
propsとして渡しているのはVisibleTodoListコンテナになります。

これまでは以下のコードで、格納されたtodosをそのまま渡していました。

const mapStateToProps = (state) => {
  return { todos: state.todos }
}

これをフィルターの値によって変更するよう修正します。
getVisibleTodos関数によって、フィルターに合致するtodosを返しています。

同じ名前でややこしいですが、todos.filterのfilterは文字列ではなく
配列のメソッドのfilterです。

containers/VisibleTodoList.js
const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter((t) => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter((t) => !t.completed)
  }
}

const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

手動で動作確認

先ほどと同じように動作確認をします。
SHOW_COMPLETEDを指定すると、完了しているもののみが表示されているはずです。

index.js
import { setVisibilityFilter } from './actions'
store.dispatch(setVisibilityFilter('SHOW_COMPLETED'))

3. リンクをクリックしてフィルターを操作してviewを変更

フィルターの値を格納することと、その値によって表示を変更することができたので、
手動ではなくリンクをクリックしてフィルターの値を変え、viewに反映するとこを作っていきます。

とりあえず、リンクを表示させる

まず、リンクを作ります。押しても何もできませんが。。
Linkコンポーネントは単なるリンクです。

props.childrenはコンポーネントの中身を取得できます。
Linkコンポーネントを使うときの、<Link>xxx</Link>のxxxです。

components/Link.js
import React, { PropTypes } from 'react'

const Link = ({ children, onClick }) => (
  <a href="#">{children}</a>
)

Link.propTypes = {
  children: PropTypes.node.isRequired
}

export default Link

LinkコンポーネントはFooterコンポーネントで表示し、
FooterコンポーネントはAppコンポーネントに表示します。

components/Footer.js
import React from 'react'
import Link from './Link'

const Footer = () => (
  <p>
    Show:
    {" "}
    <Link>
      All
    </Link>
    {", "}
    <Link>
      Active
    </Link>
    {", "}
    <Link>
      Completed
    </Link>
  </p>
)

export default Footer
components/App.js
import Footer from './Footer'

const App = () => (
  <div>
    <AddTodo />
    <VisibleTodoList />
    <Footer />
  </div>
)

これで、とりあえずリンクが表示されました。

本当に単なるリンク(外部リンクとか)を作りたいだけならLinkコンポーネントなんか
つくらず、<a href="#">All</a>とかをFooterコンポーネントに書けばできます。

これからLinkコンポーネントでいろいろつくっていくので分けています。

Linkコンテナでdispatch(setVisibilityFilter())を呼び出せるようにする

それでは、リンクをクリックしたときのフィルターの値を変える機能を作ります。

これは、クリックしたときにdispatch(setVisibilityFilter())を呼び出すことができればよく、
これはToggle Todoと同じく、connectをつかってpropsにdispatchを渡すことで実現できます。

connectの引数は、connect([mapStateToProps], [mapDispatchToProps])であるため、
mapDispatchToPropsだけを使うときでも、mapStateToPropsが必要になります。
またmapStateToPropsは、objectを返す関数でないとだめなので、とりあえずstateをそのまま返します。
(もっといい方法があるかもしれませんが、とりあえずは問題なく動きます。)

また、mapDispatchToProps、mapStateToPropsの引数はそれぞれ

  • mapDispatchToProps(dispatch, [ownProps])
  • mapStateToProps(state, [ownProps])

となっており、propsとして渡すだけでなく、自身のpropsを使うこともできます。
このあたりはreact-reduxのapiのドキュメントに詳しく書いてあります。

FilterLinkコンテナのmapDispatchToPropsでは、propsとしてonClick関数を渡しており、
この関数は、自身のpropsのフィルターの値をsetVisibilityFilter関数に渡しています。

これでLinkコンポーネントでonClick関数を呼び出せばOKです。

containers/FilterLink.js
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'

const mapStateToProps = (state, ownProps) => {
  return { state: state }
}

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    onClick: () => {
      dispatch(setVisibilityFilter(ownProps.filter))
    }
  }
}

const FilterLink = connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)

export default FilterLink

先程は、FooterコンポーネントをLinkコンポーネントを使って書いていましたが、
FilterLinkコンテナとしてconnectしたので、こちらを使います。

また、onClick関数にフィルターの値を渡すため、それぞれprops.filterに
値を入れておきます。

components/Footer.js
import FilterLink from '../containers/FilterLink'

const Footer = () => (
  <p>
    Show:
    {" "}
    <FilterLink filter="SHOW_ALL">
      All
    </FilterLink>
    {", "}
    <FilterLink filter="SHOW_ACTIVE">
      Active
    </FilterLink>
    {", "}
    <FilterLink filter="SHOW_COMPLETED">
      Completed
    </FilterLink>
  </p>
)

これでLinkコンテナでonClick関数が使えるようになり、dispatch(setVisibilityFilter())を呼び出すことが
できるようになりました。

クリックしたときにonClickを呼ぶ

LinkコンテナでonClick関数が使えるようになったので、クリックしたときにonClick関数を呼んでみます。
単なるリンクを表示していたLinkコンテナを修正します。

components/Link.js
const Link = ({ children, onClick }) => (
  <a href="#"
    onClick={(e) => {
      e.preventDefault()
      onClick()
    }}
  >
    {children}
  </a>
)

Link.propTypes = {
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func.isRequired
}

これでリンクを押すとonClick()が呼び出され、その中でdispatch(setVisibilityFilter())が呼び出され、storeに格納されているフィルターの値が更新され、その値にしたがってviewが書き換わるという一連の流れが実装できました。

activeな状態なリンクを押せないようにする

最後にactiveな状態のリンクを押せないようにする。

これは現在の状態がSHOW_ALLだったらALLとうリンクを押せないようにします。
押せないようにするというか、リンクではなくテキストを表示するように変更します。

Linkコンポーネントが現在の状態を知るために、props.activeとして値を渡します。
現在のフィルターの値(state.visibilityFilter)をそのまま渡してもできますが、
自身のpropsの値と比較してture/falseで渡しています。

現在の状態がSHOW_ALLの場合、では、
ownProps.filterもSHOW_ALLとなるので、trueとなりそれ以外はfalseとなります。

containers/FilterLink.js
const mapStateToProps = (state, ownProps) => {
  return {
    active: ownProps.filter === state.visibilityFilter
  }
}

Linkコンポーネントではactiveの値を受け取り、trueの場合テキストを
falseの場合リンクを返すようにしています。

components/Link.js
import React, { PropTypes } from 'react'

const Link = ({ active, children, onClick }) => {
  if (active) {
    return <span>{children}</span>
  }

  return (
    <a href="#"
    // ...
    </a>
  )
}

Link.propTypes = {
  active: PropTypes.bool.isRequired,
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func.isRequired
}

これでactiveな状態のリンクを押せないようになり、「Filter Todo」機能が完成しました。

  1. TodoをTodo Listに追加する「Add Todo」
  2. Todoの完了・未完了を切り替える「Toggle Todo」
  3. 表示するTodo Listを完了または未完了のTodoだけにする「Filter Todo」 の3つの機能が完成し、公式のExampleと同じ形になりました。

redux-todos.gif

ここまでのソースコードはGitHubにあげています。
(といっても公式のExampleと同じですが)
また、Github Pagesにもあげています。

Redux ExampleのTodo Listは以上になります。
もうちょっと理解してきたらReddit APIも書きたいと思います。

29
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
16