前回のRedux入門:基礎編(一)に引き続き、今回は公式ドキュメントに沿って、以下の3つの点から解説していきます。
- データフロー
- Reactとの連携
- 実例:TodoList
データフロー
Reduxのアーキテクチャは、厳格な単方向データフローを構築する方法を中心に設計されています。
これは、すべてのデータが同じライフサイクルパターンに従うことを意味し、アプリケーション全体を予測可能で理解しやすくします。Reduxアプリケーションにおけるデータのライフサイクルは、以下の4つのステップに分けられます。
1 あなたが store.dispatch(action)
を能動的に呼び出す
Action は、何が起こったかを記述するオブジェクトです。例えば:
{type:'ADD_TODO',text:'todo\'s content'}
{type: 'FETCH_USER_SUCCESS', response: {id: 3, name: 'Mary'}}
Action は、イベントの短い断片的な説明と考えることができます。store.dispatch(action)
は、コンポーネントやXHR内、さらにはタイマー内など、どこからでも呼び出すことができます。
2 Redux store があなたが提供する reducer 関数を呼び出す
store は reducer に2つの引数を渡します:現在の state と action です。例えば、Todoアプリケーションでは、ルート reducer は以下のような引数を受け取ります:
let previousState = {
visibleTodoFilter: "SHOW_ALL",
todos: [
{
text: "Read the docs",
complete: false,
},
],
};
let action = {
type: "ADD_TODO",
text: "Understand the flow",
};
// nextState 由 todoApp 这个根 reducer 来生成
let nextState = todoApp(previousState, action);
注意すべきは、reducer が純粋関数であり、副作用がなく、次の state を計算するだけであるということです。同じ引数に対しては、何度呼び出しても常に同じ値を返す、完全に予測可能なものであるべきです。API呼び出しやルーティングの変更などは、action が dispatch される前に行われるべきです。
3 ルート reducer は複数の子 reducer の出力を結合して単一の state ツリーを生成する
ルート reducer をどのように組み合わせるかは完全にあなた次第です。Redux は combineReducers()
というヘルパー関数を提供しており、ルート reducer を独立した関数に分割して state ツリーの特定の部分を管理する際に非常に役立ちます。
combineReducers()
関数がどのように機能するかを見てみましょう。Todoリストを管理する reducer と、現在選択されているフィルターの状態を管理する reducer の2つがあると仮定します:
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)
そして、各 reducer が返した state を単一の state ツリーに結合します
return {
todos: nextTodos,
visibleTodoFilter: nextVisibleTodoFilter
}
combineReducers
を使用しない選択肢もあります。結局のところ、それは便利な補助ツールに過ぎず、独自のルート reducer を実装することも可能です。
4 Redux store がルート reducer によって返された state ツリー全体を保存する
この新しい state ツリーが、現在のアプリケーションの次の state となります。store.subscribe(listener)
を通じて登録されたすべてのリスナー関数が呼び出され、リスナーオブジェクトは store.getState()
を呼び出して現在の state を取得することができます。
これで、ビュー全体が新しい state を使って更新されます。React Redux をビューバインディングツールとして使用している場合、これは component.setState(newState)
が呼び出されるポイントです。
Reactとの連携
Redux と React の間には直接的な関連性はありませんが、Redux は React や Deku のようにデータ状態によってUIを記述できるフレームワークと非常に相性が良いです。ここでは、React を使用してシンプルな Todo アプリケーションを構築します。
React Redux のインストール
React バインディングは Redux にデフォルトで含まれていないため、別途インストールする必要があります:
npm install --save react-redux
プレゼンテーショナルコンポーネントとコンテナコンポーネント
Redux の React バインディングは、プレゼンテーショナルコンポーネントとコンテナコンポーネントの分離という考え方を支持しています。
項目 | プレゼンテーショナルコンポーネント | コンテナコンポーネント |
---|---|---|
目的 | UIの見え方(マークアップ、スタイル) | 機能の動作(データ取得、状態更新) |
Reduxを意識するか | いいえ | はい |
データの読み取り | 親のpropsから読み取る | Redux stateを購読して取得する |
データの変更 | propsからコールバック関数を呼び出す | Redux actionsをdispatchする |
記述方法 | 手動で記述 | 通常はReact Reduxによって生成される |
ほとんどのコンポーネントはプレゼンテーショナルコンポーネントとして記述されるべきですが、それらをRedux storeに接続するためのコンテナコンポーネントクラスもいくつか生成する必要があります。
技術的には、store.subscribe()
を使用してコンテナコンポーネントを手動で記述することも可能です。しかし、React Redux は手動では実装が難しい多くのパフォーマンス最適化を行っているため、これは推奨されません。したがって、手動でコンテナコンポーネントを記述するよりも、React Redux が提供する connect()
関数を使って生成することをお勧めします。
コンポーネント階層の設計
私たちの設計は非常にシンプルです。Todoアイテムのリストを表示したいと考えています。Todoアイテムが完了したかどうかをマークするためのボタン。新しいTodoを追加する場所。フッターには、すべてのTodo、完了したTodo、未完了のTodoを表示するための切り替えボタンが必要です。
プレゼンテーショナルコンポーネント
以下のプレゼンテーショナルコンポーネントを通じて、propsの階層を概説できます。
TodoList
は利用可能なTodoのリストを表示します。todos: Array
{ id, text, completed }
の形式のリストonTodoClick(id: number)
Todoがクリックされたときのコールバック関数
Todo
は個々のTodoアイテムです。text: string
表示するテキストcompleted: boolean
Todoが完了したかどうかの状態onClick
Todoがクリックされたときに呼び出されるコールバック関数
Link
はコールバックを持つリンクです。onClick
このリンクがクリックされたときのコールバック
Footer
は現在表示されているTodoの内容を変更する場所です。App
はルートコンポーネントで、他のすべてをレンダリングします。
これらはアプリケーションがどのように見えるかを記述しますが、データがどこから来るのか、どのように変更するのかは知りません。ただ与えられたデータをレンダリングするだけです。Reduxから他のフレームワークに移行する場合でも、これらのコンポーネントはほとんど変更せずに済みます。これらはReduxとは関連がありません。
コンテナコンポーネント
同様に、プレゼンテーショナルコンポーネントをReduxに接続するためのコンテナコンポーネントもいくつか必要です。例えば、TodoList
コンポーネントは、現在の可視性フィルター(visibility filter)をどのように使用するかを知るために、Reduxからデータを購読する VisibleTodoList
というコンテナを必要とします。可視性フィルターを変更するために、クリック時に適切なactionをdispatchする Link
をレンダリングする FilterLink
コンテナを提供します:
VisibleTodoList
は現在の可視性フィルターに基づいて表示する内容をフィルタリングし、TodoList
をレンダリングします。FilterLink
は現在の可視性フィルターを取得し、Link
をレンダリングします。filter: string
可視性フィルターを表す文字列
その他のコンポーネント
時には、コンポーネントがプレゼンテーショナル型かコンテナ型かを区別するのが難しい場合があります。例えば、フォームと関数が相互に依存している場合など、この小さなコンポーネントのように:
AddTodo
は「追加」ボタン付きの入力ボックスです。
技術的には、これを2つのコンポーネントに分離することも可能ですが、それでは明らかに煩雑すぎます。プロジェクトが複雑で大規模になったときに分離することはできますが、今のところは、これらを混合したままにしておきましょう。
コンポーネントの実装
プレゼンテーショナルコンポーネント
これらは通常のReactコンポーネントなので、詳細な説明は省略します。
TodoList.js
import React, { PropTypes } from "react";
import Todo from "./Todo";
const TodoList = ({ todos, onTodoClick }) => (
<ul>
{todos.map((todo) => (
<Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} />
))}
</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;
コンテナコンポーネント
次に、コンテナコンポーネントを生成して、プレゼンテーショナルコンポーネントとReduxを連携させます。技術的には、コンテナコンポーネントは、store.subscribe()
を使用してRedux stateツリーの一部を読み取り、レンダリングのためにプレゼンテーショナルコンポーネントにpropsを提供する単なるReactコンポーネントです。このコンポーネントを手動で記述することもできますが、Reduxが提供する connect()
関数を使用してこれらのコンポーネントを生成することをお勧めします。これは、不要な再レンダリングを防ぐための多くの有用な最適化を提供します。
connect()
を使用するには、現在のRedux store state をプレゼンテーショナルコンポーネントに渡したいpropsに変換する方法を指定する mapStateToProps
という特別な関数を定義する必要があります。例えば、VisibleTodoList
は TodoList
に渡す todos
を計算する必要があるため、state.visibilityFilter
を使って state.todos
をフィルタリングする関数を定義し、それを mapStateToProps
で使用します:
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),
};
};
state を読み取るだけでなく、コンテナコンポーネントはactionをdispatchすることもできます。同様に、dispatch()
メソッドを受け取り、目的のプレゼンテーショナルコンポーネントに注入するコールバックpropsを返す mapDispatchToProps()
という関数を定義できます。例えば、VisibleTodoList
が TodoList
コンポーネネントに onTodoClick
というpropを注入し、onTodoClick
が TOGGLE_TODO
action をdispatchするようにしたい場合:
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id));
},
};
};
最後に、connect()
を呼び出して VisibleTodoList
を生成し、これら2つの関数を渡します:
import { connect } from "react-redux";
const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList);
export default VisibleTodoList;
これらはReact Reduxの基本的なAPIですが、いくつかのショートカットや強力なオプションがあるため、こちらのドキュメントを注意深く確認することをお勧めします。mapStateToProps
が新しいオブジェクトを作成するプロセスについて心配な場合は、reselect を使用した派生データの計算について学ぶ必要があるかもしれません。
Store への受け渡し
すべてのコンテナコンポーネントは、Redux store に接続して購読する必要があります。一つの方法は、それを各コンテナコンポーネントにpropとして渡すことですが、それは煩雑すぎます。
私たちが推奨する方法は、特定のReact Reduxコンポーネント <Provider>
を使用して、store を明示的に渡すことなく、アプリケーション内のすべてのコンテナコンポーネントで利用できるようにする「魔法」を使うことです。ルートコンポーネントをレンダリングする際に一度だけ呼び出すだけで済みます。
Index.js
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
import App from "./components/App";
import todoApp from "./reducers";
let store = createStore(todoApp);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root"),
);
実例:TodoList
ドキュメントを直接読むだけでは、多くの専門用語やメソッドを知っても、全体像を把握するのは難しいでしょう。やはりコードを見る必要があります。ここに公式のTodoListのソースコードがありますので、これら2つの記事と合わせて読めば、よく理解できるはずです。 ここでいくつかの問題に遭遇しました。
-
webpack が常にエントリファイル ’./index’ を見つけられないとエラーを吐き続け、最終的にbabelの落とし穴を発見しました。Babelが6.0にアップグレードされてから、コンパイルするjsファイルのタイプを
.babelrc
ファイルで指定する必要があり、そうしないとコンパイルエラーが発生します。{ "presets": ["es2015", "react"] }
-
ページがレンダリングされた後、追加などの操作は正常に機能しました。しかし、フィルターをクリックすると常にエラーが発生し、そのエラーも不可解なものでした。そこで、一つずつブレークポイントを置いて確認したところ、最終的に
Footer.js
内のフィルターに対応するactionのスペルミスが原因で、後続のコンポーネントが対応するactionを見つけられないことが判明しました。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;
このことから、アプリケーションのactionはやはり専用のファイルで個別に管理し、すべてのactionを変数として定義してエクスポートする必要があることがわかります。そうすれば、後でスペルミスがあった場合でも、コンパイル段階で問題を発見できます。
参考
この記事は 2016年5月3日 に公開され、2016年5月3日 に最終更新されました。3442 日が経過しており、内容が古くなっている可能性があります。