Reduxの3つの原則
- アプリケーション全体のステートは、単一のstore内でオブジェクトツリーの形式で保持されます。
- このオブジェクトツリーを変更する唯一の方法は、何が起こったかを記述するオブジェクトであるactionをディスパッチすることです。
- 純粋な関数であるreducerを記述することで、そのactionがオブジェクトツリーをどのように変更するかを記述します。
Action
Actionは、アプリケーションからstoreに送信される情報を運びます。これらはstoreの情報源にすぎません。store.dispatch()
を介してactionを渡すことができます。
actionは次のような形をしています。
const ADD_TODO = 'ADD_TODO'
{
type: ADD_TODO,
text: 'Bulid my first Redux App'
}
Actionは通常のJavaScriptオブジェクトですが、どのような種類の操作が発生したかを指定するための type
プロパティが必須です。type
は文字列定数として定義されるべきです。プロジェクトが大規模になると、これらを個別のモジュールに移動する必要があるかもしれません。
import { ADD_TODO, REMOVE_TODO } from "../actionTypes";
type
とは異なり、action全体の構造は完全にあなたが決定します。ただし、actionの構造をより良く整理する方法については、Flux Standard Action のガイドラインを参照してください。
Action Creators
Action Creatorは、actionを生成するための関数です。 Reduxでは、action creatorは単純にactionオブジェクトを返すだけで十分です。
function addTodo(text) {
return {
type: ADD_TODO,
text: "Some text",
};
}
これにより、特定の種類のactionをより簡単に作成でき、テストも容易になります。
Dispatch
実際にdispatchを初期化するには、結果を diapatch()
関数に渡すことで行えます。
store.dispatch(addTodo(text));
あるいは、自動的にdispatchするバインドされたaction creatorを作成することもできます。
const boundAddTodo = (text) => dispatch(addTodo(text));
これで、それらを直接呼び出すことができます。
boundAddTodo(text);
dispatch()
関数は store.dispatch()
を介してstoreに直接アクセスできますが、react-reduxの connect()
のようなヘルパー関数を使用してアクセスする方が好ましいかもしれません。bindActionCreators()
を使用すると、多くのaction creatorを dispatch()
関数に自動的にバインドできます。
Reducers
Actionは「何が起こったか」という事実を記述しますが、アプリケーションのstateをどのように変更するかは指定しません。これがreducerの役割です。
State構造の設計
Reduxでは、アプリケーションのすべてのstateが単一のオブジェクトに保存されます。そのため、コードを書く前にstateをどのように設計するかを検討することが重要です。アプリケーションのstateをオブジェクトとして記述する最も簡単な方法を考えましょう。 todoアプリケーションの場合、2つの異なることを保存したいと考えます。
- 現在選択されている表示フィルター条件
- 実際のtodosのリスト
シンプルなstateは次のようになります。
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true
},{
text: 'Keep all state in a single tree',
completed: false
}
]
}
Actionsの処理
Reducerは純粋な関数であり、以前のstateとactionを引数として受け取り、次のstateを返します。
(previousState, action) => newState;
reducerと呼ばれるのは、Array.prototype.reduce(reducer,?initialValue)
関数に渡されるためです。そのため、reducerの純粋性を保つことが非常に重要です。reducer内で決して以下のことを行わないでください。
- その関数引数を変更する
- API呼び出しやルーティングの遷移など、副作用のある操作を行う
Date.now()
やMath.random()
のような純粋でない関数を呼び出す
これらを理解した上で、reducer関数を始めましょう。初期stateを指定することから始めます。Reduxはreducerを最初に呼び出す際に undefined
stateを渡します。このとき、初期化されたstateを返す必要があります。
import { VisibilityFilters } from "./actions";
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todo: [],
};
function todoApp(state = initialState, action) {
//采用 ES2015 写法,当 state 传递为 undefined 时,会被赋值为 initialState
return state;
}
次に SET_VISIBILITY_FILTER
を処理します。行う必要があるのは、stateの visibilityFilter
を変更することです。
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.SET_VISIBILITY_FILTER,
});
default:
return state;
}
}
注意すべき点:
- 元のstateを変更しません。
Object.assign()
を介して、元のstateと変更内容をマージしたコピーを作成します。 - 該当する状況が見つからない、つまりdefaultの場合には、以前のstateを返さなければなりません。
より多くのActionsの処理
他にも処理すべきactionがいくつかあるので、それらもすべて追加します。
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;
}
}
reducer関数を分離することで、より理解しやすくすることができます。todos関連の処理ロジックとvisibilityFilterの処理ロジックを一緒に置くのはあまり明確ではないからです。reducerの分離は非常に簡単です。
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false,
},
];
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の state
引数は異なり、それぞれが管理する部分のstateに対応しています。
アプリケーションが大規模になると、reducerを複数の異なるファイルに分離し、独立性を保ちながら異なるデータソースを管理することができます。
最後に、Reduxは combineReducers()
関数を提供しており、上記のtodoAppと同様のロジックで複数のreducerを結合し、多くのボイラープレートコードを省略できます。
import { combineReducers } from "redux";
const todoApp = combineReducers({
visibilityFilter,
todos,
});
export default todoApp;
これは以下の記述と完全に等価です。
export default function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action),
};
}
combineReducers()
が行っているのは、関数を生成し、各reducer関数に対応するstateを渡し、それらを単一のオブジェクトに結合することです。これは黒魔術ではありません。
combineReducers 原理
2015年にReduxを見たとき、ドキュメントを読んでいなかったので、combineReducers()
が実際に何をしているのか理解できず、黒魔術だと思っていました。
さらに、combineReducers()
後の関数を介して初期化されたstoreを生成することもできます。
今日、ドキュメントと黒魔術に関するこのissueを読み直して理解しました。
実際、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
})
ご覧の通り、各reducerにはデフォルトのstateが定義されています。todos
では []
、visibleTodoFilter
では SHOW_ALL
です。
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
の動作原理を理解できます。もちろん、combineReducers
を使用しない選択も可能です。結局のところ、これは公式が提供するヘルパーツールに過ぎず、独自のルートreducerを自分で実装することもできます。
Store
Storeは、それらを連携させるためのオブジェクトです。Storeの役割は以下の通りです。
- アプリケーション全体のstateを保存する
getState()
を介してstateを取得できるdispatch(action)
を介してstateを更新できるsubscribe(listener)
を介してリスナーを登録するsubscribe(listener)
が返す値を使って、登録解除されたリスナーを処理する
Reduxアプリケーションにはstoreが1つしかないことに注意が必要です。データロジックを分離したい場合は、より多くのstoreを作成する代わりに、より多くのreducerを作成することで実現できます。
reducerが1つあれば、storeの作成は簡単です。combineReducers()
で作成したルートreducerを介してstoreを作成できます。
import { createStore } from "redux";
import todoApp from "./reducers";
let store = createStore(todoApp);
オプションの引数を渡してstateを初期化することもできます。これは、ユニバーサルアプリケーションを開発する際に非常に役立ち、サーバーから渡されたstateをクライアントの初期stateとして使用できます。
let store = createStore(todoApp, window.STATE_FROM_SERVER);
总结
これらの内容を理解すれば、Reduxがどのように機能するのかおおよそ分かるでしょう。ドキュメントを読むことはやはり非常に役立ちます。昨年、公式のサンプルコードを直接見て、結局何が何だか分からなかったのとは違います。 次に、データフローとReactとの連携について学び続け、理解を深めるためにTodoListのインスタンスを実際に作成します。
参考
この記事は 2016年4月27日 に公開され、2016年4月27日 に最終更新されました。3449 日が経過しており、内容が古くなっている可能性があります。