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のみ
なので、
- actionCreatorとreducerでフィルターの値をstore(state)に格納
- フィルターの値によってviewを変更(手動でフィルターを操作して動作確認)
- リンクをクリックしてフィルターを操作してviewを変更
の順番に開発していきます。
1. actionCreatorとreducerでフィルターの値をstore(state)に格納
actionCreatorの作成
actionCreatorはフィルター(SHOW_ALLとか)を受け取ってそれを返すだけです。
export const setVisibilityFilter = (filter) => {
return {
type: 'SET_VISIBILITY_FILTER',
filter
}
}
reducersの作成
reducersも単純で受け取ったactionのfilterを新しいstateとして
返します。なので、stateとしてfilterの値が格納されます。
todosとは別の変数として格納するので、新しくreducersを作っています。
初期値にはSHOW_ALLを与えています。
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'}
みたいな感じになります。
import visibilityFilter from './visibilityFilter'
const todoApp = combineReducers({
todos,
visibilityFilter
})
手動で動作確認
一応、正しく格納できるか確認します。
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です。
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を指定すると、完了しているもののみが表示されているはずです。
import { setVisibilityFilter } from './actions'
store.dispatch(setVisibilityFilter('SHOW_COMPLETED'))
3. リンクをクリックしてフィルターを操作してviewを変更
フィルターの値を格納することと、その値によって表示を変更することができたので、
手動ではなくリンクをクリックしてフィルターの値を変え、viewに反映するとこを作っていきます。
とりあえず、リンクを表示させる
まず、リンクを作ります。押しても何もできませんが。。
Linkコンポーネントは単なるリンクです。
props.childrenはコンポーネントの中身を取得できます。
Linkコンポーネントを使うときの、<Link>xxx</Link>
のxxxです。
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コンポーネントに表示します。
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
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です。
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に
値を入れておきます。
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コンテナを修正します。
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となります。
const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}
Linkコンポーネントではactiveの値を受け取り、trueの場合テキストを
falseの場合リンクを返すようにしています。
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」機能が完成しました。
- TodoをTodo Listに追加する「Add Todo」
- Todoの完了・未完了を切り替える「Toggle Todo」
- 表示するTodo Listを完了または未完了のTodoだけにする「Filter Todo」
の3つの機能が完成し、公式のExampleと同じ形になりました。
ここまでのソースコードはGitHubにあげています。
(といっても公式のExampleと同じですが)
また、Github Pagesにもあげています。
Redux ExampleのTodo Listは以上になります。
もうちょっと理解してきたらReddit APIも書きたいと思います。