diff --git a/src/components/connect.js b/src/components/connect.js index 5840d7e17..cdb268909 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -6,6 +6,8 @@ import isPlainObject from 'lodash/isPlainObject' import hoistStatics from 'hoist-non-react-statics' import invariant from 'invariant' + + const defaultMapStateToProps = state => ({}) // eslint-disable-line no-unused-vars const defaultMapDispatchToProps = dispatch => ({ dispatch }) const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({ @@ -31,281 +33,300 @@ function checkStateShape(stateProps, dispatch) { // Helps track hot reloading. let nextVersion = 0 -export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { - const shouldSubscribe = Boolean(mapStateToProps) - const mapState = mapStateToProps || defaultMapStateToProps - const mapDispatch = isPlainObject(mapDispatchToProps) ? - wrapActionCreators(mapDispatchToProps) : - mapDispatchToProps || defaultMapDispatchToProps - - const finalMergeProps = mergeProps || defaultMergeProps - const checkMergedEquals = finalMergeProps !== defaultMergeProps - const { pure = true, withRef = false } = options - - // Helps track hot reloading. - const version = nextVersion++ - - function computeMergedProps(stateProps, dispatchProps, parentProps) { - const mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps) - invariant( - isPlainObject(mergedProps), - '`mergeProps` must return an object. Instead received %s.', - mergedProps - ) - return mergedProps - } +export function createConnect(ConnectComponentClass) { + return (mapStateToProps, mapDispatchToProps, mergeProps, options = {}) => { + const shouldSubscribe = Boolean(mapStateToProps) + const mapState = mapStateToProps || defaultMapStateToProps + const mapDispatch = isPlainObject(mapDispatchToProps) ? + wrapActionCreators(mapDispatchToProps) : + mapDispatchToProps || defaultMapDispatchToProps + + const finalMergeProps = mergeProps || defaultMergeProps + const checkMergedEquals = finalMergeProps !== defaultMergeProps + const { pure = true, withRef = false } = options + + // Helps track hot reloading. + const version = nextVersion++ + + function computeMergedProps(stateProps, dispatchProps, parentProps) { + const mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps) + invariant( + isPlainObject(mergedProps), + '`mergeProps` must return an object. Instead received %s.', + mergedProps + ) + return mergedProps + } - return function wrapWithConnect(WrappedComponent) { - class Connect extends Component { - shouldComponentUpdate() { - return !pure || this.haveOwnPropsChanged || this.hasStoreStateChanged + return function wrapWithConnect(WrappedComponent) { + class InternalConnectClass extends ConnectComponentClass { + constructor(props, context) { + super(props, context) + + this.pure = pure + this.version = version + this.withRef = withRef + this.mapState = mapState + this.mapDispatch = mapDispatch + this.shouldSubscribe = shouldSubscribe + this.computeMergedProps = computeMergedProps + this.checkMergedEquals = checkMergedEquals + } } - 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 "${this.constructor.displayName}". ` + - `Either wrap the root component in a , ` + - `or explicitly pass "store" as a prop to "${this.constructor.displayName}".` - ) - - const storeState = this.store.getState() - this.state = { storeState } - this.clearCache() + InternalConnectClass.displayName = `${getDisplayName(ConnectComponentClass)}(${getDisplayName(WrappedComponent)})` + InternalConnectClass.WrappedComponent = WrappedComponent + InternalConnectClass.contextTypes = { + store: storeShape } + InternalConnectClass.propTypes = { + store: storeShape + } + + if (process.env.NODE_ENV !== 'production') { + InternalConnectClass.prototype.componentWillUpdate = function componentWillUpdate() { + if (this.version === version) { + return + } - computeStateProps(store, props) { - if (!this.finalMapStateToProps) { - return this.configureFinalMapState(store, props) + // We are hot reloading! + this.version = version + this.trySubscribe() + this.clearCache() } + } - const state = store.getState() - const stateProps = this.doStatePropsDependOnOwnProps ? - this.finalMapStateToProps(state, props) : - this.finalMapStateToProps(state) + return hoistStatics(hoistStatics(InternalConnectClass, ConnectComponentClass), WrappedComponent) + } + } +} - return checkStateShape(stateProps) - } +export class Connect extends Component { - configureFinalMapState(store, props) { - const mappedState = mapState(store.getState(), props) - const isFactory = typeof mappedState === 'function' + shouldComponentUpdate() { + return !this.pure || this.haveOwnPropsChanged || this.hasStoreStateChanged + } - this.finalMapStateToProps = isFactory ? mappedState : mapState - this.doStatePropsDependOnOwnProps = this.finalMapStateToProps.length !== 1 + constructor(props, context) { + super(props, context) + this.store = props.store || context.store - return isFactory ? - this.computeStateProps(store, props) : - checkStateShape(mappedState) - } + invariant(this.store, + `Could not find "store" in either the context or ` + + `props of "${this.constructor.displayName}". ` + + `Either wrap the root component in a , ` + + `or explicitly pass "store" as a prop to "${this.constructor.displayName}".` + ) - computeDispatchProps(store, props) { - if (!this.finalMapDispatchToProps) { - return this.configureFinalMapDispatch(store, props) - } + const storeState = this.store.getState() + this.state = { storeState } + this.clearCache() + } - const { dispatch } = store - const dispatchProps = this.doDispatchPropsDependOnOwnProps ? - this.finalMapDispatchToProps(dispatch, props) : - this.finalMapDispatchToProps(dispatch) + computeStateProps(store, props) { + if (!this.finalMapStateToProps) { + return this.configureFinalMapState(store, props) + } - return checkStateShape(dispatchProps, true) - } + const state = store.getState() + const stateProps = this.doStatePropsDependOnOwnProps ? + this.finalMapStateToProps(state, props) : + this.finalMapStateToProps(state) - configureFinalMapDispatch(store, props) { - const mappedDispatch = mapDispatch(store.dispatch, props) - const isFactory = typeof mappedDispatch === 'function' + return checkStateShape(stateProps) + } - this.finalMapDispatchToProps = isFactory ? mappedDispatch : mapDispatch - this.doDispatchPropsDependOnOwnProps = this.finalMapDispatchToProps.length !== 1 + configureFinalMapState(store, props) { + const mappedState = this.mapState(store.getState(), props) + const isFactory = typeof mappedState === 'function' - return isFactory ? - this.computeDispatchProps(store, props) : - checkStateShape(mappedDispatch, true) - } + this.finalMapStateToProps = isFactory ? mappedState : this.mapState + this.doStatePropsDependOnOwnProps = this.finalMapStateToProps.length !== 1 - updateStatePropsIfNeeded() { - const nextStateProps = this.computeStateProps(this.store, this.props) - if (this.stateProps && shallowEqual(nextStateProps, this.stateProps)) { - return false - } + return isFactory ? + this.computeStateProps(store, props) : + checkStateShape(mappedState) + } - this.stateProps = nextStateProps - return true - } + computeDispatchProps(store, props) { + if (!this.finalMapDispatchToProps) { + return this.configureFinalMapDispatch(store, props) + } - updateDispatchPropsIfNeeded() { - const nextDispatchProps = this.computeDispatchProps(this.store, this.props) - if (this.dispatchProps && shallowEqual(nextDispatchProps, this.dispatchProps)) { - return false - } + const { dispatch } = store + const dispatchProps = this.doDispatchPropsDependOnOwnProps ? + this.finalMapDispatchToProps(dispatch, props) : + this.finalMapDispatchToProps(dispatch) - this.dispatchProps = nextDispatchProps - return true - } + return checkStateShape(dispatchProps, true) + } - updateMergedPropsIfNeeded() { - const nextMergedProps = computeMergedProps(this.stateProps, this.dispatchProps, this.props) - if (this.mergedProps && checkMergedEquals && shallowEqual(nextMergedProps, this.mergedProps)) { - return false - } + configureFinalMapDispatch(store, props) { + const mappedDispatch = this.mapDispatch(store.dispatch, props) + const isFactory = typeof mappedDispatch === 'function' - this.mergedProps = nextMergedProps - return true - } + this.finalMapDispatchToProps = isFactory ? mappedDispatch : this.mapDispatch + this.doDispatchPropsDependOnOwnProps = this.finalMapDispatchToProps.length !== 1 - isSubscribed() { - return typeof this.unsubscribe === 'function' - } + return isFactory ? + this.computeDispatchProps(store, props) : + checkStateShape(mappedDispatch, true) + } - trySubscribe() { - if (shouldSubscribe && !this.unsubscribe) { - this.unsubscribe = this.store.subscribe(this.handleChange.bind(this)) - this.handleChange() - } - } + updateStatePropsIfNeeded() { + const nextStateProps = this.computeStateProps(this.store, this.props) + if (this.stateProps && shallowEqual(nextStateProps, this.stateProps)) { + return false + } - tryUnsubscribe() { - if (this.unsubscribe) { - this.unsubscribe() - this.unsubscribe = null - } - } + this.stateProps = nextStateProps + return true + } - componentDidMount() { - this.trySubscribe() - } + updateDispatchPropsIfNeeded() { + const nextDispatchProps = this.computeDispatchProps(this.store, this.props) + if (this.dispatchProps && shallowEqual(nextDispatchProps, this.dispatchProps)) { + return false + } - componentWillReceiveProps(nextProps) { - if (!pure || !shallowEqual(nextProps, this.props)) { - this.haveOwnPropsChanged = true - } - } + this.dispatchProps = nextDispatchProps + return true + } - componentWillUnmount() { - this.tryUnsubscribe() - this.clearCache() - } + updateMergedPropsIfNeeded() { + const nextMergedProps = this.computeMergedProps(this.stateProps, this.dispatchProps, this.props) + if (this.mergedProps && this.checkMergedEquals && shallowEqual(nextMergedProps, this.mergedProps)) { + return false + } - clearCache() { - this.dispatchProps = null - this.stateProps = null - this.mergedProps = null - this.haveOwnPropsChanged = true - this.hasStoreStateChanged = true - this.renderedElement = null - this.finalMapDispatchToProps = null - this.finalMapStateToProps = null - } + this.mergedProps = nextMergedProps + return true + } - handleChange() { - if (!this.unsubscribe) { - return - } + isSubscribed() { + return typeof this.unsubscribe === 'function' + } - const prevStoreState = this.state.storeState - const storeState = this.store.getState() + trySubscribe() { + if (this.shouldSubscribe && !this.unsubscribe) { + this.unsubscribe = this.store.subscribe(this.handleChange.bind(this)) + this.handleChange() + } + } - if (!pure || prevStoreState !== storeState) { - this.hasStoreStateChanged = true - this.setState({ storeState }) - } - } + tryUnsubscribe() { + if (this.unsubscribe) { + this.unsubscribe() + this.unsubscribe = null + } + } - getWrappedInstance() { - invariant(withRef, - `To access the wrapped instance, you need to specify ` + - `{ withRef: true } as the fourth argument of the connect() call.` - ) + componentDidMount() { + this.trySubscribe() + } - return this.refs.wrappedInstance - } + componentWillReceiveProps(nextProps) { + if (!this.pure || !shallowEqual(nextProps, this.props)) { + this.haveOwnPropsChanged = true + } + } - render() { - const { - haveOwnPropsChanged, - hasStoreStateChanged, - renderedElement - } = this - - this.haveOwnPropsChanged = false - this.hasStoreStateChanged = false - - let shouldUpdateStateProps = true - let shouldUpdateDispatchProps = true - if (pure && renderedElement) { - shouldUpdateStateProps = hasStoreStateChanged || ( - haveOwnPropsChanged && this.doStatePropsDependOnOwnProps - ) - shouldUpdateDispatchProps = - haveOwnPropsChanged && this.doDispatchPropsDependOnOwnProps - } + componentWillUnmount() { + this.tryUnsubscribe() + this.clearCache() + } - let haveStatePropsChanged = false - let haveDispatchPropsChanged = false - if (shouldUpdateStateProps) { - haveStatePropsChanged = this.updateStatePropsIfNeeded() - } - if (shouldUpdateDispatchProps) { - haveDispatchPropsChanged = this.updateDispatchPropsIfNeeded() - } + clearCache() { + this.dispatchProps = null + this.stateProps = null + this.mergedProps = null + this.haveOwnPropsChanged = true + this.hasStoreStateChanged = true + this.renderedElement = null + this.finalMapDispatchToProps = null + this.finalMapStateToProps = null + } - let haveMergedPropsChanged = true - if ( - haveStatePropsChanged || - haveDispatchPropsChanged || - haveOwnPropsChanged - ) { - haveMergedPropsChanged = this.updateMergedPropsIfNeeded() - } else { - haveMergedPropsChanged = false - } + handleChange() { + if (!this.unsubscribe) { + return + } - if (!haveMergedPropsChanged && renderedElement) { - return renderedElement - } + const prevStoreState = this.state.storeState + const storeState = this.store.getState() - if (withRef) { - this.renderedElement = createElement(WrappedComponent, { - ...this.mergedProps, - ref: 'wrappedInstance' - }) - } else { - this.renderedElement = createElement(WrappedComponent, - this.mergedProps - ) - } + if (!this.pure || prevStoreState !== storeState) { + this.hasStoreStateChanged = true + this.setState({ storeState }) + } + } - return this.renderedElement - } + getWrappedInstance() { + invariant(this.withRef, + `To access the wrapped instance, you need to specify ` + + `{ withRef: true } as the fourth argument of the connect() call.` + ) + + return this.refs.wrappedInstance + } + + render() { + const { + haveOwnPropsChanged, + hasStoreStateChanged, + renderedElement + } = this + + this.haveOwnPropsChanged = false + this.hasStoreStateChanged = false + + let shouldUpdateStateProps = true + let shouldUpdateDispatchProps = true + if (this.pure && renderedElement) { + shouldUpdateStateProps = hasStoreStateChanged || ( + haveOwnPropsChanged && this.doStatePropsDependOnOwnProps + ) + shouldUpdateDispatchProps = + haveOwnPropsChanged && this.doDispatchPropsDependOnOwnProps } - Connect.displayName = `Connect(${getDisplayName(WrappedComponent)})` - Connect.WrappedComponent = WrappedComponent - Connect.contextTypes = { - store: storeShape + let haveStatePropsChanged = false + let haveDispatchPropsChanged = false + if (shouldUpdateStateProps) { + haveStatePropsChanged = this.updateStatePropsIfNeeded() } - Connect.propTypes = { - store: storeShape + if (shouldUpdateDispatchProps) { + haveDispatchPropsChanged = this.updateDispatchPropsIfNeeded() } - if (process.env.NODE_ENV !== 'production') { - Connect.prototype.componentWillUpdate = function componentWillUpdate() { - if (this.version === version) { - return - } + let haveMergedPropsChanged = true + if ( + haveStatePropsChanged || + haveDispatchPropsChanged || + haveOwnPropsChanged + ) { + haveMergedPropsChanged = this.updateMergedPropsIfNeeded() + } else { + haveMergedPropsChanged = false + } - // We are hot reloading! - this.version = version - this.trySubscribe() - this.clearCache() - } + if (!haveMergedPropsChanged && renderedElement) { + return renderedElement } - return hoistStatics(Connect, WrappedComponent) + if (this.withRef) { + this.renderedElement = createElement(this.constructor.WrappedComponent, { + ...this.mergedProps, + ref: 'wrappedInstance' + }) + } else { + this.renderedElement = createElement(this.constructor.WrappedComponent, + this.mergedProps + ) + } + + return this.renderedElement } } + +export default createConnect(Connect)