LoginSignup
8
10

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-04-29

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

Children
<Provider store={hogehoe}>
  <App /> <- Children
</Provider>
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にコピーしている。

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する。
あとは引数チェックと初期値の代入処理。

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の中身をみていく。

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の内部を見てみる。

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が実行される

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が返される。

  • haveStatePropsBeenPrecalculatedhasStoreStateChangedにtrueが設定される
  • setState({ storeState })でstateを更新

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

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をみていく。

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
      }
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は以下。

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を使うならほぼ必須。

8
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
10