Skip to content

Instantly share code, notes, and snippets.

@cyan33
Created December 27, 2017 14:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cyan33/55f5ad417830d256e6bf4bab3b5dec83 to your computer and use it in GitHub Desktop.
Save cyan33/55f5ad417830d256e6bf4bab3b5dec83 to your computer and use it in GitHub Desktop.
connect v4.2.0
/*
* author: Dan Abromov
*/
const { Component, createElement } = require('react')
const storeShape = require('../utils/storeShape')
const shallowEqual = require('../utils/shallowEqual')
const wrapActionCreators = require('../utils/wrapActionCreators')
const isPlainObject = require('lodash/isPlainObject')
const hoistStatics = require('hoist-non-react-statics')
const invariant = require('invariant')
const defaultMapStateToProps = state => ({}) // eslint-disable-line no-unused-vars
const defaultMapDispatchToProps = dispatch => ({ dispatch })
const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({
...parentProps,
...stateProps,
...dispatchProps
})
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}
// Helps track hot reloading.
let nextVersion = 0
function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
const shouldSubscribe = Boolean(mapStateToProps)
const finalMapStateToProps = mapStateToProps || defaultMapStateToProps
const finalMapDispatchToProps = isPlainObject(mapDispatchToProps) ?
wrapActionCreators(mapDispatchToProps) :
mapDispatchToProps || defaultMapDispatchToProps
const finalMergeProps = mergeProps || defaultMergeProps
const doStatePropsDependOnOwnProps = finalMapStateToProps.length !== 1
const doDispatchPropsDependOnOwnProps = finalMapDispatchToProps.length !== 1
const { pure = true, withRef = false } = options
// Helps track hot reloading.
const version = nextVersion++
function computeStateProps(store, props) {
const state = store.getState()
const stateProps = doStatePropsDependOnOwnProps ?
finalMapStateToProps(state, props) :
finalMapStateToProps(state)
invariant(
isPlainObject(stateProps),
'`mapStateToProps` must return an object. Instead received %s.',
stateProps
)
return stateProps
}
function computeDispatchProps(store, props) {
const { dispatch } = store
const dispatchProps = doDispatchPropsDependOnOwnProps ?
finalMapDispatchToProps(dispatch, props) :
finalMapDispatchToProps(dispatch)
invariant(
isPlainObject(dispatchProps),
'`mapDispatchToProps` muxst return an object. Instead received %s.',
dispatchProps
)
return dispatchProps
}
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
}
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 <Provider>, ` +
`or explicitly pass "store" as a prop to "${this.constructor.displayName}".`
)
const storeState = this.store.getState()
this.state = { storeState }
this.clearCache()
}
updateStatePropsIfNeeded() {
const nextStateProps = computeStateProps(this.store, this.props)
if (this.stateProps && shallowEqual(nextStateProps, this.stateProps)) {
return false
}
this.stateProps = nextStateProps
return true
}
updateDispatchPropsIfNeeded() {
const nextDispatchProps = computeDispatchProps(this.store, this.props)
if (this.dispatchProps && shallowEqual(nextDispatchProps, this.dispatchProps)) {
return false
}
this.dispatchProps = nextDispatchProps
return true
}
updateMergedProps() {
this.mergedProps = computeMergedProps(
this.stateProps,
this.dispatchProps,
this.props
)
}
isSubscribed() {
return typeof this.unsubscribe === 'function'
}
trySubscribe() {
if (shouldSubscribe && !this.unsubscribe) {
this.unsubscribe = this.store.subscribe(::this.handleChange)
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.renderedElement = null
}
handleChange() {
if (!this.unsubscribe) {
return
}
const prevStoreState = this.state.storeState
const storeState = this.store.getState()
// why there the reference check?
// if some actions are not handled, the store state stays unchanged,
// which leaves prevStoreState === storeState
if (!pure || prevStoreState !== storeState) {
this.hasStoreStateChanged = true
this.setState({ storeState })
}
}
getWrappedInstance() {
invariant(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 (pure && renderedElement) {
shouldUpdateStateProps = hasStoreStateChanged || (
haveOwnPropsChanged && doStatePropsDependOnOwnProps
)
shouldUpdateDispatchProps =
haveOwnPropsChanged && doDispatchPropsDependOnOwnProps
}
let haveStatePropsChanged = false
let haveDispatchPropsChanged = false
if (shouldUpdateStateProps) {
haveStatePropsChanged = this.updateStatePropsIfNeeded()
}
if (shouldUpdateDispatchProps) {
haveDispatchPropsChanged = this.updateDispatchPropsIfNeeded()
}
let haveMergedPropsChanged = true
if (
haveStatePropsChanged ||
haveDispatchPropsChanged ||
haveOwnPropsChanged
) {
this.updateMergedProps()
} 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
}
}
Connect.displayName = `Connect(${getDisplayName(WrappedComponent)})`
Connect.WrappedComponent = WrappedComponent
Connect.contextTypes = {
store: storeShape
}
Connect.propTypes = {
store: storeShape
}
if (process.env.NODE_ENV !== 'production') {
Connect.prototype.componentWillUpdate = function componentWillUpdate() {
if (this.version === version) {
return
}
// We are hot reloading!
this.version = version
this.trySubscribe()
this.clearCache()
}
}
return hoistStatics(Connect, WrappedComponent)
}
}
module.exports = connect
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment