katekichiのゆるブログ

普段の作業メモや日常の出来事とか

【自由研究③】react-reduxのconnectについて調査してみた話

Reduxで実装する場合にReact部とRedux部の繋ぎ込みに react-redux が有効ということは、分かったのですがどうもconnect() に渡している引数や、戻り値として何が生成されるのか分からず 結果として、なんでconnect()使うと幸せになれるのか分からなかったため 調査してみました。

github.com


なぜ分からないのか?

なぜ分からないのか考えてみました。

  1. チュートリアル等で、手を動かしただけでドキュメントの読み込みが足りない
  2. そもそもReduxの理解が不足している

ドキュメントを再度読んでみる。

Usage with React · Redux を読んでまずは まとめてみることにしました。


Presentational / Container componentについて

再利用性を考慮して、Presentational(プレゼンテーショナル) / Container(コンテナ) component の2つに分離されるようです。 2つのコンポーネントの役割を簡単にまとめてみましたが、ざっくりいうと

  • 見た目(表示)担当のPresentational component
  • ロジック担当のContainer component

って感じのようです。

medium.com

また、調べていたら良い記事を既に書いていらっしゃる方がいたので参考にさせて 頂きました。

qiita.com

Presentational Componentsとは

  • 役割:見た目の実装
  • データ取得時:propsより取得
  • データ更新時:propsのcallback関数
  • コンポーネントの作成:自分で作る

実際のイメージとしては、こんな感じです(こちらからの抜粋です)。

import React from 'react'

export default React.createClass({
    render() {
        const {
            text,
            onButtonClick,
        } = this.props

        return (
            <div>
                <p>{text}</p>
                <button onClick={onButtonClick}>Click!</button>
            </div>
        )
    }
})
  • {text}の表示は、props経由
  • ボタンクリック時の{onButtonClick}の実装もprops経由

Container Componentsとは

  • 役割:データフェッチやStateの更新
  • データ取得時:reduxのstateにより取得
  • データ更新時:reduxのactionにより更新
  • コンポーネントの作成:react-reduxが生成

実際のイメージとしては、こんな感じです(こちらからの抜粋です)。

import { connect } from 'react-redux'

import SomePresentationalComponent from 'some-presentational-compnent'

const mapStateToProps = (state, ownProps) => {
    return {
        // blah blah blah
    }
}

const mapDispatchToProps = dispatch => {
    return {
        // blah blah blah
    }
}

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(SomePresentationalComponent)

はい、出てきましたー。connect() この辺りから、分からなくなってきます・・・。

こちらに以下のようなコメントが ありました。

mapStateToProps(state, ownProps)は,store.getState()の結果を第一引数に,このContainer component
へ渡されたpropsを第二引数にして呼び出される関数で,これらのstateとpropsを使って子のPresentational
 componentにpropsとして渡す値を生成します.

mapDispatchToProps(dispatch)は,store.dispatchを第一引数にして呼び出される関数で,
子のPresentational componentにpropsとして渡す値(というか,コールバック関数)を生成します.
通常,このコールバック関数では,引数として渡されているdispatchを呼び出し,子のPresentational Componentが
StoreへActionを送信できるようにしておきます.
ボタン押下時に呼ばれる処理などもコールバック関数として書いてPresentational componentにpropsとして
渡すと言いましたが,このコールバック関数を作るためのものと思っておけばいいと思います.

すごく丁寧な解説をして頂いているのですが、そもそも以下が良く分かっていないことが判明しました。 その結果として、connect()の引数から何が生成されるのかが分かっていないのだという結論に至りました。

  • Store.getState()
  • Store.dispatch()

以下のドキュメントで各メソッドの仕様を調査しました http://redux.js.org/docs/api/Store.html

getState()

  • 要訳すると、現状のStoreが持つ最新のState郡を返す。

dispatch()

  • 要訳すると、dispatchは、唯一Stateを変更する手段。
  • 引数として、actionを渡す。actionとは、UIイベント、 networkのcallback等を指す。 actionのfieldには、"type"が必要

これを踏まえて

上記を踏まえて、connect()の解説を読むとより理解が深まりました。 要は、Presentational componentに対して、propsとして、stateと stateを更新するためのCallback関数として渡してるだけのようです。

変に、connect()の第1引数と、第2引数がなにかしらの対応づけがされているのでは と推察していたことがそもそもの間違いでした・・

ちゃんとドキュメント読めよと・・

以前作成した、以下のコードを「React Developer Tools」で確認(スクショ参照)すると、それが良く分かりました。CalculatorContainerコンテナ(Presentational component)に対して、propsとして引数に渡していることが 分かります。

import * as actions from '../actions';

const mapState = (state, ownProps) => ({
    calculator: state.calculator,
});

function mapDispatch(dispatch) {
    return {
        actions: bindActionCreators(actions, dispatch)
    }
}

export default connect(mapState, mapDispatch)(CalculatorContainer);

f:id:katekichi:20170815185231p:plain

(上記のスクショが見えづらかったため、Reactコードを転記しました。)

<Provider store={dispatch: dispatch() subscribe: subscribe() getState: getState() replaceReducer: replaceReducer()}>
 <Connect(CalculatorContainer)>
   <CalculatorContainer calculator={inputValue: 0, resultValue: 0,showingResult: false} actions={onNumClick:fn(), onPlusClickfn():, onMinusClick:fn()}/>
 </Connect(CalculatorContainer)>
</Provider>

そして再びconnet()へ

ここまでで、connet()の挙動に関しては、理解できました。 最後に謎なのが・・。

export default  connect(mapState, mapDispatch)(CalculatorContainer);

という呼び出し時の記述です。connect(mapState, mapDispatch)の戻り値になんらかの関数が 戻ってきて、それに対してコンテナ(Presentational component)に引数に渡して、Class生成している ことが予想されます。

connect()の戻り値をドキュメントで確認すると

Returns

A higher-order React component class that passes state and action creators into your component derived from the supplied arguments. This is created by connectAdvanced, and details of this higher-order component are covered there.

とありますが、higher-order React component classというなんとも説明しづらい記述があったため 実際にreact-reduxの実装を見てみることにしました。

connect.jsを読む

src/connect/connect.jsが対象です。 ソース内のconnectHOC(selectorFactory)が ポイントのようです!。 connectAdvanced()の実装を確認します。

connectAdvanced.jsを読む

src/components/connectAdvanced.jsが対象です。 ソース を確認すると、Connectコンポーネントを作成し、それの子コンポーネントとして引数のコンテナ(Presentational component) を使用しているようです。

なので、connect()の戻りは、

「higher-order React component class」 == 「Connectコンポーネント

ということになります。


まとめ

長々と調査してきましたが、自分なりの結論としては以下に至りました。

connect()とは

  • 引数には、stateとstateを更新するCallback関数を渡す(引数のPresentational componentはpropsでのみ扱える)。
  • Connectコンポーネントを返す(引数のPresentational componentを子コンポーネントして持つ)。

connect()のへの疑問から様々な箇所まで波及しましたが、そもそもReduxの理解不足という一言につきました・・。 ライブラリの内部実装を確認することは、いつも時間の無さにかまけて出来てなかったのですが 今回のconnect()の実装が腑に落ちなく、別のエンジニアに説明する材料が乏しかったため今回の確認作業を実施しました。

中々、ヘビーな作業でしたが、ライブラリのメリデメの理解は断然高まりますので、まとまった時間がある際に再度トライ してみたいです。

最後に

これを読まれている方で、ちょっと認識違うんじゃないかとか、この辺が分かりづらいというご意見ありましたら 是非、コメント頂ければと思います!