Redux公式サイトのチュートリアルで学習した内容をまとめたものです。
基本的には、本家サイトに記載されている内容を自分なりに日本語でまとめなおしたものです。
2019年5月時点の情報を基にしています。
Reactの基本事項は既に習得されている方向けです。
概要
Reduxとは
Reduxは、JavaScriptアプリケーションの状態管理を容易にする為のフレームワークです。
主にReactを用いて構築されたアプリケーションで利用されますが、その他のフレームワークやライブラリーで構築された場合も、もちろん利用できます。
アプリケーション上で表示されるデータや、各コンポーネントの表示非表示の状況を一元的に管理することで、見通しのよいアプリケーションの構築を実現させようというものです。
Reactの提供元であるFaceBookが提唱した、Fluxという設計パターンからヒントを得て作られています。
React-Reduxとは
名前のとおり、ReactコンポーネントとReduxを接続する為のフレームワークです。
React・Reduxに依存した造りというわけではないため、AngularやVueなどのその他のフレームワークやVanillaJSでの利用も可能とのことです。
Fluxとは
Reactによるアプリケーション構築を保管する為に考案された、設計パターンです。
データの流れを一方通行にすること、アプリケーションの状態「State」を一元的に管理することで、アプリケーションの管理が煩雑化することを防ぐ試みです。
Fluxによるアプリケーションは、下記のパートで構成されます。
- Dispatcher: アプリケーションへの更新情報を集約・通知
- Store: アプリケーションの状態を保持・管理
- Action: アプリケーションの更新情報を得る為の内部API
- View: Reactコンポーネントによるインターフェース
Dispatcherは、アプリケーションに1つだけ存在します。
Storeは、アプリケーションの規模などに応じて1つ以上存在します。
Actionは、アプリケーションに発生しうる変更の数だけ存在します。
Vewは、Storeと接続したコンポーネントを頂点としてツリー構造を形成します。
詳細はこちらの記事で扱っております。
FluxによるReactアプリの状態管理 Flux・FluxUtils編
本家サイトはこちら。
Flux
インストール
npmのパッケージとして公開されています。
npm install redux react-redux --save
introduction > installation | Redux
Itroduction > Quick Start | React Redux
Todoアプリ
本家サイトのベーシックチュートリアルには、Todoアプリの解説とソースコードがあります。
ちょくちょく更新されているようです。
2019年5月に写経したソースはこちらです。
todoapp-flux-redux
動作のサンプル
ReactのJSX記法を利用する為、Babelなどのトランスパイラを利用します。
webpackなどのモジュールバンドラーの利用も前提としています。
今回は、Parcelを利用しています。
Actions
Actionは、Storeに対して送信するデータです。
実体はJavaScriptのオブジェクトです。
Actionの種類を示す、type
プロパティを必ず持っています。type
プロパティの値は、一般的に文字列が用いられます。
アプリケーションの規模が大きい場合は、予め定数として定義しておくことが望ましいでしょう。
その他Storeに送信したいデータが含まれます。
// ActionType
const ADD_TODO = 'ADD_TODO'
// Action
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
このActionを生成するのがActionCreatorです。
ReduxにおけるActionCreatorは、単純にActionを返すのみです。
let nextTodoId = 0;
/**
* TODOリストを追加する
*/
export const addTodo = text => ({
type: 'ADD_TODO',
id: nextTodoId++,
text
});
/**
* 表示フィルターをかける
*/
export const setVisibilityFilter = filter => ({
type: 'SET_VISIBILITY_FILTER',
filter
});
/**
* 完了未完了切り替える
*/
export const toggleTodo = id => ({
type: 'TOGGLE_TODO',
id
});
/**
* フィルターの定数
*/
export const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL',
SHOW_COMPLETED: 'SHOW_COMPLETED',
SHOW_ACTIVE: 'SHOW_ACTIVE'
};
この戻り値をStoreに送信するには、後述するStoreのdispatch()
メソッドを利用します。
store.dispatch(addTodo('Learn about Redux'))
Reducers
Actionはあくまで「何が起きたか」を定義するだけです。それを受けて「どうするか」に関する具体的な指示は含みません。
その責務を負うのがReducerです。Reducerは、Storeに送信されたActionをもとに、Storeが保持するアプリケーションのStateを、どう変更するかを決めます。
アプリケーションのStateの設計
Reduxでは、アプリケーションの状態「State」は一つのオブジェクトで表されます。
今回のTodoアプリのStateは、以下のようになります。
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
id: 1,
text: 'Write a article about Redux',
completed: true
},
{
id: 2,
text: 'Go to the Cats cafee',
completed: false
}
]
}
Stateを変更する
Reducerは、副作用のない純粋な関数です。
Storeから呼び出され、引数で以前のStateと新たに生成されたActionを受け取り、新しいStateを返します。
(previousState, action) => newState
previousState
とaction
の内容が同一であれば、何度呼び出しても同じnewState
を返します。
その為、Reducerには下記のルールがあります。
- 引数を変更してはいけません。
- APIの呼び出しやルーティングなどの、副作用が発生する処理を記述してはいけません。
- 純粋でない関数を呼び出してはいけません。(例:Date.now(), Math.random())
上記Stateを変更するStateは下記のようになります。
import { VisibilityFilters } from './actions';
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos:[]
};
function todoApp(state = initialState, action){
switch(action.type){
case 'ADD_TODO':
return Object.assign({}, state, {
todos: [/* 項目追加 */]
});
case 'TOGGLE_TODO':
return // 項目切替後newState
case 'SET_VISIBILITY_FILTER':
return {
visibilityFilter: action.filter
};
default:
return state;
}
}
初回呼び出し時、引数state
はundefined
の状態で渡されます。
その為、デフォルト引数には、Stateの初期値を設定しています。
ActionTypeでActionの種類が「追加」であることを識別し、以前StateにActionの情報を加味した新しいStateを作成しています。
Stateを変更する必要がない場合は、以前のStateをそのまま返します。
Reactは、Stateの変更を確認した上で再描画を実行しています。
その際の比較方法は値ではなく、オブジェクト参照元の比較です。
その為、Reducer内でStateを変更する場合は、引数で受け取った以前のStateをそのまま変更してはいけません。
Object.assign()やスプレッド構文等を利用して、予めStateを複製した上で変更する必要があります。
Reduxユーザーが最もハマるstateの不正変更とその検出方法
Why is immutability required by Redux? | Redux
Reducerの分割
アプリケーションの規模が大きい場合、StateおよびReducerのサイズも大きくなってきます。
その場合、一つのReducerで管理することが難しくなる為、適宜分割することが望ましいでしょう。
todos
とvisibilityFilter
、それぞれ管理するReducerに分割します。
初期値も、それぞれの項目の初期値のみ用意します。
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
default:
return state;
}
};
export default todos;
import { VisibilityFilters } from '../actions';
const visibilityFilter = (state = VisibilityFilters.SHOW_ALL, action) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
};
export default visibilityFilter;
Reducerの結合
分割したものの、Storeから参照されるReducerは一つだけです。
そのため、各ReducerはReduxが提供するcombineReducers()
関数を利用して、一つに束ねます。
この関数は各Reducerから返されるStateのパーツを、一つのオブジェクトにまとめたStateにして返す、RootReducerを生成します。
import { combineReducers } from 'redux';
import todos from './todos';
import visibilityFilter from './visibilityFilter';
export default combineReducers({
todos,
visibilityFilter
});
// [参考]自作した場合
function roodReducer(state = {}, action){
return {
todos: todos(state.todos, action),
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
};
}
combineReducers(reducers) | Redux
Store
アプリケーションのStateを一元的に管理するモジュールです。
アプリケーションに1つだけ存在します。
下記の責務を負います。
- アプリケーションの現在のStateを保持する
-
getState()
メソッドを通じて、Stateを提供する -
dispatch(action)
メソッドを通じて、Stateの更新を可能にする -
subscribe(listener)
メソッドを通じて、Stateの更新イベントのリスナーを登録する -
subscribe(listener)
メソッドの戻り値として、イベントリスナー解除用関数を提供する
Fluxでは独立したモジュールとして存在したDispatcherは、Storeのメソッドになりました。
実行順序の調整などは行いません。
Storeは、Actionの受付とState更新の通知を行いますが、新しい状態を生成することはしません。
それはReducerの責務です。
dispatch()
が実行されると、受け取ったActionとStoreが保持している現在のStateをReducerに渡し、新しいStateを受け取ります。
Reduxで提供しているcreateStore
関数に、RootReducerを渡して作成します。
import { createStore } from 'redux';
import rootReducer from './reducers/';
const store = createStore(rootReducer);
// 第二引数に初期Stateを渡すことも可能
const store = createStore(rootReducer, initialState);
基本的にはこれだけです。こちらで何か処理を書くことはありません。
ReduxとReactの接続
StoreとReactComponentの接続
StoreとViewを、React-Reduxが提供しているProvider
コンポーネントを利用して繋げます。
Reduxで用意したstoreをProvider
コンポーネントにpropsとして渡します。
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './reducers/';
import App from './components/app';
const store = createStore(rootReducer);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
これで、View側がStoreの持つStateを参照する為の準備が出来ました。
アプリケーション内でStoreのデータを共有する
Reactでは基本的に、propsを通じて親コンポーネントから子孫コンポーネントへとデータを受け渡します。
コンポーネントの階層が深くなると、データのバケツリレーが発生してしまいます。
React-Reduxが提供しているconnect
関数を利用することで、Storeとの接点を持つProvider
コンポーネントへのショートカットを作ります。
import { connect } from 'react-redux';
import { someActionA, someActionB } from '../actions';
// 子孫コンポーネントに必要なStateを抽出(任意)
const mapStateToProps = ({paramA, paramB}) => {
return {
paramA,
paramB
}
};
// 子孫コンポーネントに渡すコールバック関数を定義(任意)
const mapDispatchToProps = dispatch => {
return {
onClick: data => dispatch(someActionA(data)),
onChange: data => dispatch(someActionB(data))
};
};
// ChildComponentをラップしたコンポーネントを作成
export default connect(
mapStateToProps,
mapDispatchToProps
)(ChildComponent);
// Stateの抽出や、コールバック関数の定義が不要な場合
export default connect()(ChildComponent);
// 下位コンポーネントはpropsで受け取れる
const ChildComponent = ({paramA, onClick, ...otherProps})=>{
return (
<div>
<button onClick={e=> onClick(paramA)}>{paramA}<button>
<GrandchildComponent {...otherProps} />
</div>
);
};
connect()
関数の部分はカリー化が含まれていてややこしいですが、connect()
の戻り値は関数です。それをさらに実行すると、Reactのコンポーネントが作成されます。
これにより、ChildComponent
はpropsとしてStateの全データを受け取ることが出来ます。
一部分だけ渡したい場合は、mapStateToProps
関数をconnect()
の第一引数に渡します。
この関数はStateが更新されるたびに呼ばれ、引数でStateを受け取ります。
そして、戻り値が下位のコンポーネントへpropsとして渡されます。
第二引数のmapDispatchToProps()
は、StoreにActionを送信する処理を定義したい場合に利用します。
Store.dispatch()
メソッドをを引数で受け取るので、ActionCreatorと共にコールバック関数を定義します。
この関数は初回に実行され、以降は戻り値がpropsの一つとして下位コンポーネントに渡されます。
PresentationalComponents と ContainerComponents
ReactとReduxでアプリケーションを構築する際は、基本的にコンポーネントをPresentationalComponentとContainerComponentの2種に分けます。
PresentationalComponentはビジネスロジックを持たず、描画に徹します。
ContainerComponentはReduxと接点を持ち、PresentationalComponentに必要なpropsを受け渡します。
こうすることで、より見通しの良いアプリケーションの構築を可能とし、コンポーネントの再利用性を高めます。
Presentational Components | Container Components | |
---|---|---|
目的 | どのように見せるか | どのように動くか |
Reduxを意識 しているか |
しない | する |
データの提供元 | propsから | ReduxのStateから |
データの変更 | propsの コールバック関数を実行 |
Reduxのメソッドを実行 |
作成方法 | 自分で | 通常は、 ReactReduxを利用して |
Presentational and Container Components | Redux
TodoListの場合
Todoリストを表示するコンポーネントのツリーは下記のとおりです。
VisibleTodoListがContainerComponentsです。
Provider
└ VisibleTodoList ☆
└ TodoList
└ Todo
Stateからtodoリストの配列を取り出し、さらに現在のフィルターにマッチするものを抽出して、下位のコンポーネントに渡しています。
また、項目をクリック時に完了状況を切り替えるためのコールバック関数も定義しています。
import { connect } from 'react-redux';
import { toggleTodo } from '../actions';
import TodoList from '../components/todoList';
import { VisibilityFilters } from '../actions';
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos;
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(t => t.completed);
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(t => !t.completed);
default:
throw new Error('Unknown filter' + filter);
}
};
const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
};
const mapDispatchToProps = dispatch => {
return {
toggleTodo: id => dispatch(toggleTodo(id))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoList);
Todo項目のコンポーネントは、受け取った情報を表示したり、コールバック関数を実行するだけです。
const Todo = ({ onClick, completed, text }) => (
<li
onClick={onClick}
style={{
textDecoration: completed ? 'line-through' : 'none'
}}
>
{text}
</li>
);
AddTodoの場合
Todoに項目を追加するコンポーネントを構築します。
Form要素を含むコンポーネントなど、責務の分離が難しい場合は、分離しないという選択肢もあります。
mapStateToProps()
を利用しない場合は全Stateを受け取ることになるので、分割代入を利用して必要分だけ受け取っています。
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>
);
};
export default connect()(AddTodo);
Designing Other Components | Redux