ReactとReduxを結ぶパッケージ「react-redux」についてconnectの実装パターンを試す

  • 145
    Like
  • 2
    Comment

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さんより頂いたお言葉を引用したいと思います。

柔軟性が素人を殺す」この一言に限ります。
公式の引用やサンプルを使ってどういった動きをするのかというのを解説していきます。

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つ
1. Reactコンポーネント内でreact-reduxのconnect()関数を使えるようにすること
2. ラップしたコンポーネントにstore情報を渡すこと

項目1についてはReactをそれなりにやっていると理解できると思いますが、項目2ですでに初見殺しだと思います。
実際のコードを使って説明したいと思います。

create-react-appで雛形を作り、ボタンを押すとカウントされるReduxの構成でアプリを作成します。

/index.js
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')
);
App.js
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にて、動作時にはどういう構造になっているかを見てみます。

devtool01.png

まず、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)

上記を実装したサンプルは以下の通り

App.js
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);

以下、実行画面

devtool02.png

  • 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)

上記を実装したサンプルは以下の通り

App.js
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);

以下、実行画面
devtool03.png

  • 今度は、dispatch1としてdispatch1()が渡っている。
  • このため、this.props.dispatch1()でActionが呼び出される。

3. stateを直接渡す

Inject dispatch and every field in the global state

export default connect(state => state)(TodoApp)

上記を実装したサンプルは以下の通り

App.js
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);

以下、実行画面
devtool04.png

  • store内のstate情報のみがpropsとして渡されている。「this.props」で渡されたstateを取得できる。

4. state内の一部の情報のみを渡す

Inject dispatch and todos

function mapStateToProps(state) {
  return { todos: state.todos }
}

export default connect(mapStateToProps)(TodoApp)

上記を実装したサンプルは以下の通り

App.js
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);

以下、実行画面
devtool05.png

  • 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)

上記を実装したサンプルは以下の通り

App.js
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);

以下、実行画面
devtool06.png

  • 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);

上記を実装したサンプルは以下の通り

actionCreators1.js
import { createAction } from 'redux-actions';

export const INC = 'INC';
export const DEC = 'DEC';

export const dispatch1 = createAction(INC);
export const dispatch2 = createAction(DEC);
App.js
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);

以下、実行画面
devtool07.png

  • 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);

上記を実装したサンプルは以下の通り

App.js
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);

以下、実行画面
devtool10.png

  • 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);

上記を実装したサンプルは以下の通り

actionCreators1.js
import { createAction } from 'redux-actions';

export const INC = 'INC';
export const DEC = 'DEC';

export const dispatch1 = createAction(INC);
export const dispatch2 = createAction(DEC);
actionCreators2.js
import { createAction } from 'redux-actions';

export const PLUS2 = 'PLUS2';
export const PLUS3 = 'PLUS3';

export const dispatch3 = createAction(PLUS2);
export const dispatch4 = createAction(PLUS3);
App.js
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);

以下、実行画面
devtool08.png

  • 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)

上記を実装したサンプルは以下の通り

App.js
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);

以下、実行画面
devtool09.png

  • 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)

上記を実装したサンプルは以下の通り

App.js
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);

以下、実行画面
devtool11.png

  • 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についてはどう実装したらいいのかすら分からず、一旦保留しています)

記事について、間違っている箇所がありましたらご指摘のほどお願い致します。

公式ドキュメント

参考サイト

「React/Reduxチュートリアル」

手っ取り早くReduxがどのようなものか理解する~react-redux編~

※、Reduxの初見殺しについては、こちらのサイトを参照
Reduxでコンポーネントを再利用する