はじめに
Reactを本格的に実装するためには,Reduxという代物を使うと良いらしいということを知り,実装したくてRedux調べてたけど一向に理解できない。。。
ということで,とりあえず公式ドキュメントを読み解くことにしました。
ちなみに,公式ドキュメントのBasicsの章のはじめにこうあります。
Don't be fooled by all the fancy talk about reducers, middleware, store enhancers—Redux is incredibly simple. If you've ever built a Flux application, you will feel right at home. If you're new to Flux, it's easy too!
少なくとも今のところ,そうは感じられない。。。(能力不足)
ということで目標は以下。
目標
ReduxドキュメントのBasicsを一通り理解する
自分なりの解釈が間違ってるところとか出るかもしれないので悪しからず。。。
Basics
ドキュメントのBasicsの章では,シンプルなToDoアプリを題材に,
- Actions
- Reducers
- Store
- Data Flow
- Usage with React
- Example: Todo List
の説明がされています。今回は1-5までをまとめようと思います。
Actions
Actionとは,アプリからStoreに対して送るデータのこと。
新しいtodoの項目を追加するためのアクションは,
const ADD_TODO = 'ADD_TODO'
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
という感じで書けます。ActionはJavaScriptの一般的なオブジェクトで,typeは必須プロパティ。
Actionの構造は好きにして定義して良いらしいです。ただ,Actionの中身(データ)はできるだけ小さい方が良くて,例えば,todoを完了するためのトグル用のActionは
{
type: TOGGLE_TODO,
index: 5
}
こんな感じで,todoオブジェクト自体を入れるのではなくて, index
だけを入れたりして小さくなるようにしたほうが良い。
todoリストの表示非表示を制御するためのactionは,
{
type: SET_VISIBILITY_FILTER,
filter: SHOW_COMPLETED
}
こうなります。
Action Creators
Action Creatorとは,文字通りactionを生成する関数のこと。
ActionとAction Creatorは混同しやすいので気をつけましょう。
Action Creatorは次のようにActionを返すだけのシンプルな関数にします。
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
ちなみに,Fluxでは次のようにAction Creator中でdispatchすることがよくあったらしい。
function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text
}
dispatch(action)
}
でも,Reduxではこんな書き方はせずに,dispatch()
関数にActionの結果を渡すようにします。
dispatch(addTodo(text))
dispatch(completeTodo(index))
これをラップしてBound Action Creatorを作ることもできます。
const boundAddTodo = text => dispatch(addTodo(text))
const boundCompleteTodo = index => dispatch(completeTodo(index))
こうすると,
boundAddTodo(text)
boundCompleteTodo(index)
こんな感じで直接dispatchすることができます。
dispatch()
関数はstore.dispatch()
としてstoreから直接触ることもできるけど,react-reduxのconnect()
みたいなhelperを使ってアクセスするのが普通のようです。
bindActionCreators()を使って,自動的にAction creatorをdispatch()
関数に接続することもできます。これは,Action Creatorが多くなったときに便利らしいですが,Advancedな用法ということなので,今回は割愛します。
Reducers
Actionからは何が起きたのかという事実を知ることができます。ですが,アプリの状態(State)がどのように変化するのかということはわかりません。これを担うのが,Reducerです。
Stateの型を設計する
Reduxでは,全てのアプリの状態をひとつのオブジェクトとして保持します。この仕様のおかげで,コードを書く前に状態の持ち方を考えやすくなっているそう。
例えば,todoアプリの例だと,次の2種類の状態を保持したい。
- 表示/非表示のフィルタ
- todoリスト
UIの状態から保持するデータを決めがちですが,UIの状態とデータは出来るだけ分けて考えたほうが良いようです。
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
補足:
Stateはネストせず,出来るだけ正規化したほうが良いです。ストアする全てのオブジェクトはkeyとしてIDを持つようにして,他のエンティティからはできるだけIDを参照するようにします。アプリのstateはDBのように考えると良いようです。
Actionの操作
ここまでで,オブジェクトのStateが決まったので,Reducerを書きます。
Reducerは,前のStateとActionを持ち,次のStateを返す関数で,Actionの操作を司ります。
最初に,Reducerでしてはいけないことが記述されていました。
- 入力値の変更
- API呼び出しやルーティングの移行などの副作用を伴う操作
- ピュアじゃない関数の呼び出し(例:
Date.now()
やMath.random()
)
要するに,同じ入力が与えられたときに,次のStateを返すだけに留めるべきで,副作用が起きるようなAPIコールや,入力値の変更などはするべきではないということのようです。
Reduxは最初にStateをundefined
としてReducerをコールします。このときに,Stateの初期化を行うようにします。
import { VisibilityFilters } from './actions'
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
}
function todoApp(state = initialState, action) {
// アクションの操作はしない
// 与えられたStateをただ返す
return state
}
次に,SET_VISIBILITY_FILTER
を操作してみます。ここではvisibilityFilter
を変更するだけ。
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
default:
return state
}
}
ここで,
-
state
を変更していないこと - デフォルト値として前のstateを返していること
に注意!Object.assign()
のところは,オブジェクトの展開を使って{ ...state, ...newState}
というようにも書くこともできます。
複数のActionを操作する
複数Actionの操作の例は,
import {
ADD_TODO,
TOGGLE_TODO,
SET_VISIBILITY_FILTER,
VisibilityFilters
} from './actions'
...
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
})
default:
return state
}
}
となります。ここでは,ADD_TODO
とTOGGLE_TODO
をimportして, ReducerにはADD_TODO
の操作を記述しています。
ここでも,state
やそのフィールドを直接書かずに,代わりに新しいオブジェクトを返しています。この新しいオブジェクトは,古いtodos
オブジェクトに新しいtodos
を加えたオブジェクトになっています。
新しいtodoはActionのデータを使って,追加されていますね。
最後に,TOGGLE_TODO
の操作の実装です。
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: state.todos.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
})
既存のActionを変更せずに,配列の特定の項目を更新したいので,該当するindexの項目以外の同じ項目の新しい配列を生成しています。
もし,この操作を頻繁に書くようであれば,immutablitiy-helper,updeepのようなヘルパーや,Immutableのようなライブラリを使うと良いよいです。この辺りはとりあえず割愛します。
また,クローンするまえにstate
の中身に何かを勝手に割り当てるとかそういうことはしてはいけません。あくまでクローンしたオブジェクトに関して変更を加えます。
Reducerの分割
まず,Reducerのコードの全容を。
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
})
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: state.todos.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
})
default:
return state
}
}
todos
やvisibilityFilter
は完全に独立して更新されています。
往々にして,Stateフィールドは他のフィールドに依存していたりして,考慮が必要だったりするようです。
ただ,今回の場合は簡単にtodos
を別関数に分割することができます。
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
case ADD_TODO:
return Object.assign({}, state, {
todos: todos(state.todos, action)
})
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: todos(state.todos, action)
})
default:
return state
}
}
todos
もstate
を受け取っていますが,これは配列であることに注意!
今,todoApp
はtodos
にstateの一部分を渡し,todos
がそれを更新します。
これは,reducer compositioin と呼ばれ,Reduxアプリの基本的なパターン のようです。
要は,共通するオブジェクトを返す部分は,それだけで抜き出して分割できるってことですね。
次に,visibilityFilter
を管理するReducerの抜き出し方を見てみます。
まず, SHOW_ALL
を宣言します。
const { SHOW_ALL } = VisibilityFilters
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
メインのReducerを,Stateを部分的に管理するReducer達を呼んで,ひとつのオブジェクトで結びつける関数として書き換えることができます。
完全な初期状態を知る必要はもはやなく,子Reducerは最初,undefined
を与えられたときの初期状態(default
の返り値)を返せば良くなります。
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}
これらのReducerはグローバル状態の一部だけを管理していることに注意してください。各関数に渡されるstate
はReducer毎に違います。
このようにすると,アプリが大きくなったときにReducerを別ファイルに分割し,各ファイルを完全に独立させて違うデータ領域を管理することができます。
ReduxはcombineReducers()
というユーティリティを提供していて,これは,上記のtodoAppと同じ定型ロジックを実行します。そのため,todoApp
はcombineReducers()
を使って,書き直すことができます。
import { combineReducers } from 'redux'
const todoApp = combineReducers({
visibilityFilter,
todos
})
export default todoApp
combineReducers()
を使わずに,次のようにも書けます。
export default function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}
また,違うキーを使ったり,違う関数を呼ぶことも可能です。この2つのcombinedReducerの書き方は同じ動作をします。
const reducer = combineReducers({
a: doSomethingWithA,
b: processB,
c: c
})
function reducer(state = {}, action) {
return {
a: doSomethingWithA(state.a, action),
b: processB(state.b, action),
c: c(state.c, action)
}
}
上の例のcombineReducers()
は全て,キーに対応して選択されたstateの一部を伴うReducerを読んで,その結果をひとつのオブジェクトに結合しています。
combineReducers()
は,Reducerがstateを変更しない限り,新しいオブジェクトを生成しません。
個人的には,combineReducers()
を使うほうが見やすいし理解しやすいと思います。他の方の記事を見ていてもcombineReducers()
を使う例が多く,これを使うのが自然な気がしています。
Store
ここまでで,
- 何が起きるのかついての事実を表現するAction
- Actionに応じてstateを更新するReducer
を定義しました。
Storeは上記2つをつなげるオブジェクトになります。Storeは以下のような役割を担っています。
- アプリのstateを保持する
-
getState()
でstateへのアクセスを許可する -
dispatch()
でstateを更新することを許可する -
subscribe(listener)
でリスナーを登録する -
subscribe(listener)
で返される関数を通して,リスナーの登録解除を行なう
Reduxアプリ内ではただひとつのStoreを持つということは重要な注意事項なので忘れないようにしましょう!
データ操作のロジックを分割したいときには,Storeをたくさん使う代わりに,reducer compositionを使って分割します。
Reducerを既に準備していたら,storeを作るのは簡単です。
combineReducers()
でまとめたReducerをimportして,createStore()
に渡すだけです。
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)
第2引数で初期状態を設定することも可能です。
これはサーバ上で実行しているReduxアプリのstateとクライアントのstateを一致させるのに役立ったりするようです。
let store = createStore(todoApp, window.STATE_FROM_SERVER)
とりあえずこういうのもあるよ。ということですね。必要になったら再度調べよう。。。
Actionをdispatchする
Storeを生成したら,プログラムが動くことを確認してみましょう。実行の仕方は以下のようにします。UIがなくても,更新ロジックをテストすることができます。
import {
addTodo,
toggleTodo,
setVisibilityFilter,
VisibilityFilters
} from './actions'
// Log the initial state
console.log(store.getState())
// Every time the state changes, log it
// Note that subscribe() returns a function for unregistering the listener
const unsubscribe = store.subscribe(() =>
console.log(store.getState())
)
// Dispatch some actions
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))
// Stop listening to state updates
unsubscribe()
Reduxを使うと,全てがピュアな関数で表現されることになるので,UIを書くことなくアプリの挙動を確認できます。だから,テストも書きやすいんですね。ただ関数をコールして,アウトプットを確認するだけ。いいですね。
Data Flow
Reduxアーキテクチャは、単一方向の厳密なデータフローを中心に展開されます。
どういうことかというと,アプリ内のデータは全て,同じライフサイクルのパターンに従うので,アプリのロジックはより理解しやすいものになります。予想外のことをしたりすることはできないので,結果的にわかりやすくなるはずであるということのようです。
また,こうすることで,データの正規化が促進され,相互に認識されない同一データのコピーが作成されることはなくなります。
つまりは,単一方向のデータフローにすることで,データフローを追いやすく,不要なデータのコピーとかが作成されにくくなっているようです。
Reduxアプリのデータライフサイクルは以下の4つのステップに従います。
1. store.dispatch(action)
を呼ぶ
Actionは何が起こったのか,だけを説明する一般的なオブジェクトです。Storeに対するdispatch()はアプリ内のどこからでも呼び出し可能になります。
{ type: 'LIKE_ARTICLE', articleId: 42 }
{ type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } }
{ type: 'ADD_TODO', text: 'Read the Redux docs.'
2. Reduxのstoreは,reducer関数を呼ぶ
Storeは,現在のStateツリーとActionの2つをReducerに渡します。例えば,todoアプリにおいて,Root reducerは次のようになります。
// The current application state (list of todos and chosen filter)
let previousState = {
visibleTodoFilter: 'SHOW_ALL',
todos: [
{
text: 'Read the docs.',
complete: false
}
]
}
// The action being performed (adding a todo)
let action = {
type: 'ADD_TODO',
text: 'Understand the flow.'
}
// Your reducer returns the next application state
let nextState = todoApp(previousState, action)
Reducerはピュアな関数であることに注意してください!Reducerはただ次のStateを計算しているだけです。Reducerに対して,同じ入力をすると,いくらやっても同じ出力しか出てきません。
APIコールやルーターの遷移のような副作用を起こすことはありません。ただ,次のStateを返すだけです。APIコールのような操作は,アクションがdispatchされる前に行われるべき操作です。
3. Root reducerは,複数のreducerの出力をひとつのStateツリーに結合する
どのようにRoot reducerを構成するかは自由に決めて良いようです。
Reduxはreducerを分割して管理するためのcombineReducers()
というヘルパー関数を持っているので,これを使うことが推奨されています。書き方は以下のようになります。
function todos(state = [], action) {
// Somehow calculate it...
return nextState
}
function visibleTodoFilter(state = 'SHOW_ALL', action) {
// Somehow calculate it...
return nextState
}
let todoApp = combineReducers({
todos,
visibleTodoFilter
})
Actionを実行するとき,combineReducers()
によって返ってきたtodoApp
は両方のreducer呼びます。
let nextTodos = todos(state.todos, action)
let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action)
これは2つの結果をひとつのstateツリーに結合します。
return {
todos: nextTodos,
visibleTodoFilter: nextVisibleTodoFilter
}
どうやら,関数の名前がオブジェクトのkeyになっているようですね。
4. ReduxのStoreは,Root reducerから返ってきた完全なStateツリーを保存する
新しいツリーはアプリの次のStateになります。このとき,store.subscribe(listener)
で登録された全てのリスナーが呼び出されることになります。リスナーが,現在のstateを取得するときにはstore.getState()
を呼んで取得します。
このタイミングで,新しいStateを反映するためにUIが更新されます。React Reduxのようなバインディングを使用しているときは,component.setState(newState)が呼ばれ,更新されることになります。
Reactとの併用方法
ReduxはReactに依存しているわけではないため,Angular,Ember,jQuery,vanilla JavaScriptなどで使用することができます。ただ,ReactとDekuはstateの関数としてUIを記述できるためReduxと相性が良いといえます。ReduxはActionに応じてStateの更新を行います。
react-reduxのインストール
react-reduxを使用するためnpmでインストールします。npmを使わない方法もありますが割愛します。
$ npm install --save react-redux
Presentational and Container Components
Reduxを使用したReactには,
- Presentational components
- Container components
の2つのコンポーネントがあります。
Container Componentsが,Reduxを使ったコンポーネントになります。
ほとんどのコンポーネントはPresentationalになります。一般的なReactのコンポーネントと思ってもらって良いです。ReduxのstoreをPresentational Componentsに紐付けるためにContainer Componentsを生成する必要があります。
もし,Container Componentsが複雑になりすぎる(例えば,Presentional Componentsがたくさんネストしていて,下位に渡されるコールバックが大量にある)場合,コンポーネントツリー内のもう一つのコンテナを導入した方が良いです。
store.subscribe()
を使えば手でContainer Componentsを書くこともできますが,React Reduxは手で書いて最適化するのは難しいみたいなので辞めておいた方が良さそうです。
connect()
関数を使ってContainer Componentsを生成することが推奨されています。
コンポーネントの階層を設計する
コチラに関してはドキュメントでも他記事参照となっていたので割愛します。
(Thinking in React)[https://reactjs.org/docs/thinking-in-react.html]を参照。
Presentational Componentsを設計する
Presentational Componentsと,その中身の説明を簡単に列挙すると下のようになります。
-
TodoList
は表示するtodosのリスト-
todos: Array
はtodoの項目の配列{id, text, completed}
-
onTodoClick(id: number)
はtodoをクリックしたときに発火するコールバック
-
-
Todo
はtodoの項目-
text: string
はテキスト -
completed: boolean
は表示するかどうか -
onClick()
はtodoがクリックされたときに発火するコールバック
-
-
Link
はコールバックを伴うリンク-
onClick()
はクリックされたときに発火するコールバック
-
-
Footer
は表示されているtodosをユーザに変更させる場所 -
App
はルートコンポーネント
これらは,見た目を描画しますが,どこからデータが来るのかや,どうやってデータを変更するかは知りません。ただ与えられたものをレンダーするだけです。
あるものがReduxから渡されたとき,コンポーネントは常に同じものになります。
Container Componentsの設計
Container Componentsは,Presentational ComponentsをReduxに接続するために必須です。
例えば,PresentationalなTodoList
コンポーネントは,ReduxのStoreを定期的にみて,現在のvisibility filterを適用するVisibleTodoList
のようなコンテナを必要とします。また,visibility filterを変更するために,適切なクリックアクションをdispatchするLink
をレンダーするFilterLink
コンテナコンポーネントも必要です。
まとめると,
-
VisbleTodoList
は現在のvisibility filterに応じてtodosを絞り込み,TodoList
を描画する -
FilterLink
は現在のvisibility filterを取得し,Link
を描画する-
filter: string
は表示するvisibility filter
-
他のコンポーネントを設計する
コンポーネントを必ず,Presentationalか,Containerかに区別しなければならなないわけではありません。例えば,コンポーネントが小さく,formと関数が一緒になっている場合などがあります。
-
AddTodo
はAddボタンを伴うinputフィールド
技術的に分割することはできるけど,分割するほどじゃない場合は,両者を一緒に書いてしまったほうがよいかもしれません。これが大きくなってくると,どのように分ければいいかが自ずと見えてくるようです。
Presentational Componentsの実装
これは普通のReactコンポーネントなので,Reduxのドキュメントでは詳細には触れられていませんでした。それぞれのコンポーネントを下に貼っておきます。単純なReactのコンポーネントです。
import React from 'react'
import PropTypes from 'prop-types'
const Todo = ({ onClick, completed, text }) => (
<li
onClick={onClick}
style={ {
textDecoration: completed ? 'line-through' : 'none'
}}
>
{text}
</li>
)
Todo.propTypes = {
onClick: PropTypes.func.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}
export default Todo
import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'
const TodoList = ({ todos, onTodoClick }) => (
<ul>
{todos.map((todo, index) => (
<Todo key={index} {...todo} onClick={() => onTodoClick(index)} />
))}
</ul>
)
TodoList.propTypes = {
todos: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}).isRequired
).isRequired,
onTodoClick: PropTypes.func.isRequired
}
export default TodoList
import React from 'react'
import PropTypes from 'prop-types'
const Link = ({ active, children, onClick }) => {
if (active) {
return <span>{children}</span>
}
return (
<a
href=""
onClick={e => {
e.preventDefault()
onClick()
}}
>
{children}
</a>
)
}
Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
}
export default Link
import React from 'react'
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>
)
export default Footer
Container Componentsの実装
技術的には,Container Componentsは,ReduxのStateツリーの一部分を読んだり,描画するPresentational Componentsのプロパティを供給するためのReactコンポーネントである。Container Componentsの中で,store.subscribe()
を使ってPresentational ComponentsとReduxを接続することになる。
だが,Container ComponentsはReact Redux ライブラリのconnect()
関数を使うことが推奨されている。これを使うことによって,Reactのレンダリングが最適化されようです。
connect()
を使うためには,現在のReduxのStoreのStateを,Presentational Componentsに渡すプロパティ場合に変換する必要がある。この方法を示しているのが,mapStateToProps
という関数で,これを定義しないといけないようです。
例えば,VisibleTodoList
はTodoList
に渡すためのtodos
を計算する必要があるので,state.visibilityFilter
に対応するstate.todos
を絞り込む関数を定義します。そして,mapStateToProps
でそれを使用する。
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
case 'SHOW_ALL':
default:
return todos
}
}
const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
さらにStateを読むことに加えて,Container ComponentsはActionをdispatchすることができます。dispatch()メソッドを受け取って,Presentational Componentsに挿入したいpropsのコールバックを返すmapDispatchToProps()
という関数を定義することができます。
例えばTodoList
コンポーネントにonTodoClick
というプロパティを挿入するためには,VisibleTodoList
と,TOGGLE_TODO
アクションをdispatchするonTodoClick
が必要になります。
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
最後に,connect()
を呼び,これらの2つの関数を渡すVisibleTodoList
を生成します。
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
これらは,React redux APIの基本ですが,もっと簡単に記述する方法があるので,気になる方はドキュメントを参照。
残りのコンテナコンポーネントの書き方は以下のようになります。
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'
const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter))
}
}
}
const FilterLink = connect(
mapStateToProps,
mapDispatchToProps
)(Link)
export default FilterLink
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
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)
}
}
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
他のコンポーネントの実装
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'
let AddTodo = ({ dispatch }) => {
let input
return (
<div>
<form
onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
input.value = ''
}}
>
<input
ref={node => {
input = node
}}
/>
<button type="submit">
Add Todo
</button>
</form>
</div>
)
}
AddTodo = connect()(AddTodo)
export default AddTodo
refとかよく分かってないけど今回は飛ばします。こう書けばいいらしいということはわかる。
コンポーネント内でコンテナを結びつける
import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'
const App = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
)
export default App
Storeの渡し方
Container Componentsは,ReduxのStoreにアクセスする必要があるので,Storeにsubscribeすることができる。全てのコンテナコンポーネントにpropとして渡す方法もありますが,それは何かと面倒らしいです。Presentational Componentsのコールバックのネストが深くなったりしたときにいちいち手で書かないといけなかったりするらしい。
それを解決するために,<Provider>
を使用して接続することが推奨されているようです。
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
まとめ
以上で,Basicsの記述は一通り読むことができました。
Container Componentsを書いて,ReactのPresentational Componentsに対してStoreの情報を渡せるんですね。このあたりは実装しながら理解を深められたらと思います。