Redux Advent Calendar 2016 23日目
初めてAdvent Calendarに参加させていただきます。
当初はReduxとcreate-react-appを使って、サクッと素振りできる環境構築手順をまとめようと思ったのですが、ここ最近で自分が一番悩んだ「react-redux」についてReduxを始める方へ同じドツボにはまらないようにという願いを込めて投稿させていただきました。
react-reduxとは?
react-redux公式より引用
Official React bindings for Redux.
Performant and flexible.
React公式のReactとReduxをバインディングするライブラリ
パフォーマンスと柔軟性があります
ここで出て来る「柔軟性(flexible)」というのがこのライブラリが初見殺しと言われる所以となっており、実際によく
使う2つのAPI
- Provider
- connect
について、何をやっている関数かというのが今回扱う内容となります。
タイトルの通り、ほとんどがconnect実装パターンとなります。
どうして初見殺しになるのか?
ここで、自分がReduxの師匠と崇めている@kuyさんより頂いたお言葉を引用したいと思います。
@MegaBlackLabel それはよかったー。react-reduxのAPIがアレなのはすべての人の要求に答えようとした結果なので、シンプルなやつを自作することを検討してもいいと思います。
— Yuki Kodama (@kuy) 2016年12月19日
「柔軟性が素人を殺す」この一言に限ります。
公式の引用やサンプルを使ってどういった動きをするのかというのを解説していきます。
Provider
以下公式より引用
Makes the Redux store available to the connect() calls in the component hierarchy below. Normally, you can’t use connect() without wrapping the root component in .
If you really need to, you can manually pass store as a prop to every connect()ed component, but we only recommend to do this for stubbing store in unit tests, or in non-fully-React codebases. Normally, you should just use .
Props
- store (Redux Store): The single Redux store in your application.
- children (ReactElement) The root of your component hierarchy.
Providerの目的は2つ
- Reactコンポーネント内でreact-reduxのconnect()関数を使えるようにすること
- ラップしたコンポーネントにstore情報を渡すこと
項目1についてはReactをそれなりにやっていると理解できると思いますが、項目2ですでに初見殺しだと思います。
実際のコードを使って説明したいと思います。
create-react-appで雛形を作り、ボタンを押すとカウントされるReduxの構成でアプリを作成します。
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, combineReducers } from "redux";
import { Provider } from "react-redux";
import App from './App';
import './index.css';
const appInitialState = { count : 0 }
export const app = (state = appInitialState, action) => {
switch (action.type) {
case 'INC':
console.log("click INC");
return Object.assign({}, { count: state.count + 1 });
case 'DEC':
console.log("click DEC");
return Object.assign({}, { count: state.count - 1 });
case 'PLUS2':
console.log("click PLUS2");
return Object.assign({}, { count: state.count + 2 });
case 'PLUS3':
console.log("click PLUS3");
return Object.assign({}, { count: state.count + 3 });
default:
return state;
}
}
const RootReducer = combineReducers({app});
const store = createStore(RootReducer);
ReactDOM.render(
<Provider store={ store }>
<App />
</Provider>,
document.getElementById('root')
);
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import { connect } from 'react-redux';
class App extends Component {
render() {
const { app: { count } } = this.props.state;
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
<button onClick={ e => {this.props.dispatch({ type: "INC" });}}>INC: { count }</button>
</div>
);
}
}
function mapStateToProps(state) {
return {state};
}
export default connect(mapStateToProps)(App);
このアプリについて
npm start
として実行します。実行後React Devloper Toolsにて、動作時にはどういう構造になっているかを見てみます。
まず、ProviderタグにてAppがくくられて、store情報が渡っていることがわかります。
次にConnectタグで括られており、state情報が渡っています。
connectでの渡し方がreact-reduxの柔軟性なのですが、それ故に初見殺しになってます。connectについて、公式からのサンプルを使って実際にどのような動きをしているかを実際に動いた際の画面のハードコピーを見ながら説明していきます
connect
以下公式より引用
Connects a React component to a Redux store. connect is a facade around connectAdvanced, providing a convenient API for the most common use cases.
It does not modify the component class passed to it; instead, it returns a new, connected component class for you to use.
Reduxの「store」にReactがアクセスするための関数であり、そのユースケースがいっぱいあるよとあります。
connectのユースケース
connectのユースケースについて公式の12パターンをサンプルに組み込み、実際の動作を見ていきます。
1. store情報を渡さない
Inject just dispatch and don't listen to store
export default connect()(TodoApp)
上記を実装したサンプルは以下の通り
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import { connect } from 'react-redux';
class App extends Component {
render() {
const { app: { count } } = this.props.state;
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
<button onClick={ e => {this.props.dispatch({ type: "INC" });}}>INC: { count }</button>
</div>
);
}
}
export default connect()(App);
以下、実行画面
- Providerからdispach関数のみが渡されている。
- そのため、クリック時に「this.props.dispatch({ type: "INC" });」でActionが呼ぶことができる。
2. action creatorのみを渡す
Inject all action creators (addTodo, completeTodo, ...) without subscribing to the store
import * as actionCreators from './actionCreators'
export default connect(null, actionCreators)(TodoApp)
上記を実装したサンプルは以下の通り
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import { connect } from 'react-redux';
class App extends Component {
render() {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
<button onClick={ e => this.props.dispatch1()}>INC</button>
</div>
);
}
}
function mapDispatchToProps(dispatch) {
return { dispatch1: () => { dispatch({ type: "INC" }); } };
}
export default connect(null, mapDispatchToProps)(App);
- 今度は、dispatch1としてdispatch1()が渡っている。
- このため、this.props.dispatch1()でActionが呼び出される。
3. stateを直接渡す
Inject dispatch and every field in the global state
export default connect(state => state)(TodoApp)
上記を実装したサンプルは以下の通り
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import { connect } from 'react-redux';
class App extends Component {
render() {
const { app: { count } } = this.props;
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
<button onClick={ e => {this.props.dispatch({ type: "INC" });}}>INC: { count }</button>
</div>
);
}
}
export default connect(state => state)(App);
- store内のstate情報のみがpropsとして渡されている。「this.props」で渡されたstateを取得できる。
4. state内の一部の情報のみを渡す
function mapStateToProps(state) {
return { todos: state.todos }
}
export default connect(mapStateToProps)(TodoApp)
上記を実装したサンプルは以下の通り
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import { connect } from 'react-redux';
class App extends Component {
render() {
const { app: { count } } = this.props;
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
<button onClick={ e => {this.props.dispatch({ type: "INC" });}}>INC: { count }</button>
</div>
);
}
}
function mapStateToProps(state) {
return { app: state.app };
}
export default connect(mapStateToProps)(App);
- store内のstateのうち、appがappとして渡されている。
5. stateとactionの両方を渡す
Inject todos and all action creators
import * as actionCreators from './actionCreators'
function mapStateToProps(state) {
return { todos: state.todos }
}
export default connect(mapStateToProps, actionCreators)(TodoApp)
上記を実装したサンプルは以下の通り
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import { connect } from 'react-redux';
class App extends Component {
render() {
const { app: { count } } = this.props;
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
<button onClick={ e => this.props.dispatch1()}>INC: { count }</button>
</div>
);
}
}
function mapStateToProps(state) {
return { app: state.app };
}
function mapDispatchToProps(dispatch) {
return { dispatch1: () => { dispatch({ type: "INC" }); } };
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
- store内のstateのうち、appがappとして渡されている。
- dispatchについては、dispatch1としてdispatch1()が渡っている。このため、this.props.dispatch1()でActionが呼び出される。
6. actionについてbindActionCreatorsでbindして定義した名前で渡す
Inject todos and all action creators (addTodo, completeTodo, ...) as actions
import * as actionCreators from './actionCreators'
import { bindActionCreators } from 'redux';
function mapStateToProps(state) {
return { todos: state.todos };
}
function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators(actionCreators, dispatch) };
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoApp);
上記を実装したサンプルは以下の通り
import { createAction } from 'redux-actions';
export const INC = 'INC';
export const DEC = 'DEC';
export const dispatch1 = createAction(INC);
export const dispatch2 = createAction(DEC);
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actionCreators1 from './actionCreators1';
class App extends Component {
render() {
const { app: { count }, actions: { dispatch1, dispatch2 } } = this.props;
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
<button onClick={ e => dispatch1()}>INC: { count }</button>
<button onClick={ e => dispatch2()}>DEC: { count }</button>
</div>
);
}
}
function mapStateToProps(state) {
return { app: state.app };
}
function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators(actionCreators1, dispatch) };
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
- store内のstateのうち、appがappとして渡されている。
- actionsとしてactionCreators1で定義したdispatch1とdispatch2をまとめて渡っている。そのため、this.propsで「actions: { dispatch1, dispatch2 }」という形で取得でき、dispatch1()、dispatch2()でActionが呼び出される。
7. 複数のアクションのうち、1つのみをbindして渡す
Inject todos and a specific action creator (addTodo)
import { addTodo } from './actionCreators'
import { bindActionCreators } from 'redux';
function mapStateToProps(state) {
return { todos: state.todos }
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ addTodo }, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoApp);
上記を実装したサンプルは以下の通り
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { dispatch1 } from './actionCreators1';
class App extends Component {
render() {
const { app: { count }, dispatch1 } = this.props;
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
<button onClick={ e => dispatch1()}>INC: { count }</button>
</div>
);
}
}
function mapStateToProps(state) {
return { app: state.app };
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ dispatch1 }, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
- store内のstateのうち、appがappとして渡されている。
- actionsとしてactionCreators1で定義したdispatch1のみが渡っている。そのため、this.propsで「dispatch1」という形で取得でき、dispatch1()でActionが呼び出される。
8. actionについて複数のアクションを別々にまとめてbindActionCreatorsでbindしてそれぞれ定義した名前で渡す
Inject todos, todoActionCreators as todoActions, and counterActionCreators as counterActions
import * as todoActionCreators from './todoActionCreators'
import * as counterActionCreators from './counterActionCreators'
import { bindActionCreators } from 'redux'
function mapStateToProps(state) {
return { todos: state.todos }
}
function mapDispatchToProps(dispatch) {
return {
todoActions: bindActionCreators(todoActionCreators, dispatch),
counterActions: bindActionCreators(counterActionCreators, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoApp);
上記を実装したサンプルは以下の通り
import { createAction } from 'redux-actions';
export const INC = 'INC';
export const DEC = 'DEC';
export const dispatch1 = createAction(INC);
export const dispatch2 = createAction(DEC);
import { createAction } from 'redux-actions';
export const PLUS2 = 'PLUS2';
export const PLUS3 = 'PLUS3';
export const dispatch3 = createAction(PLUS2);
export const dispatch4 = createAction(PLUS3);
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actionCreators1 from './actionCreators1';
import * as actionCreators2 from './actionCreators2';
class App extends Component {
render() {
const { app: { count }, actions1: { dispatch1, dispatch2 }, actions2: { dispatch3, dispatch4 } } = this.props;
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
<button onClick={ e => dispatch1()}>INC: { count }</button>
<button onClick={ e => dispatch2()}>DEC: { count }</button>
<button onClick={ e => dispatch3()}>PLUS2: { count }</button>
<button onClick={ e => dispatch4()}>PLUS3: { count }</button>
</div>
);
}
}
function mapStateToProps(state) {
return { app: state.app };
}
function mapDispatchToProps(dispatch) {
return {
actions1: bindActionCreators(actionCreators1, dispatch),
actions2: bindActionCreators(actionCreators2, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
- store内のstateのうち、appがappとして渡されている
- actions1としてactionCreators1で定義したdispatch1とdispatch2をまとめて渡っている。
- actions2としてactionCreators2で定義したdispatch3とdispatch4をまとめて渡っている。
- そのため、this.propsで「actions1: { dispatch1, dispatch2 }」、「actions2: { dispatch3, dispatch4 }」という形で取得でき、dispatch1()、dispatch2()、dispatch3()、dispatch4()でActionが呼び出される。
9.actionについて複数のアクションを全部まとめてbindActionCreatorsでbindして渡す
Inject todos, and todoActionCreators and counterActionCreators together as actions
import * as todoActionCreators from './todoActionCreators'
import * as counterActionCreators from './counterActionCreators'
import { bindActionCreators } from 'redux'
function mapStateToProps(state) {
return { todos: state.todos }
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Object.assign({}, todoActionCreators, counterActionCreators), dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoApp)
上記を実装したサンプルは以下の通り
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actionCreators1 from './actionCreators1';
import * as actionCreators2 from './actionCreators2';
class App extends Component {
render() {
const { app: { count }, actions: { dispatch1, dispatch2, dispatch3, dispatch4 }} = this.props;
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
<button onClick={ e => dispatch1()}>INC: { count }</button>
<button onClick={ e => dispatch2()}>DEC: { count }</button>
<button onClick={ e => dispatch3()}>PLUS2: { count }</button>
<button onClick={ e => dispatch4()}>PLUS3: { count }</button>
</div>
);
}
}
function mapStateToProps(state) {
return { app: state.app };
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Object.assign({}, actionCreators1, actionCreators2), dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
- store内のstateのうち、appがappとして渡されている。
- actionsとしてactionCreators1で定義したdispatch1とdispatch2とactionCreators2で定義したdispatch3とdispatch4をまとめて渡っている。this.propsで「actions: { dispatch1, dispatch2, dispatch3, dispatch4 }」という形で取得でき、dispatch1()、dispatch2()、dispatch3()、dispatch4()でActionが呼び出される。
10. actionについて複数のアクションを全部まとめてbindActionCreatorsでbindしてpropsとして直接渡す
Inject todos, and all todoActionCreators and counterActionCreators directly as props
import * as todoActionCreators from './todoActionCreators'
import * as counterActionCreators from './counterActionCreators'
import { bindActionCreators } from 'redux'
function mapStateToProps(state) {
return { todos: state.todos }
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(Object.assign({}, todoActionCreators, counterActionCreators), dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoApp)
上記を実装したサンプルは以下の通り
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actionCreators1 from './actionCreators1';
import * as actionCreators2 from './actionCreators2';
class App extends Component {
render() {
const { app: { count }, dispatch1, dispatch2, dispatch3, dispatch4 } = this.props;
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
<button onClick={ e => dispatch1()}>INC: { count }</button>
<button onClick={ e => dispatch2()}>DEC: { count }</button>
<button onClick={ e => dispatch3()}>PLUS2: { count }</button>
<button onClick={ e => dispatch4()}>PLUS3: { count }</button>
</div>
);
}
}
function mapStateToProps(state) {
return { app: state.app };
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(Object.assign({}, actionCreators1, actionCreators2), dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
- store内のstateのうち、appがappとして渡されている。
- actionsとしてactionCreators1で定義したdispatch1とdispatch2とactionCreators2で定義したdispatch3とdispatch4をまとめて渡っている。
- そのため、this.propsで「dispatch1, dispatch2, dispatch3, dispatch4」という形で取得でき、dispatch1()、dispatch2()、dispatch3()、dispatch4()でActionが呼び出される。
11. [WIP]
Inject todos of a specific user depending on props
import * as actionCreators from './actionCreators'
function mapStateToProps(state, ownProps) {
return { todos: state.todos[ownProps.userId] }
}
export default connect(mapStateToProps)(TodoApp)
12. [WIP]
Inject todos of a specific user depending on props, and inject props.userId into the action
import * as actionCreators from './actionCreators'
function mapStateToProps(state) {
return { todos: state.todos }
}
function mergeProps(stateProps, dispatchProps, ownProps) {
return Object.assign({}, ownProps, {
todos: stateProps.todos[ownProps.userId],
addTodo: (text) => dispatchProps.addTodo(ownProps.userId, text)
})
}
export default connect(mapStateToProps, actionCreators, mergeProps)(TodoApp)
まとめ
これだけのパターンを覚えてられっか!!!
パターンについては、よく使いそうなパターンについては、ある程度素振りをして覚えておいてソースを追う際に「あ~、こんなパターンあったね」くらいできれば、後々楽になると思います。
(個人的にはパターン6以降はめったに使わないと思うので覚えなくてもいいんじゃないと思うレベル。11、12についてはどう実装したらいいのかすら分からず、一旦保留しています)
記事について、間違っている箇所がありましたらご指摘のほどお願い致します。
公式ドキュメント
参考サイト
手っ取り早くReduxがどのようなものか理解する~react-redux編~
※、Reduxの初見殺しについては、こちらのサイトを参照
Reduxでコンポーネントを再利用する