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
<Provider store={hogehoe}>
<App /> <- Children
</Provider>
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にコピーしている。
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する。
あとは引数チェックと初期値の代入処理。
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
...,
return function wrapWithConnect(WrappedComponent) {
...,
return hoistStatics(Connect, WrappedComponent)
}
}
React Componentを引数にして新しいConnect Componentを返すfunctionがreturnされる。
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の内部を見てみる。
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
が実行される
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を見ていく。
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() {
const nextMergedProps = computeMergedProps(this.stateProps, this.dispatchProps, this.props)
if (this.mergedProps && checkMergedEquals && shallowEqual(nextMergedProps, this.mergedProps)) {
return false
}
this.mergedProps = nextMergedProps
return true
}
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.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を使うならほぼ必須。