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

## react-redux
Official React bindings for Redux.
https://github.com/reactjs/react-redux

## まとめ
* 元のComponentからstoreにアクセスできるようにするbinding。
* 結果、元のComponentからdispatchなどのstoreのmethodにアクセスできるようになる。

## 概要
Providerとconnectから成る。

## Provider
ChildrenのComponentにstoreをprivideする。
補足： Children

```jsx:Children
<Provider store={hogehoe}>
  <App /> <- Children
</Provider>
```

```js:Provider.js
export default class Provider extends Component {
  getChildContext() {
    return { store: this.store }
  }

  constructor(props, context) {
    super(props, context)
    this.store = props.store
  }

  render() {
    return Children.only(this.props.children)
  }
}
```

Childrenにstoreをpropsとしてprovideするだけ。

## connect

これが重要。
ComponentをWrapしたConnectComponentを返す。
https://github.com/mridgway/hoist-non-react-statics/blob/master/index.js
↑で元のComponentにあるプロパティを新しいConnectComponentにコピーしている。

```js:connect.js
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
  const shouldSubscribe = Boolean(mapStateToProps)
  const mapState = mapStateToProps || defaultMapStateToProps

  let mapDispatch
  if (typeof mapDispatchToProps === 'function') {
    mapDispatch = mapDispatchToProps
  } else if (!mapDispatchToProps) {
    mapDispatch = defaultMapDispatchToProps
  } else {
    mapDispatch = wrapActionCreators(mapDispatchToProps)
  }

  const finalMergeProps = mergeProps || defaultMergeProps
  const { pure = true, withRef = false } = options
  const checkMergedEquals = pure && finalMergeProps !== defaultMergeProps

  // Helps track hot reloading.
  const version = nextVersion++

  ...,
```

`mapStateToProps`がnullでなければ、subcribeする。
あとは引数チェックと初期値の代入処理。

```js:connect.js
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
  ...,

  return function wrapWithConnect(WrappedComponent) {
    ...,

    return hoistStatics(Connect, WrappedComponent)
  }
}
```

React Componentを引数にして新しいConnect Componentを返すfunctionがreturnされる。
wrapWithConnectの中身をみていく。

```js:wrapWithConnect
  function wrapWithConnect(WrappedComponent) {
    const connectDisplayName = `Connect(${getDisplayName(WrappedComponent)})`

    function checkStateShape(props, methodName) {
      if (!isPlainObject(props)) {
        warning(
          `${methodName}() in ${connectDisplayName} must return a plain object. ` +
          `Instead received ${props}.`
        )
      }
    }

    function computeMergedProps(stateProps, dispatchProps, parentProps) {
      const mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps)
      if (process.env.NODE_ENV !== 'production') {
        checkStateShape(mergedProps, 'mergeProps')
      }
      return mergedProps
    }

    class Connect extends Component {
      ...,
    }

    ...,
  }
```

まずは、Connectの外側でmethod定義。
本丸のConnectの内部を見てみる。

```js:Connect
    class Connect extends Component {
      shouldComponentUpdate() {
        return !pure || this.haveOwnPropsChanged || this.hasStoreStateChanged
      }

      constructor(props, context) {
        super(props, context)
        this.version = version
        this.store = props.store || context.store

        invariant(this.store,
          `Could not find "store" in either the context or ` +
          `props of "${connectDisplayName}". ` +
          `Either wrap the root component in a <Provider>, ` +
          `or explicitly pass "store" as a prop to "${connectDisplayName}".`
        )

        const storeState = this.store.getState()
        this.state = { storeState }
        this.clearCache()
      }

      ...,

      trySubscribe() {
        if (shouldSubscribe && !this.unsubscribe) {
          this.unsubscribe = this.store.subscribe(this.handleChange.bind(this))
          this.handleChange()
        }
      }

      tryUnsubscribe() {
        if (this.unsubscribe) {
          this.unsubscribe()
          this.unsubscribe = null
        }
      }
      componentDidMount() {
        this.trySubscribe()
      }

      componentWillReceiveProps(nextProps) {
        if (!pure || !shallowEqual(nextProps, this.props)) {
          this.haveOwnPropsChanged = true
        }
      }

      componentWillUnmount() {
        this.tryUnsubscribe()
        this.clearCache()
      }

      clearCache() {
        this.dispatchProps = null
        this.stateProps = null
        this.mergedProps = null
        this.haveOwnPropsChanged = true
        this.hasStoreStateChanged = true
        this.haveStatePropsBeenPrecalculated = false
        this.statePropsPrecalculationError = null
        this.renderedElement = null
        this.finalMapDispatchToProps = null
        this.finalMapStateToProps = null
      }

      handleChange() {
        if (!this.unsubscribe) {
          return
        }

        const storeState = this.store.getState()
        const prevStoreState = this.state.storeState
        if (pure && prevStoreState === storeState) {
          return
        }

        if (pure && !this.doStatePropsDependOnOwnProps) {
          const haveStatePropsChanged = tryCatch(this.updateStatePropsIfNeeded, this)
          if (!haveStatePropsChanged) {
            return
          }
          if (haveStatePropsChanged === errorObject) {
            this.statePropsPrecalculationError = errorObject.value
          }
          this.haveStatePropsBeenPrecalculated = true
        }

        this.hasStoreStateChanged = true
        this.setState({ storeState })
      }

      ...,
    }
```

まずは、React組み込みのmethodからみていく。

* shouldComponentUpdate
state, propsが変化したときにUpdateするか

* constructor
storeには直接propsとして渡されるか、Providerを介してcontextとして渡されるかの2種類が想定されているようだ。
`clearCache`でプロパティをすべてclearしている。

* componentDidMount
Componentがmountされた後の処理。
`trySubscribe`でhandleChangeをsubscribeしている。
shouldSubscribeがtrue = mapStateToPropsがnullではない

* componentWillReceiveProps
propsがupdateされた時に、propsが更新されていれば`haveOwnPropsChanged`にtrueが設定される。

* componentWillUnmount
Componentがumountされる前の処理。
`tryUnsubscribe`でsubscribeのlistnerを削除して、`clearCache`でプロパティをすべてclearしている。

storeにactionがdispatchされた時の処理であるhandlerChangeをじっくり見てみる。

* unsubscribeがnull、つまりsubscribeされていないときは何もしない

* `prevStoreState === storeState`
storeの状態が変化していなければ何もしない

* `doStatePropsDependOnOwnProps == false`のときに`updateStatePropsIfNeeded`が実行される

```js:updateStatePropsIfNeeded
     computeStateProps(store, props) {
        if (!this.finalMapStateToProps) {
          return this.configureFinalMapState(store, props)
        }

        const state = store.getState()
        const stateProps = this.doStatePropsDependOnOwnProps ?
          this.finalMapStateToProps(state, props) :
          this.finalMapStateToProps(state)

        if (process.env.NODE_ENV !== 'production') {
          checkStateShape(stateProps, 'mapStateToProps')
        }
        return stateProps
      }

      configureFinalMapState(store, props) {
        const mappedState = mapState(store.getState(), props)
        const isFactory = typeof mappedState === 'function'

        this.finalMapStateToProps = isFactory ? mappedState : mapState
        this.doStatePropsDependOnOwnProps = this.finalMapStateToProps.length !== 1

        if (isFactory) {
          return this.computeStateProps(store, props)
        }

        if (process.env.NODE_ENV !== 'production') {
          checkStateShape(mappedState, 'mapStateToProps')
        }
        return mappedState
      }

      computeDispatchProps(store, props) {
        if (!this.finalMapDispatchToProps) {
          return this.configureFinalMapDispatch(store, props)
        }

        const { dispatch } = store
        const dispatchProps = this.doDispatchPropsDependOnOwnProps ?
          this.finalMapDispatchToProps(dispatch, props) :
          this.finalMapDispatchToProps(dispatch)

        if (process.env.NODE_ENV !== 'production') {
          checkStateShape(dispatchProps, 'mapDispatchToProps')
        }
        return dispatchProps
      }

      configureFinalMapDispatch(store, props) {
        const mappedDispatch = mapDispatch(store.dispatch, props)
        const isFactory = typeof mappedDispatch === 'function'

        this.finalMapDispatchToProps = isFactory ? mappedDispatch : mapDispatch
        this.doDispatchPropsDependOnOwnProps = this.finalMapDispatchToProps.length !== 1

        if (isFactory) {
          return this.computeDispatchProps(store, props)
        }

        if (process.env.NODE_ENV !== 'production') {
          checkStateShape(mappedDispatch, 'mapDispatchToProps')
        }
        return mappedDispatch
      }

      updateStatePropsIfNeeded() {
        const nextStateProps = this.computeStateProps(this.store, this.props)
        if (this.stateProps && shallowEqual(nextStateProps, this.stateProps)) {
          return false
        }

        this.stateProps = nextStateProps
        return true
      }
```

色々やってますが、mapState=mapStateToPropsしだいですね。
結果としては、nextStatePropsが今のpropsから更新されていればtrueが返される。

* `haveStatePropsBeenPrecalculated`と`hasStoreStateChanged`にtrueが設定される
* `setState({ storeState })`でstateを更新

最後にConnectのrenderを見ていく。

```js:render
      render() {
        ...,

        if (
          haveStatePropsChanged ||
          haveDispatchPropsChanged ||
          haveOwnPropsChanged
        ) {
          haveMergedPropsChanged = this.updateMergedPropsIfNeeded()
        } else {
          haveMergedPropsChanged = false
        }

        if (!haveMergedPropsChanged && renderedElement) {
          return renderedElement
        }

        if (withRef) {
          this.renderedElement = createElement(WrappedComponent, {
            ...this.mergedProps,
            ref: 'wrappedInstance'
          })
        } else {
          this.renderedElement = createElement(WrappedComponent,
            this.mergedProps
          )
        }

        return this.renderedElement
      }
```

renderされるのは最終的に`renderElement`となる。
renderElementはWrappedComponent(元のComponent)に`mergedProps`をpropsとして与えたものとなる。
`mergedProps`が与えられる`updateMergedPropsIfNeeded`をみていく。

```js:updateMergedPropsIfNeeded
      updateMergedPropsIfNeeded() {
        const nextMergedProps = computeMergedProps(this.stateProps, this.dispatchProps, this.props)
        if (this.mergedProps && checkMergedEquals && shallowEqual(nextMergedProps, this.mergedProps)) {
          return false
        }

        this.mergedProps = nextMergedProps
        return true
      }
```

```js:computeMergedProps
    function computeMergedProps(stateProps, dispatchProps, parentProps) {
      const mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps)
      if (process.env.NODE_ENV !== 'production') {
        checkStateShape(mergedProps, 'mergeProps')
      }
      return mergedProps
    }
```

3種のpropsをmergeするfunctionに渡してmergeされた新しいpropsを生成する。
defaultの処理だと、すべてのプロパティが1つのObjectに展開される。
よって、元のComponentから`this.props.dispatch`のようにstoreのmethodをprops越しにアクセスできる。

これでConnectはなんとなくわかった気になる。

最終的に返されるComponentは以下。

```js:Connect
    ...,

    Connect.displayName = connectDisplayName
    Connect.WrappedComponent = WrappedComponent
    Connect.contextTypes = {
      store: storeShape
    }
    Connect.propTypes = {
      store: storeShape
    }

    ...,

    return hoistStatics(Connect, WrappedComponent)
  }
}
```

元のComponentのプロパティをコピーしたConnectComponentが生成される。
contextTypesは必須。これがないとProviderから与えられたstoreにアクセスできない。
propTypesはないとeslintでエラーが出るのでReactを使うならほぼ必須。