From ee4366b9a420f93483df3ce6c12bc72bc83f7d18 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Wed, 15 Jun 2016 21:23:02 -0400 Subject: [PATCH 01/76] Reimplementing connect() and extracting connectToStore() based on 'reselect'. (6 failing tests to go) --- package.json | 3 +- src/components/connect.js | 409 ++++++------------------------- src/components/connectToStore.js | 222 +++++++++++++++++ 3 files changed, 303 insertions(+), 331 deletions(-) create mode 100644 src/components/connectToStore.js diff --git a/package.json b/package.json index 576522b83..e0bac92c5 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,8 @@ "hoist-non-react-statics": "^1.0.3", "invariant": "^2.0.0", "lodash": "^4.2.0", - "loose-envify": "^1.1.0" + "loose-envify": "^1.1.0", + "reselect": "^2.5.1" }, "peerDependencies": { "react": "^0.14.0 || ^15.0.0-0", diff --git a/src/components/connect.js b/src/components/connect.js index 3b60ebbce..0879dbc58 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -1,367 +1,116 @@ -import { Component, createElement } from 'react' -import storeShape from '../utils/storeShape' -import shallowEqual from '../utils/shallowEqual' -import wrapActionCreators from '../utils/wrapActionCreators' -import warning from '../utils/warning' import isPlainObject from 'lodash/isPlainObject' -import hoistStatics from 'hoist-non-react-statics' -import invariant from 'invariant' +import { bindActionCreators } from 'redux' +import { createSelector } from 'reselect' +import warning from '../utils/warning' + +import connectToStore, { createShallowEqualSelector } from './connectToStore' -const defaultMapStateToProps = state => ({}) // eslint-disable-line no-unused-vars -const defaultMapDispatchToProps = dispatch => ({ dispatch }) -const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({ - ...parentProps, +const defaultMergeProps = (stateProps, dispatchProps, ownProps) => ({ + ...ownProps, ...stateProps, ...dispatchProps }) -function getDisplayName(WrappedComponent) { - return WrappedComponent.displayName || WrappedComponent.name || 'Component' -} - -let errorObject = { value: null } -function tryCatch(fn, ctx) { - try { - return fn.apply(ctx) - } catch (e) { - errorObject.value = e - return errorObject - } -} - -// Helps track hot reloading. -let nextVersion = 0 - -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++ - - return function wrapWithConnect(WrappedComponent) { - const connectDisplayName = `Connect(${getDisplayName(WrappedComponent)})` - +const empty = {} + +export default function connect( + mapStateToProps, + mapDispatchToProps, + mergeProps, + { + pure = true, + withRef = false + } = {} +) { + function selectorFactory({ displayName }) { function checkStateShape(props, methodName) { if (!isPlainObject(props)) { warning( - `${methodName}() in ${connectDisplayName} must return a plain object. ` + + `${methodName}() in ${displayName} 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') + function verify(methodName, selector) { + return (...args) => { + const result = selector(...args) + checkStateShape(result, methodName) + return result } - return mergedProps } - 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 , ` + - `or explicitly pass "store" as a prop to "${connectDisplayName}".` - ) - - const storeState = this.store.getState() - this.state = { storeState } - this.clearCache() - } - - 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 - } - - updateDispatchPropsIfNeeded() { - const nextDispatchProps = this.computeDispatchProps(this.store, this.props) - if (this.dispatchProps && shallowEqual(nextDispatchProps, this.dispatchProps)) { - return false - } + const ownPropsSelector = createShallowEqualSelector( + (_, props) => props, + props => props + ) - this.dispatchProps = nextDispatchProps - return true - } - - updateMergedPropsIfNeeded() { - const nextMergedProps = computeMergedProps(this.stateProps, this.dispatchProps, this.props) - if (this.mergedProps && checkMergedEquals && shallowEqual(nextMergedProps, this.mergedProps)) { - return false - } + function getStatePropsSelector() { + if (!mapStateToProps) return () => empty - this.mergedProps = nextMergedProps - return true + if (!pure) { + return (state, props) => mapStateToProps(state, props) } - isSubscribed() { - return typeof this.unsubscribe === 'function' - } - - 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() + if (mapStateToProps.length === 1) { + return createSelector( + state => state, + mapStateToProps + ) } - componentWillReceiveProps(nextProps) { - if (!pure || !shallowEqual(nextProps, this.props)) { - this.haveOwnPropsChanged = true - } - } + return createSelector( + state => state, + ownPropsSelector, + mapStateToProps + ) + } - componentWillUnmount() { - this.tryUnsubscribe() - this.clearCache() - } + function getDispatchPropsSelector() { + if (!mapDispatchToProps) return (_, __, dispatch) => ({ dispatch }) - 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 + if (typeof mapDispatchToProps !== 'function') { + return createSelector( + (_, __, dispatch) => dispatch, + dispatch => bindActionCreators(mapDispatchToProps, dispatch) + ) } - 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 }) + if (!pure) { + return (_, props, dispatch) => mapDispatchToProps(dispatch, props) } - getWrappedInstance() { - invariant(withRef, - `To access the wrapped instance, you need to specify ` + - `{ withRef: true } as the fourth argument of the connect() call.` + if (mapDispatchToProps.length === 1) { + return createSelector( + (_, __, dispatch) => dispatch, + mapDispatchToProps ) - - return this.refs.wrappedInstance } - render() { - const { - haveOwnPropsChanged, - hasStoreStateChanged, - haveStatePropsBeenPrecalculated, - statePropsPrecalculationError, - renderedElement - } = this - - this.haveOwnPropsChanged = false - this.hasStoreStateChanged = false - this.haveStatePropsBeenPrecalculated = false - this.statePropsPrecalculationError = null - - if (statePropsPrecalculationError) { - throw statePropsPrecalculationError - } - - let shouldUpdateStateProps = true - let shouldUpdateDispatchProps = true - if (pure && renderedElement) { - shouldUpdateStateProps = hasStoreStateChanged || ( - haveOwnPropsChanged && this.doStatePropsDependOnOwnProps - ) - shouldUpdateDispatchProps = - haveOwnPropsChanged && this.doDispatchPropsDependOnOwnProps - } - - let haveStatePropsChanged = false - let haveDispatchPropsChanged = false - if (haveStatePropsBeenPrecalculated) { - haveStatePropsChanged = true - } else if (shouldUpdateStateProps) { - haveStatePropsChanged = this.updateStatePropsIfNeeded() - } - if (shouldUpdateDispatchProps) { - haveDispatchPropsChanged = this.updateDispatchPropsIfNeeded() - } - - let haveMergedPropsChanged = true - 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 - } - } - - Connect.displayName = connectDisplayName - Connect.WrappedComponent = WrappedComponent - Connect.contextTypes = { - store: storeShape - } - Connect.propTypes = { - store: storeShape + return createSelector( + (_, __, dispatch) => dispatch, + ownPropsSelector, + mapDispatchToProps + ) } - if (process.env.NODE_ENV !== 'production') { - Connect.prototype.componentWillUpdate = function componentWillUpdate() { - if (this.version === version) { - return - } + return verify('mergeProps', createShallowEqualSelector( + verify('mapStateToProps', getStatePropsSelector()), + verify('mapDispatchToProps', getDispatchPropsSelector()), + ownPropsSelector, + mergeProps || defaultMergeProps + )) + } - // We are hot reloading! - this.version = version - this.trySubscribe() - this.clearCache() - } + return connectToStore( + selectorFactory, + { + pure, + withRef, + getDisplayName: name => `Connect(${name})`, + recomputationsProp: null, + shouldIncludeOriginalProps: !mergeProps, + shouldUseState: Boolean(mapStateToProps) } - - return hoistStatics(Connect, WrappedComponent) - } + ) } diff --git a/src/components/connectToStore.js b/src/components/connectToStore.js new file mode 100644 index 000000000..90b34bf5e --- /dev/null +++ b/src/components/connectToStore.js @@ -0,0 +1,222 @@ +import hoistStatics from 'hoist-non-react-statics' +import invariant from 'invariant' +import { Component, createElement } from 'react' +import { + createSelector, + createSelectorCreator, + createStructuredSelector, + defaultMemoize +} from 'reselect' + +import shallowEqual from '../utils/shallowEqual' +import storeShape from '../utils/storeShape' + +export { createSelector as createSelector } +export { createStructuredSelector as createStructuredSelector } +export const createShallowEqualSelector = createSelectorCreator(defaultMemoize, shallowEqual) + +export const selectDispatch = (_, __, dispatch) => dispatch + +export function dispatchable(actionCreator, ...selectorsToPartiallyApply) { + if (selectorsToPartiallyApply.length === 0) { + return createSelector( + selectDispatch, + dispatch => (...args) => dispatch(actionCreator(...args)) + ) + } + + return createSelector( + selectDispatch, + ...selectorsToPartiallyApply, + (dispatch, ...partialArgs) => (...args) => dispatch(actionCreator(...partialArgs, ...args)) + ) +} + +let hotReloadingVersion = 0 + +export default function connectToStore( + /* + this func is responsible for returning the selector function used to compute new props from + state, props, and dispatch. For example: + + export default connectToStore(() => (state, props, dispatch) => ({ + thing: state.things[props.thingId], + saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)), + }))(YourComponent) + + Alternatively, it can return a plain object which will be passed to reselect's + 'createStructuredSelector' function to create the selector. For example: + + return connectToStore(() => ({ + thing: (state, props) => state.things[props.thingId], + saveThing: (_, props, dispatch) => fields => ( + dispatch(actionCreators.saveThing(props.thingId, fields)) + ), + }))(YourComponent) + + This is equivalent to wrapping the returned object in a call to `createStructuredSelector`, + but is supported as a convenience; This is the recommended approach to defining your + selectorFactory methods. The above example can be simplfied by using the `dispatchable` helper + method provided with connectToStore: + + connectToStore(() => ({ + thing: (state, props) => state.things[props.thingId], + saveThing: dispatchable(actionCreators.saveThing, (_, props) => props.thingId), + }))(YourComponent) + + A verbose but descriptive name for `dispatchable` would be `createBoundActionCreatorSelector`. + `dispatchable` will return a selector that binds the passed action creator arg to dispatch. Any + additional args given will be treated as selectors whose results should be partially applied to + the action creator. + */ + selectorFactory, + // options object: + { + // the func used to compute this HOC's displayName from the wrapped component's displayName. + getDisplayName = name => `connectToStore(${name})`, + + // if true, shouldComponentUpdate will only be true of the selector recomputes for nextProps. + // if false, shouldComponentUpdate will always be true. + pure = true, + + // the name of the property passed to the wrapped element indicating the number of. + // recomputations since it was mounted. useful for watching for unnecessary re-renders. + recomputationsProp = process.env.NODE_ENV !== 'production' ? '__recomputations' : null, + + // if true, the props passed to this HOC are merged with the results of the selector; in the + // case of key collision, selector value is kept and prop is discared. if false, only the + // selector results are passed to the wrapped element. + shouldIncludeOriginalProps = true, + + // if true, the selector receieves the current store state as the first arg, and this HOC + // subscribes to store changes. if false, null is passed as the first arg of selector. + shouldUseState = true, + + // the key of props/context to get the store + storeKey = 'store', + + // if true, the wrapped element is exposed by this HOC via the getWrappedInstance() function. + withRef = false + } = {} +) { + function buildSelector() { + const { displayName, store } = this + const factoryResult = selectorFactory({ displayName }) + const ref = withRef ? 'wrappedInstance' : undefined + + const selector = createShallowEqualSelector( + // original props selector: + shouldIncludeOriginalProps + ? ((_, props) => props) + : (() => null), + + // sourceSelector + typeof factoryResult === 'function' + ? factoryResult + : createStructuredSelector(factoryResult), + + // combine original props + selector props + ref + (props, sourceSelectorResults) => ({ + ...props, + ...sourceSelectorResults, + ref + })) + + return function runSelector(props) { + const recomputationsBefore = selector.recomputations() + const storeState = shouldUseState ? store.getState() : null + const selectorResults = selector(storeState, props, store.dispatch) + const recomputationsAfter = selector.recomputations() + + const finalProps = recomputationsProp + ? { ...selectorResults, [recomputationsProp]: recomputationsAfter } + : selectorResults + + return { + props: finalProps, + shouldUpdate: recomputationsBefore !== recomputationsAfter + } + } + } + + const version = hotReloadingVersion++ + + return function wrapWithConnect(WrappedComponent) { + class Connect extends Component { + componentWillMount() { + this.version = version + this.displayName = Connect.displayName + this.store = this.props[storeKey] || this.context[storeKey] + + invariant(this.store, + `Could not find "store" in either the context or ` + + `props of "${Connect.displayName}". ` + + `Either wrap the root component in a , ` + + `or explicitly pass "store" as a prop to "${Connect.displayName}".` + ) + + this.selector = buildSelector.call(this) + this.trySubscribe() + } + + shouldComponentUpdate(nextProps) { + return !pure || this.selector(nextProps).shouldUpdate + } + + componentWillUnmount() { + if (this.unsubscribe) this.unsubscribe() + this.unsubscribe = null + this.selector = () => ({ props: {}, shouldUpdate: false }) + this.store = null + } + + 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 + } + + trySubscribe() { + if (!shouldUseState || this.unsubscribe) return + + this.unsubscribe = this.store.subscribe(() => { + if (this.selector(this.props).shouldUpdate) this.forceUpdate() + }) + } + + isSubscribed() { + return typeof this.unsubscribe === 'function' + } + + render() { + const { props } = this.selector(this.props) + return createElement(WrappedComponent, props) + } + } + + const wrappedComponentName = WrappedComponent.displayName + || WrappedComponent.name + || 'Component' + + Connect.displayName = getDisplayName(wrappedComponentName) + Connect.WrappedComponent = WrappedComponent + Connect.contextTypes = { [storeKey]: storeShape } + Connect.propTypes = { [storeKey]: 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.selector = buildSelector.call(this) + } + } + + return hoistStatics(Connect, WrappedComponent) + } +} From 2aec4363aa070e0da59ce92efb24bbe8adff14cb Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Wed, 15 Jun 2016 21:42:00 -0400 Subject: [PATCH 02/76] Changes to fix failing tests (4 fails to go) --- src/components/connectToStore.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/connectToStore.js b/src/components/connectToStore.js index 90b34bf5e..ae673a577 100644 --- a/src/components/connectToStore.js +++ b/src/components/connectToStore.js @@ -143,6 +143,11 @@ export default function connectToStore( return function wrapWithConnect(WrappedComponent) { class Connect extends Component { + constructor(props, context) { + super(props, context) + this.state = {} + } + componentWillMount() { this.version = version this.displayName = Connect.displayName @@ -182,8 +187,9 @@ export default function connectToStore( trySubscribe() { if (!shouldUseState || this.unsubscribe) return + let storeUpdates = 0 this.unsubscribe = this.store.subscribe(() => { - if (this.selector(this.props).shouldUpdate) this.forceUpdate() + if (this.unsubscribe) this.setState({ storeUpdates: storeUpdates++ }) }) } From fe7358cae716570bd294800311817c29e2e4b305 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Wed, 15 Jun 2016 23:04:20 -0400 Subject: [PATCH 03/76] Change code to make tests for factory mapStateToProps/mapDispatchToProps pass. BREAKING CHANGE: requires setting an option parameter mapStateIsFactory/mapDispatchIsFactory to signal factory methods. --- src/components/connect.js | 44 +++++++++++++++----------------- src/components/connectToStore.js | 3 ++- src/index.js | 3 ++- test/components/connect.spec.js | 4 +-- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/components/connect.js b/src/components/connect.js index 0879dbc58..abaa910b6 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -18,6 +18,8 @@ export default function connect( mapDispatchToProps, mergeProps, { + mapStateIsFactory, + mapDispatchIsFactory, pure = true, withRef = false } = {} @@ -46,52 +48,46 @@ export default function connect( ) function getStatePropsSelector() { - if (!mapStateToProps) return () => empty + const mstp = mapStateIsFactory ? mapStateToProps() : mapStateToProps + + if (!mstp) { + return () => empty + } if (!pure) { - return (state, props) => mapStateToProps(state, props) + return (state, props) => mstp(state, props) } if (mapStateToProps.length === 1) { - return createSelector( - state => state, - mapStateToProps - ) + return createSelector(state => state, mstp) } - return createSelector( - state => state, - ownPropsSelector, - mapStateToProps - ) + return createSelector(state => state, ownPropsSelector, mstp) } function getDispatchPropsSelector() { - if (!mapDispatchToProps) return (_, __, dispatch) => ({ dispatch }) + const mdtp = mapDispatchIsFactory ? mapDispatchToProps() : mapDispatchToProps - if (typeof mapDispatchToProps !== 'function') { + if (!mdtp) { + return (_, __, dispatch) => ({ dispatch }) + } + + if (typeof mdtp !== 'function') { return createSelector( (_, __, dispatch) => dispatch, - dispatch => bindActionCreators(mapDispatchToProps, dispatch) + dispatch => bindActionCreators(mdtp, dispatch) ) } if (!pure) { - return (_, props, dispatch) => mapDispatchToProps(dispatch, props) + return (_, props, dispatch) => mdtp(dispatch, props) } if (mapDispatchToProps.length === 1) { - return createSelector( - (_, __, dispatch) => dispatch, - mapDispatchToProps - ) + return createSelector((_, __, dispatch) => dispatch, mdtp) } - return createSelector( - (_, __, dispatch) => dispatch, - ownPropsSelector, - mapDispatchToProps - ) + return createSelector((_, __, dispatch) => dispatch, ownPropsSelector, mdtp) } return verify('mergeProps', createShallowEqualSelector( diff --git a/src/components/connectToStore.js b/src/components/connectToStore.js index ae673a577..06321d0ea 100644 --- a/src/components/connectToStore.js +++ b/src/components/connectToStore.js @@ -103,12 +103,13 @@ export default function connectToStore( const { displayName, store } = this const factoryResult = selectorFactory({ displayName }) const ref = withRef ? 'wrappedInstance' : undefined + const empty = {} const selector = createShallowEqualSelector( // original props selector: shouldIncludeOriginalProps ? ((_, props) => props) - : (() => null), + : (() => empty), // sourceSelector typeof factoryResult === 'function' diff --git a/src/index.js b/src/index.js index ad89eec2d..0f96439fc 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,5 @@ import Provider from './components/Provider' import connect from './components/connect' +import connectToStore from './components/connectToStore' -export { Provider, connect } +export { Provider, connect, connectToStore } diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 514eea018..04cdf9f39 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1677,7 +1677,7 @@ describe('React', () => { } } - @connect(mapStateFactory) + @connect(mapStateFactory, null, null, { mapStateIsFactory: true }) class Container extends Component { componentWillUpdate() { updatedCount++ @@ -1721,7 +1721,7 @@ describe('React', () => { return { ...stateProps, ...dispatchProps, name: parentProps.name } } - @connect(null, mapDispatchFactory, mergeParentDispatch) + @connect(null, mapDispatchFactory, mergeParentDispatch, { mapDispatchIsFactory: true }) class Passthrough extends Component { componentWillUpdate() { updatedCount++ From 9967a18208bf554ac6e1c38faf054926e489c675 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Wed, 15 Jun 2016 23:06:59 -0400 Subject: [PATCH 04/76] Adjust remaining 2 failing tests to account for slightly different behind-the-scenes behavior of new implementation. --- test/components/connect.spec.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 04cdf9f39..5f26e78e3 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1469,8 +1469,8 @@ describe('React', () => { const target = TestUtils.findRenderedComponentWithType(tree, Passthrough) const wrapper = TestUtils.findRenderedComponentWithType(tree, StatefulWrapper) - expect(mapStateSpy.calls.length).toBe(2) - expect(mapDispatchSpy.calls.length).toBe(2) + expect(mapStateSpy.calls.length).toBe(1) + expect(mapDispatchSpy.calls.length).toBe(1) expect(target.props.statefulValue).toEqual('foo') // Impure update @@ -1478,8 +1478,8 @@ describe('React', () => { storeGetter.storeKey = 'bar' wrapper.setState({ storeGetter }) - expect(mapStateSpy.calls.length).toBe(3) - expect(mapDispatchSpy.calls.length).toBe(3) + expect(mapStateSpy.calls.length).toBe(2) + expect(mapDispatchSpy.calls.length).toBe(2) expect(target.props.statefulValue).toEqual('bar') }) @@ -1611,17 +1611,17 @@ describe('React', () => { store.dispatch({ type: 'APPEND', body: 'a' }) expect(mapStateCalls).toBe(2) expect(renderCalls).toBe(1) - expect(spy.calls.length).toBe(0) + expect(spy.calls.length).toBe(1) store.dispatch({ type: 'APPEND', body: 'a' }) expect(mapStateCalls).toBe(3) expect(renderCalls).toBe(1) - expect(spy.calls.length).toBe(0) + expect(spy.calls.length).toBe(2) store.dispatch({ type: 'APPEND', body: 'a' }) expect(mapStateCalls).toBe(4) expect(renderCalls).toBe(2) - expect(spy.calls.length).toBe(1) + expect(spy.calls.length).toBe(3) spy.destroy() }) From a12c1ece627f8c8e35684a28514fc582b597ff31 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Wed, 15 Jun 2016 23:42:47 -0400 Subject: [PATCH 05/76] add dispatchable to the exported functions --- src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 0f96439fc..0f1c0996c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ import Provider from './components/Provider' import connect from './components/connect' -import connectToStore from './components/connectToStore' +import connectToStore, { dispatchable } from './components/connectToStore' -export { Provider, connect, connectToStore } +export { Provider, connect, connectToStore, dispatchable } From 27f613da6902d40fc449622d0716b09cbe9ea673 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Wed, 15 Jun 2016 23:58:38 -0400 Subject: [PATCH 06/76] Replace string ref with function ref. Adjust test to match --- src/components/connectToStore.js | 4 ++-- test/components/connect.spec.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/connectToStore.js b/src/components/connectToStore.js index 06321d0ea..c81534557 100644 --- a/src/components/connectToStore.js +++ b/src/components/connectToStore.js @@ -102,7 +102,7 @@ export default function connectToStore( function buildSelector() { const { displayName, store } = this const factoryResult = selectorFactory({ displayName }) - const ref = withRef ? 'wrappedInstance' : undefined + const ref = withRef ? (c => { this.wrappedInstance = c }) : undefined const empty = {} const selector = createShallowEqualSelector( @@ -182,7 +182,7 @@ export default function connectToStore( `{ withRef: true } as the fourth argument of the connect() call.` ) - return this.refs.wrappedInstance + return this.wrappedInstance } trySubscribe() { diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 5f26e78e3..75294b9ad 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1368,7 +1368,7 @@ describe('React', () => { expect(() => decorated.someInstanceMethod()).toThrow() expect(decorated.getWrappedInstance().someInstanceMethod()).toBe(someData) - expect(decorated.refs.wrappedInstance.someInstanceMethod()).toBe(someData) + expect(decorated.wrappedInstance.someInstanceMethod()).toBe(someData) }) it('should wrap impure components without supressing updates', () => { From 106ded6a259c66ec8f331c811a7a02c0c475d8e5 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 16 Jun 2016 00:02:15 -0400 Subject: [PATCH 07/76] Generalized a couple error messages --- src/components/connect.js | 1 + src/components/connectToStore.js | 9 ++++++--- test/components/connect.spec.js | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/connect.js b/src/components/connect.js index abaa910b6..a4d139738 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -104,6 +104,7 @@ export default function connect( pure, withRef, getDisplayName: name => `Connect(${name})`, + methodName: 'connect', recomputationsProp: null, shouldIncludeOriginalProps: !mergeProps, shouldUseState: Boolean(mapStateToProps) diff --git a/src/components/connectToStore.js b/src/components/connectToStore.js index c81534557..b74d4f7ce 100644 --- a/src/components/connectToStore.js +++ b/src/components/connectToStore.js @@ -75,6 +75,9 @@ export default function connectToStore( // the func used to compute this HOC's displayName from the wrapped component's displayName. getDisplayName = name => `connectToStore(${name})`, + // shown in error messages + methodName = 'connectToStore', + // if true, shouldComponentUpdate will only be true of the selector recomputes for nextProps. // if false, shouldComponentUpdate will always be true. pure = true, @@ -155,10 +158,10 @@ export default function connectToStore( this.store = this.props[storeKey] || this.context[storeKey] invariant(this.store, - `Could not find "store" in either the context or ` + + `Could not find "${storeKey}" in either the context or ` + `props of "${Connect.displayName}". ` + `Either wrap the root component in a , ` + - `or explicitly pass "store" as a prop to "${Connect.displayName}".` + `or explicitly pass "${storeKey}" as a prop to "${Connect.displayName}".` ) this.selector = buildSelector.call(this) @@ -179,7 +182,7 @@ export default function connectToStore( getWrappedInstance() { invariant(withRef, `To access the wrapped instance, you need to specify ` + - `{ withRef: true } as the fourth argument of the connect() call.` + `{ withRef: true } in the options argument of the ${methodName}() call.` ) return this.wrappedInstance diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 75294b9ad..4ac4ba9fa 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1334,7 +1334,7 @@ describe('React', () => { const decorated = TestUtils.findRenderedComponentWithType(tree, Decorated) expect(() => decorated.getWrappedInstance()).toThrow( - /To access the wrapped instance, you need to specify \{ withRef: true \} as the fourth argument of the connect\(\) call\./ + /To access the wrapped instance, you need to specify \{ withRef: true \} in the options argument of the connect\(\) call\./ ) }) From cde8ba2200279114ad95f126c8ec3e445cd6b20a Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 16 Jun 2016 00:06:49 -0400 Subject: [PATCH 08/76] make additional options passed to connect() fall through to connectToStore() --- src/components/connect.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/connect.js b/src/components/connect.js index a4d139738..2d1ee1b56 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -21,7 +21,7 @@ export default function connect( mapStateIsFactory, mapDispatchIsFactory, pure = true, - withRef = false + ...options } = {} ) { function selectorFactory({ displayName }) { @@ -102,10 +102,10 @@ export default function connect( selectorFactory, { pure, - withRef, getDisplayName: name => `Connect(${name})`, - methodName: 'connect', recomputationsProp: null, + ...options, + methodName: 'connect', shouldIncludeOriginalProps: !mergeProps, shouldUseState: Boolean(mapStateToProps) } From 5a631ab2c769e81e2af9ea4822c98ec76cbd3293 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 16 Jun 2016 17:04:03 -0400 Subject: [PATCH 09/76] rename connectToStore to connectAdvanced --- src/components/connect.js | 4 ++-- .../{connectToStore.js => connectAdvanced.js} | 18 +++++++++--------- src/index.js | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) rename src/components/{connectToStore.js => connectAdvanced.js} (94%) diff --git a/src/components/connect.js b/src/components/connect.js index 2d1ee1b56..03bf86583 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -3,7 +3,7 @@ import { bindActionCreators } from 'redux' import { createSelector } from 'reselect' import warning from '../utils/warning' -import connectToStore, { createShallowEqualSelector } from './connectToStore' +import connectAdvanced, { createShallowEqualSelector } from './connectAdvanced' const defaultMergeProps = (stateProps, dispatchProps, ownProps) => ({ ...ownProps, @@ -98,7 +98,7 @@ export default function connect( )) } - return connectToStore( + return connectAdvanced( selectorFactory, { pure, diff --git a/src/components/connectToStore.js b/src/components/connectAdvanced.js similarity index 94% rename from src/components/connectToStore.js rename to src/components/connectAdvanced.js index b74d4f7ce..a073d05d9 100644 --- a/src/components/connectToStore.js +++ b/src/components/connectAdvanced.js @@ -34,12 +34,12 @@ export function dispatchable(actionCreator, ...selectorsToPartiallyApply) { let hotReloadingVersion = 0 -export default function connectToStore( +export default function connectAdvanced( /* - this func is responsible for returning the selector function used to compute new props from - state, props, and dispatch. For example: + selectorFactory is a func is responsible for returning the selector function used to compute new + props from state, props, and dispatch. For example: - export default connectToStore(() => (state, props, dispatch) => ({ + export default connectAdvanced(() => (state, props, dispatch) => ({ thing: state.things[props.thingId], saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)), }))(YourComponent) @@ -47,7 +47,7 @@ export default function connectToStore( Alternatively, it can return a plain object which will be passed to reselect's 'createStructuredSelector' function to create the selector. For example: - return connectToStore(() => ({ + return connectAdvanced(() => ({ thing: (state, props) => state.things[props.thingId], saveThing: (_, props, dispatch) => fields => ( dispatch(actionCreators.saveThing(props.thingId, fields)) @@ -57,9 +57,9 @@ export default function connectToStore( This is equivalent to wrapping the returned object in a call to `createStructuredSelector`, but is supported as a convenience; This is the recommended approach to defining your selectorFactory methods. The above example can be simplfied by using the `dispatchable` helper - method provided with connectToStore: + method provided with connectAdvanced: - connectToStore(() => ({ + connectAdvanced(() => ({ thing: (state, props) => state.things[props.thingId], saveThing: dispatchable(actionCreators.saveThing, (_, props) => props.thingId), }))(YourComponent) @@ -73,10 +73,10 @@ export default function connectToStore( // options object: { // the func used to compute this HOC's displayName from the wrapped component's displayName. - getDisplayName = name => `connectToStore(${name})`, + getDisplayName = name => `connectAdvanced(${name})`, // shown in error messages - methodName = 'connectToStore', + methodName = 'connectAdvanced', // if true, shouldComponentUpdate will only be true of the selector recomputes for nextProps. // if false, shouldComponentUpdate will always be true. diff --git a/src/index.js b/src/index.js index 0f1c0996c..d0c189e68 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ import Provider from './components/Provider' import connect from './components/connect' -import connectToStore, { dispatchable } from './components/connectToStore' +import connectAdvanced, { dispatchable } from './components/connectAdvanced' -export { Provider, connect, connectToStore, dispatchable } +export { Provider, connect, connectAdvanced, dispatchable } From 3416b4223433f8518de1c77cccb323c6748592e4 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 16 Jun 2016 17:18:35 -0400 Subject: [PATCH 10/76] pull some utility functions out of connectAdvanced into their own utils files --- src/components/connect.js | 5 ++-- src/components/connectAdvanced.js | 32 +++---------------------- src/index.js | 12 ++++++++-- src/utils/createShallowEqualSelector.js | 4 ++++ src/utils/dispatchable.js | 18 ++++++++++++++ 5 files changed, 38 insertions(+), 33 deletions(-) create mode 100644 src/utils/createShallowEqualSelector.js create mode 100644 src/utils/dispatchable.js diff --git a/src/components/connect.js b/src/components/connect.js index 03bf86583..f7bcd08b7 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -1,9 +1,10 @@ import isPlainObject from 'lodash/isPlainObject' import { bindActionCreators } from 'redux' import { createSelector } from 'reselect' -import warning from '../utils/warning' -import connectAdvanced, { createShallowEqualSelector } from './connectAdvanced' +import connectAdvanced from './connectAdvanced' +import createShallowEqualSelector from '../utils/createShallowEqualSelector' +import warning from '../utils/warning' const defaultMergeProps = (stateProps, dispatchProps, ownProps) => ({ ...ownProps, diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index a073d05d9..3a9b0703d 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -1,36 +1,10 @@ import hoistStatics from 'hoist-non-react-statics' import invariant from 'invariant' import { Component, createElement } from 'react' -import { - createSelector, - createSelectorCreator, - createStructuredSelector, - defaultMemoize -} from 'reselect' - -import shallowEqual from '../utils/shallowEqual' -import storeShape from '../utils/storeShape' - -export { createSelector as createSelector } -export { createStructuredSelector as createStructuredSelector } -export const createShallowEqualSelector = createSelectorCreator(defaultMemoize, shallowEqual) +import { createStructuredSelector } from 'reselect' -export const selectDispatch = (_, __, dispatch) => dispatch - -export function dispatchable(actionCreator, ...selectorsToPartiallyApply) { - if (selectorsToPartiallyApply.length === 0) { - return createSelector( - selectDispatch, - dispatch => (...args) => dispatch(actionCreator(...args)) - ) - } - - return createSelector( - selectDispatch, - ...selectorsToPartiallyApply, - (dispatch, ...partialArgs) => (...args) => dispatch(actionCreator(...partialArgs, ...args)) - ) -} +import createShallowEqualSelector from '../utils/createShallowEqualSelector' +import storeShape from '../utils/storeShape' let hotReloadingVersion = 0 diff --git a/src/index.js b/src/index.js index d0c189e68..96e8d37da 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,13 @@ import Provider from './components/Provider' import connect from './components/connect' -import connectAdvanced, { dispatchable } from './components/connectAdvanced' +import connectAdvanced from './components/connectAdvanced' +import createShallowEqualSelector from './utils/createShallowEqualSelector' +import dispatchable from './utils/dispatchable' -export { Provider, connect, connectAdvanced, dispatchable } +export { + Provider, + connect, + connectAdvanced, + createShallowEqualSelector, + dispatchable +} diff --git a/src/utils/createShallowEqualSelector.js b/src/utils/createShallowEqualSelector.js new file mode 100644 index 000000000..e60bdbadf --- /dev/null +++ b/src/utils/createShallowEqualSelector.js @@ -0,0 +1,4 @@ +import { createSelectorCreator, defaultMemoize } from 'reselect' +import shallowEqual from './shallowEqual' + +export default createSelectorCreator(defaultMemoize, shallowEqual) diff --git a/src/utils/dispatchable.js b/src/utils/dispatchable.js new file mode 100644 index 000000000..fb59ad53d --- /dev/null +++ b/src/utils/dispatchable.js @@ -0,0 +1,18 @@ +import { createSelector } from 'reselect' + +export const selectDispatch = (_, __, dispatch) => dispatch + +export default function dispatchable(actionCreator, ...selectorsToPartiallyApply) { + if (selectorsToPartiallyApply.length === 0) { + return createSelector( + selectDispatch, + dispatch => (...args) => dispatch(actionCreator(...args)) + ) + } + + return createSelector( + selectDispatch, + ...selectorsToPartiallyApply, + (dispatch, ...partialArgs) => (...args) => dispatch(actionCreator(...partialArgs, ...args)) + ) +} From 67868248eda7821ec3e89e065fa470b84bf7fb00 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 16 Jun 2016 18:48:33 -0400 Subject: [PATCH 11/76] refactor connect + connectAdvanced --- src/components/connect.js | 106 +++++++++++----------- src/components/connectAdvanced.js | 141 ++++++++++-------------------- 2 files changed, 96 insertions(+), 151 deletions(-) diff --git a/src/components/connect.js b/src/components/connect.js index f7bcd08b7..265806818 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -14,6 +14,19 @@ const defaultMergeProps = (stateProps, dispatchProps, ownProps) => ({ const empty = {} +function verify(displayName, methodName, selector) { + return (...args) => { + const props = selector(...args) + if (!isPlainObject(props)) { + warning( + `${methodName}() in ${displayName} must return a plain object. ` + + `Instead received ${props}.` + ) + } + return props + } +} + export default function connect( mapStateToProps, mapDispatchToProps, @@ -25,75 +38,55 @@ export default function connect( ...options } = {} ) { - function selectorFactory({ displayName }) { - function checkStateShape(props, methodName) { - if (!isPlainObject(props)) { - warning( - `${methodName}() in ${displayName} must return a plain object. ` + - `Instead received ${props}.` - ) - } - } + function getStatePropsSelector(ownPropsSelector) { + const mstp = mapStateIsFactory ? mapStateToProps() : mapStateToProps - function verify(methodName, selector) { - return (...args) => { - const result = selector(...args) - checkStateShape(result, methodName) - return result - } + if (!mstp) { + return () => empty } - const ownPropsSelector = createShallowEqualSelector( - (_, props) => props, - props => props - ) - - function getStatePropsSelector() { - const mstp = mapStateIsFactory ? mapStateToProps() : mapStateToProps + if (!pure) { + return (state, props) => mstp(state, props) + } - if (!mstp) { - return () => empty - } + if (mapStateToProps.length === 1) { + return createSelector(state => state, mstp) + } - if (!pure) { - return (state, props) => mstp(state, props) - } + return createSelector(state => state, ownPropsSelector, mstp) + } - if (mapStateToProps.length === 1) { - return createSelector(state => state, mstp) - } + function getDispatchPropsSelector(ownPropsSelector) { + const mdtp = mapDispatchIsFactory ? mapDispatchToProps() : mapDispatchToProps - return createSelector(state => state, ownPropsSelector, mstp) + if (!mdtp) { + return (_, __, dispatch) => ({ dispatch }) } - function getDispatchPropsSelector() { - const mdtp = mapDispatchIsFactory ? mapDispatchToProps() : mapDispatchToProps - - if (!mdtp) { - return (_, __, dispatch) => ({ dispatch }) - } + if (typeof mdtp !== 'function') { + return createSelector( + (_, __, dispatch) => dispatch, + dispatch => bindActionCreators(mdtp, dispatch) + ) + } - if (typeof mdtp !== 'function') { - return createSelector( - (_, __, dispatch) => dispatch, - dispatch => bindActionCreators(mdtp, dispatch) - ) - } + if (!pure) { + return (_, props, dispatch) => mdtp(dispatch, props) + } - if (!pure) { - return (_, props, dispatch) => mdtp(dispatch, props) - } + if (mapDispatchToProps.length === 1) { + return createSelector((_, __, dispatch) => dispatch, mdtp) + } - if (mapDispatchToProps.length === 1) { - return createSelector((_, __, dispatch) => dispatch, mdtp) - } + return createSelector((_, __, dispatch) => dispatch, ownPropsSelector, mdtp) + } - return createSelector((_, __, dispatch) => dispatch, ownPropsSelector, mdtp) - } + function selectorFactory({ displayName }) { + const ownPropsSelector = createShallowEqualSelector((_, props) => props, props => props) - return verify('mergeProps', createShallowEqualSelector( - verify('mapStateToProps', getStatePropsSelector()), - verify('mapDispatchToProps', getDispatchPropsSelector()), + return verify(displayName, 'mergeProps', createShallowEqualSelector( + verify(displayName, 'mapStateToProps', getStatePropsSelector(ownPropsSelector)), + verify(displayName, 'mapDispatchToProps', getDispatchPropsSelector(ownPropsSelector)), ownPropsSelector, mergeProps || defaultMergeProps )) @@ -104,10 +97,9 @@ export default function connect( { pure, getDisplayName: name => `Connect(${name})`, - recomputationsProp: null, + shouldIncludeRecomputationsProp: false, ...options, methodName: 'connect', - shouldIncludeOriginalProps: !mergeProps, shouldUseState: Boolean(mapStateToProps) } ) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 3a9b0703d..31d213acc 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -1,7 +1,6 @@ import hoistStatics from 'hoist-non-react-statics' import invariant from 'invariant' import { Component, createElement } from 'react' -import { createStructuredSelector } from 'reselect' import createShallowEqualSelector from '../utils/createShallowEqualSelector' import storeShape from '../utils/storeShape' @@ -17,37 +16,12 @@ export default function connectAdvanced( thing: state.things[props.thingId], saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)), }))(YourComponent) - - Alternatively, it can return a plain object which will be passed to reselect's - 'createStructuredSelector' function to create the selector. For example: - - return connectAdvanced(() => ({ - thing: (state, props) => state.things[props.thingId], - saveThing: (_, props, dispatch) => fields => ( - dispatch(actionCreators.saveThing(props.thingId, fields)) - ), - }))(YourComponent) - - This is equivalent to wrapping the returned object in a call to `createStructuredSelector`, - but is supported as a convenience; This is the recommended approach to defining your - selectorFactory methods. The above example can be simplfied by using the `dispatchable` helper - method provided with connectAdvanced: - - connectAdvanced(() => ({ - thing: (state, props) => state.things[props.thingId], - saveThing: dispatchable(actionCreators.saveThing, (_, props) => props.thingId), - }))(YourComponent) - - A verbose but descriptive name for `dispatchable` would be `createBoundActionCreatorSelector`. - `dispatchable` will return a selector that binds the passed action creator arg to dispatch. Any - additional args given will be treated as selectors whose results should be partially applied to - the action creator. */ selectorFactory, // options object: { // the func used to compute this HOC's displayName from the wrapped component's displayName. - getDisplayName = name => `connectAdvanced(${name})`, + getDisplayName = name => `ConnectAdvanced(${name})`, // shown in error messages methodName = 'connectAdvanced', @@ -58,12 +32,8 @@ export default function connectAdvanced( // the name of the property passed to the wrapped element indicating the number of. // recomputations since it was mounted. useful for watching for unnecessary re-renders. - recomputationsProp = process.env.NODE_ENV !== 'production' ? '__recomputations' : null, - - // if true, the props passed to this HOC are merged with the results of the selector; in the - // case of key collision, selector value is kept and prop is discared. if false, only the - // selector results are passed to the wrapped element. - shouldIncludeOriginalProps = true, + recomputationsProp = '__recomputations', + shouldIncludeRecomputationsProp = process.env.NODE_ENV !== 'production', // if true, the selector receieves the current store state as the first arg, and this HOC // subscribes to store changes. if false, null is passed as the first arg of selector. @@ -76,44 +46,21 @@ export default function connectAdvanced( withRef = false } = {} ) { - function buildSelector() { - const { displayName, store } = this - const factoryResult = selectorFactory({ displayName }) - const ref = withRef ? (c => { this.wrappedInstance = c }) : undefined - const empty = {} - + function buildSelector({ displayName, store }) { + // wrap the source selector in a shallow equals because props objects with + // same properties are symantically equal to React... no need to re-render. const selector = createShallowEqualSelector( - // original props selector: - shouldIncludeOriginalProps - ? ((_, props) => props) - : (() => empty), - - // sourceSelector - typeof factoryResult === 'function' - ? factoryResult - : createStructuredSelector(factoryResult), - - // combine original props + selector props + ref - (props, sourceSelectorResults) => ({ - ...props, - ...sourceSelectorResults, - ref - })) - - return function runSelector(props) { - const recomputationsBefore = selector.recomputations() - const storeState = shouldUseState ? store.getState() : null - const selectorResults = selector(storeState, props, store.dispatch) - const recomputationsAfter = selector.recomputations() - - const finalProps = recomputationsProp - ? { ...selectorResults, [recomputationsProp]: recomputationsAfter } - : selectorResults - - return { - props: finalProps, - shouldUpdate: recomputationsBefore !== recomputationsAfter - } + selectorFactory({ displayName, dispatch: store.dispatch }), + result => result + ) + + return function runSelector(ownProps) { + const before = selector.recomputations() + const state = shouldUseState ? store.getState() : null + const props = selector(state, ownProps, store.dispatch) + const recomputations = selector.recomputations() + + return { props, recomputations, shouldUpdate: before !== recomputations } } } @@ -123,14 +70,11 @@ export default function connectAdvanced( class Connect extends Component { constructor(props, context) { super(props, context) - this.state = {} + this.state = { storeUpdates: 0 } } componentWillMount() { - this.version = version - this.displayName = Connect.displayName this.store = this.props[storeKey] || this.context[storeKey] - invariant(this.store, `Could not find "${storeKey}" in either the context or ` + `props of "${Connect.displayName}". ` + @@ -138,8 +82,7 @@ export default function connectAdvanced( `or explicitly pass "${storeKey}" as a prop to "${Connect.displayName}".` ) - this.selector = buildSelector.call(this) - this.trySubscribe() + this.init() } shouldComponentUpdate(nextProps) { @@ -149,8 +92,27 @@ export default function connectAdvanced( componentWillUnmount() { if (this.unsubscribe) this.unsubscribe() this.unsubscribe = null - this.selector = () => ({ props: {}, shouldUpdate: false }) this.store = null + this.selector = () => ({ props: {}, shouldUpdate: false }) + } + + init() { + this.version = version + + this.selector = buildSelector({ + displayName: Connect.displayName, + store: this.store + }) + + if (shouldUseState) { + if (this.unsubscribe) this.unsubscribe() + + this.unsubscribe = this.store.subscribe(() => { + if (this.unsubscribe) { + this.setState(state => ({ storeUpdates: state.storeUpdates++ })) + } + }) + } } getWrappedInstance() { @@ -158,26 +120,21 @@ export default function connectAdvanced( `To access the wrapped instance, you need to specify ` + `{ withRef: true } in the options argument of the ${methodName}() call.` ) - return this.wrappedInstance } - trySubscribe() { - if (!shouldUseState || this.unsubscribe) return - - let storeUpdates = 0 - this.unsubscribe = this.store.subscribe(() => { - if (this.unsubscribe) this.setState({ storeUpdates: storeUpdates++ }) - }) - } - isSubscribed() { return typeof this.unsubscribe === 'function' } render() { - const { props } = this.selector(this.props) - return createElement(WrappedComponent, props) + const { props, recomputations } = this.selector(this.props) + + return createElement(WrappedComponent, { + ...props, + ref: withRef ? (c => { this.wrappedInstance = c }) : undefined, + [recomputationsProp]: shouldIncludeRecomputationsProp ? recomputations : undefined + }) } } @@ -192,12 +149,8 @@ export default function connectAdvanced( 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.selector = buildSelector.call(this) + if (this.version !== version) this.init() } } From 5e72f23fca302034832ccd6f6a4316891c236859 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 16 Jun 2016 19:33:58 -0400 Subject: [PATCH 12/76] remove dispatchable()... this probably doesn't need to be a part of react-redux. --- src/index.js | 4 +--- src/utils/dispatchable.js | 18 ------------------ 2 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 src/utils/dispatchable.js diff --git a/src/index.js b/src/index.js index 96e8d37da..365804caa 100644 --- a/src/index.js +++ b/src/index.js @@ -2,12 +2,10 @@ import Provider from './components/Provider' import connect from './components/connect' import connectAdvanced from './components/connectAdvanced' import createShallowEqualSelector from './utils/createShallowEqualSelector' -import dispatchable from './utils/dispatchable' export { Provider, connect, connectAdvanced, - createShallowEqualSelector, - dispatchable + createShallowEqualSelector } diff --git a/src/utils/dispatchable.js b/src/utils/dispatchable.js deleted file mode 100644 index fb59ad53d..000000000 --- a/src/utils/dispatchable.js +++ /dev/null @@ -1,18 +0,0 @@ -import { createSelector } from 'reselect' - -export const selectDispatch = (_, __, dispatch) => dispatch - -export default function dispatchable(actionCreator, ...selectorsToPartiallyApply) { - if (selectorsToPartiallyApply.length === 0) { - return createSelector( - selectDispatch, - dispatch => (...args) => dispatch(actionCreator(...args)) - ) - } - - return createSelector( - selectDispatch, - ...selectorsToPartiallyApply, - (dispatch, ...partialArgs) => (...args) => dispatch(actionCreator(...partialArgs, ...args)) - ) -} From 7890b4c8fafcbbb17d1e22315de98d83d78f8f65 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 16 Jun 2016 19:36:42 -0400 Subject: [PATCH 13/76] remove unneeded selector value on unmount --- src/components/connectAdvanced.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 31d213acc..7a11c963f 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -93,7 +93,7 @@ export default function connectAdvanced( if (this.unsubscribe) this.unsubscribe() this.unsubscribe = null this.store = null - this.selector = () => ({ props: {}, shouldUpdate: false }) + this.selector = null } init() { From 612562df84737f285d9d6d4f785ee108e1112fe0 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 16 Jun 2016 20:12:01 -0400 Subject: [PATCH 14/76] avoid excess verify() when using defaultMergeProps --- src/components/connect.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/connect.js b/src/components/connect.js index 265806818..49a60ce44 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -14,9 +14,9 @@ const defaultMergeProps = (stateProps, dispatchProps, ownProps) => ({ const empty = {} -function verify(displayName, methodName, selector) { +function verify(displayName, methodName, func) { return (...args) => { - const props = selector(...args) + const props = func(...args) if (!isPlainObject(props)) { warning( `${methodName}() in ${displayName} must return a plain object. ` + @@ -84,12 +84,14 @@ export default function connect( function selectorFactory({ displayName }) { const ownPropsSelector = createShallowEqualSelector((_, props) => props, props => props) - return verify(displayName, 'mergeProps', createShallowEqualSelector( + return createShallowEqualSelector( verify(displayName, 'mapStateToProps', getStatePropsSelector(ownPropsSelector)), verify(displayName, 'mapDispatchToProps', getDispatchPropsSelector(ownPropsSelector)), ownPropsSelector, - mergeProps || defaultMergeProps - )) + mergeProps + ? verify(displayName, 'mergeProps', mergeProps) + : defaultMergeProps + ) } return connectAdvanced( From 81434df164b8e5c091bd9d211b12421135786e73 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 16 Jun 2016 20:13:17 -0400 Subject: [PATCH 15/76] pull buildSelector out of connectAdvanced... less function nesting --- src/components/connectAdvanced.js | 42 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 7a11c963f..e3ed0b1be 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -5,8 +5,25 @@ import { Component, createElement } from 'react' import createShallowEqualSelector from '../utils/createShallowEqualSelector' import storeShape from '../utils/storeShape' -let hotReloadingVersion = 0 +function buildSelector({ displayName, store, selectorFactory, shouldUseState }) { + // wrap the source selector in a shallow equals because props objects with + // same properties are symantically equal to React... no need to re-render. + const selector = createShallowEqualSelector( + selectorFactory({ displayName, dispatch: store.dispatch }), + result => result + ) + + return function runSelector(ownProps) { + const before = selector.recomputations() + const state = shouldUseState ? store.getState() : null + const props = selector(state, ownProps, store.dispatch) + const recomputations = selector.recomputations() + + return { props, recomputations, shouldUpdate: before !== recomputations } + } +} +let hotReloadingVersion = 0 export default function connectAdvanced( /* selectorFactory is a func is responsible for returning the selector function used to compute new @@ -46,26 +63,7 @@ export default function connectAdvanced( withRef = false } = {} ) { - function buildSelector({ displayName, store }) { - // wrap the source selector in a shallow equals because props objects with - // same properties are symantically equal to React... no need to re-render. - const selector = createShallowEqualSelector( - selectorFactory({ displayName, dispatch: store.dispatch }), - result => result - ) - - return function runSelector(ownProps) { - const before = selector.recomputations() - const state = shouldUseState ? store.getState() : null - const props = selector(state, ownProps, store.dispatch) - const recomputations = selector.recomputations() - - return { props, recomputations, shouldUpdate: before !== recomputations } - } - } - const version = hotReloadingVersion++ - return function wrapWithConnect(WrappedComponent) { class Connect extends Component { constructor(props, context) { @@ -101,7 +99,9 @@ export default function connectAdvanced( this.selector = buildSelector({ displayName: Connect.displayName, - store: this.store + store: this.store, + selectorFactory, + shouldUseState }) if (shouldUseState) { From 71655adda6be573c16131ad22390edf1d6b74f38 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 16 Jun 2016 20:40:04 -0400 Subject: [PATCH 16/76] refactor connect() selector builders --- src/components/connect.js | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/components/connect.js b/src/components/connect.js index 49a60ce44..2efdfa56e 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -1,6 +1,6 @@ import isPlainObject from 'lodash/isPlainObject' import { bindActionCreators } from 'redux' -import { createSelector } from 'reselect' +import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect' import connectAdvanced from './connectAdvanced' import createShallowEqualSelector from '../utils/createShallowEqualSelector' @@ -38,6 +38,10 @@ export default function connect( ...options } = {} ) { + const equalityCheck = pure + ? ((a, b) => a === b) + : ((a, b) => a === empty && b === empty) + function getStatePropsSelector(ownPropsSelector) { const mstp = mapStateIsFactory ? mapStateToProps() : mapStateToProps @@ -45,15 +49,11 @@ export default function connect( return () => empty } - if (!pure) { - return (state, props) => mstp(state, props) - } - - if (mapStateToProps.length === 1) { - return createSelector(state => state, mstp) - } - - return createSelector(state => state, ownPropsSelector, mstp) + return createSelectorCreator(defaultMemoize, equalityCheck)( + state => state, + (...args) => mstp && mstp.length !== 1 ? ownPropsSelector(...args) : empty, + mstp + ) } function getDispatchPropsSelector(ownPropsSelector) { @@ -70,15 +70,11 @@ export default function connect( ) } - if (!pure) { - return (_, props, dispatch) => mdtp(dispatch, props) - } - - if (mapDispatchToProps.length === 1) { - return createSelector((_, __, dispatch) => dispatch, mdtp) - } - - return createSelector((_, __, dispatch) => dispatch, ownPropsSelector, mdtp) + return createSelectorCreator(defaultMemoize, equalityCheck)( + (_, __, dispatch) => dispatch, + (...args) => mdtp && mdtp.length !== 1 ? ownPropsSelector(...args) : empty, + mdtp + ) } function selectorFactory({ displayName }) { From 3266aa9c8e384f1219d6ecd9d8568366eb7e7f91 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 16 Jun 2016 20:58:50 -0400 Subject: [PATCH 17/76] remove need for explicit factory flags --- src/components/connect.js | 49 ++++++++++++++++++++------------- test/components/connect.spec.js | 4 +-- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/components/connect.js b/src/components/connect.js index 2efdfa56e..5fe172c74 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -32,48 +32,59 @@ export default function connect( mapDispatchToProps, mergeProps, { - mapStateIsFactory, - mapDispatchIsFactory, pure = true, ...options } = {} ) { - const equalityCheck = pure - ? ((a, b) => a === b) - : ((a, b) => a === empty && b === empty) + function createFactoryAwareSelector(selectFirstArg, ownPropsSelector, mapSomethingToProps) { + const equalityCheck = pure + ? ((a, b) => a === b) + : ((a, b) => a === empty && b === empty) - function getStatePropsSelector(ownPropsSelector) { - const mstp = mapStateIsFactory ? mapStateToProps() : mapStateToProps + let map = mapSomethingToProps + let proxy = (...args) => { + const result = map(...args) + if (typeof result !== 'function') return result + map = result + proxy = map + return map(...args) + } + + return createSelectorCreator(defaultMemoize, equalityCheck)( + selectFirstArg, + (...args) => map && map.length !== 1 ? ownPropsSelector(...args) : empty, + (...args) => proxy(...args) + ) + } - if (!mstp) { + function getStatePropsSelector(ownPropsSelector) { + if (!mapStateToProps) { return () => empty } - return createSelectorCreator(defaultMemoize, equalityCheck)( + return createFactoryAwareSelector( state => state, - (...args) => mstp && mstp.length !== 1 ? ownPropsSelector(...args) : empty, - mstp + ownPropsSelector, + mapStateToProps ) } function getDispatchPropsSelector(ownPropsSelector) { - const mdtp = mapDispatchIsFactory ? mapDispatchToProps() : mapDispatchToProps - - if (!mdtp) { + if (!mapDispatchToProps) { return (_, __, dispatch) => ({ dispatch }) } - if (typeof mdtp !== 'function') { + if (typeof mapDispatchToProps !== 'function') { return createSelector( (_, __, dispatch) => dispatch, - dispatch => bindActionCreators(mdtp, dispatch) + dispatch => bindActionCreators(mapDispatchToProps, dispatch) ) } - return createSelectorCreator(defaultMemoize, equalityCheck)( + return createFactoryAwareSelector( (_, __, dispatch) => dispatch, - (...args) => mdtp && mdtp.length !== 1 ? ownPropsSelector(...args) : empty, - mdtp + ownPropsSelector, + mapDispatchToProps ) } diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 4ac4ba9fa..6ad3684ad 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1677,7 +1677,7 @@ describe('React', () => { } } - @connect(mapStateFactory, null, null, { mapStateIsFactory: true }) + @connect(mapStateFactory) class Container extends Component { componentWillUpdate() { updatedCount++ @@ -1721,7 +1721,7 @@ describe('React', () => { return { ...stateProps, ...dispatchProps, name: parentProps.name } } - @connect(null, mapDispatchFactory, mergeParentDispatch, { mapDispatchIsFactory: true }) + @connect(null, mapDispatchFactory, mergeParentDispatch) class Passthrough extends Component { componentWillUpdate() { updatedCount++ From 4dbeeb1a0787838f344dbeb69667601e8535a0e2 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Fri, 17 Jun 2016 00:33:08 -0400 Subject: [PATCH 18/76] don't do shape verification in production ENV --- src/components/connect.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/connect.js b/src/components/connect.js index 5fe172c74..f16339146 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -15,6 +15,8 @@ const defaultMergeProps = (stateProps, dispatchProps, ownProps) => ({ const empty = {} function verify(displayName, methodName, func) { + if (process.env.NODE_ENV === 'production') return func + return (...args) => { const props = func(...args) if (!isPlainObject(props)) { From 74e0e09b797a4429b5acb1d301c10b12cffbfe79 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Fri, 17 Jun 2016 01:21:33 -0400 Subject: [PATCH 19/76] refactor to split init from trySubscribe --- src/components/connectAdvanced.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index e3ed0b1be..f550ea81c 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -69,10 +69,8 @@ export default function connectAdvanced( constructor(props, context) { super(props, context) this.state = { storeUpdates: 0 } - } - - componentWillMount() { this.store = this.props[storeKey] || this.context[storeKey] + invariant(this.store, `Could not find "${storeKey}" in either the context or ` + `props of "${Connect.displayName}". ` + @@ -83,6 +81,11 @@ export default function connectAdvanced( this.init() } + componentWillMount() { + // TODO this is bad. needs to be didMount, but fails a test. + this.trySubscribe() + } + shouldComponentUpdate(nextProps) { return !pure || this.selector(nextProps).shouldUpdate } @@ -96,14 +99,16 @@ export default function connectAdvanced( init() { this.version = version - + this.selector = buildSelector({ displayName: Connect.displayName, store: this.store, selectorFactory, shouldUseState }) + } + trySubscribe() { if (shouldUseState) { if (this.unsubscribe) this.unsubscribe() @@ -150,7 +155,10 @@ export default function connectAdvanced( if (process.env.NODE_ENV !== 'production') { Connect.prototype.componentWillUpdate = function componentWillUpdate() { // We are hot reloading! - if (this.version !== version) this.init() + if (this.version !== version) { + this.init() + this.trySubscribe() + } } } From cd75a22764d0fcf8be3d8fc74da1fb3063c11ae9 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Fri, 17 Jun 2016 10:27:48 -0400 Subject: [PATCH 20/76] Move subscribe from willMount to didMount to avoid serverside memory leaks --- src/components/connect.js | 5 +++-- src/components/connectAdvanced.js | 8 ++++++-- test/components/connect.spec.js | 8 ++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/components/connect.js b/src/components/connect.js index f16339146..1275c1d68 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -16,15 +16,16 @@ const empty = {} function verify(displayName, methodName, func) { if (process.env.NODE_ENV === 'production') return func - + let hasVerified = false return (...args) => { const props = func(...args) - if (!isPlainObject(props)) { + if (!hasVerified && !isPlainObject(props)) { warning( `${methodName}() in ${displayName} must return a plain object. ` + `Instead received ${props}.` ) } + hasVerified = true return props } } diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index f550ea81c..ba07df616 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -81,9 +81,12 @@ export default function connectAdvanced( this.init() } - componentWillMount() { - // TODO this is bad. needs to be didMount, but fails a test. + componentDidMount() { this.trySubscribe() + + if (this.recomputations !== this.selector(this.props).recomputations) { + this.forceUpdate() + } } shouldComponentUpdate(nextProps) { @@ -134,6 +137,7 @@ export default function connectAdvanced( render() { const { props, recomputations } = this.selector(this.props) + this.recomputations = recomputations return createElement(WrappedComponent, { ...props, diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 6ad3684ad..bbd831502 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1469,8 +1469,8 @@ describe('React', () => { const target = TestUtils.findRenderedComponentWithType(tree, Passthrough) const wrapper = TestUtils.findRenderedComponentWithType(tree, StatefulWrapper) - expect(mapStateSpy.calls.length).toBe(1) - expect(mapDispatchSpy.calls.length).toBe(1) + expect(mapStateSpy.calls.length).toBe(2) + expect(mapDispatchSpy.calls.length).toBe(2) expect(target.props.statefulValue).toEqual('foo') // Impure update @@ -1478,8 +1478,8 @@ describe('React', () => { storeGetter.storeKey = 'bar' wrapper.setState({ storeGetter }) - expect(mapStateSpy.calls.length).toBe(2) - expect(mapDispatchSpy.calls.length).toBe(2) + expect(mapStateSpy.calls.length).toBe(3) + expect(mapDispatchSpy.calls.length).toBe(3) expect(target.props.statefulValue).toEqual('bar') }) From 9de43b2509d649622c7b9d90ed490f44df2c530d Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Fri, 17 Jun 2016 12:08:59 -0400 Subject: [PATCH 21/76] Remove extra createShallowEqualSelector file since it's not adding much value --- src/components/connect.js | 9 ++++++--- src/components/connectAdvanced.js | 5 +++-- src/index.js | 4 ++-- src/utils/createShallowEqualSelector.js | 4 ---- 4 files changed, 11 insertions(+), 11 deletions(-) delete mode 100644 src/utils/createShallowEqualSelector.js diff --git a/src/components/connect.js b/src/components/connect.js index 1275c1d68..10ba5db6f 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -3,7 +3,7 @@ import { bindActionCreators } from 'redux' import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect' import connectAdvanced from './connectAdvanced' -import createShallowEqualSelector from '../utils/createShallowEqualSelector' +import shallowEqual from '../utils/shallowEqual' import warning from '../utils/warning' const defaultMergeProps = (stateProps, dispatchProps, ownProps) => ({ @@ -92,9 +92,12 @@ export default function connect( } function selectorFactory({ displayName }) { - const ownPropsSelector = createShallowEqualSelector((_, props) => props, props => props) + const ownPropsSelector = createSelectorCreator(defaultMemoize, shallowEqual)( + (_, props) => props, + props => props + ) - return createShallowEqualSelector( + return createSelectorCreator(defaultMemoize, shallowEqual)( verify(displayName, 'mapStateToProps', getStatePropsSelector(ownPropsSelector)), verify(displayName, 'mapDispatchToProps', getDispatchPropsSelector(ownPropsSelector)), ownPropsSelector, diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index ba07df616..9ceffb4c4 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -1,14 +1,15 @@ import hoistStatics from 'hoist-non-react-statics' import invariant from 'invariant' import { Component, createElement } from 'react' +import { createSelectorCreator, defaultMemoize } from 'reselect' -import createShallowEqualSelector from '../utils/createShallowEqualSelector' +import shallowEqual from '../utils/shallowEqual' import storeShape from '../utils/storeShape' function buildSelector({ displayName, store, selectorFactory, shouldUseState }) { // wrap the source selector in a shallow equals because props objects with // same properties are symantically equal to React... no need to re-render. - const selector = createShallowEqualSelector( + const selector = createSelectorCreator(defaultMemoize, shallowEqual)( selectorFactory({ displayName, dispatch: store.dispatch }), result => result ) diff --git a/src/index.js b/src/index.js index 365804caa..fd4592c60 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,11 @@ import Provider from './components/Provider' import connect from './components/connect' import connectAdvanced from './components/connectAdvanced' -import createShallowEqualSelector from './utils/createShallowEqualSelector' +import shallowEqual from './utils/shallowEqual' export { Provider, connect, connectAdvanced, - createShallowEqualSelector + shallowEqual } diff --git a/src/utils/createShallowEqualSelector.js b/src/utils/createShallowEqualSelector.js deleted file mode 100644 index e60bdbadf..000000000 --- a/src/utils/createShallowEqualSelector.js +++ /dev/null @@ -1,4 +0,0 @@ -import { createSelectorCreator, defaultMemoize } from 'reselect' -import shallowEqual from './shallowEqual' - -export default createSelectorCreator(defaultMemoize, shallowEqual) From 3e1ddbd63a55585b0e844cb07f6f286d48be075b Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Fri, 17 Jun 2016 12:38:01 -0400 Subject: [PATCH 22/76] move ref/recomputationsProp setters into buildSelector... simplify render() --- src/components/connectAdvanced.js | 45 ++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 9ceffb4c4..e2bf3c72c 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -6,12 +6,21 @@ import { createSelectorCreator, defaultMemoize } from 'reselect' import shallowEqual from '../utils/shallowEqual' import storeShape from '../utils/storeShape' -function buildSelector({ displayName, store, selectorFactory, shouldUseState }) { - // wrap the source selector in a shallow equals because props objects with - // same properties are symantically equal to React... no need to re-render. +function buildSelector({ + displayName, + ref, + selectorFactory, + recomputationsProp, + shouldIncludeRecomputationsProp, + shouldUseState, + store + }) { + // wrap the source selector in a shallow equals because props objects with same properties are + // symantically equal to React... no need to re-render. make a shallow copy of the result so that + // mutations for ref and recomputationsProp don't get leaked to the original result const selector = createSelectorCreator(defaultMemoize, shallowEqual)( selectorFactory({ displayName, dispatch: store.dispatch }), - result => result + result => ({ ...result }) ) return function runSelector(ownProps) { @@ -19,8 +28,14 @@ function buildSelector({ displayName, store, selectorFactory, shouldUseState }) const state = shouldUseState ? store.getState() : null const props = selector(state, ownProps, store.dispatch) const recomputations = selector.recomputations() + const shouldUpdate = before !== recomputations - return { props, recomputations, shouldUpdate: before !== recomputations } + if (shouldUpdate) { + props.ref = ref + if (shouldIncludeRecomputationsProp) props[recomputationsProp] = recomputations + } + + return { props, recomputations, shouldUpdate } } } @@ -69,6 +84,7 @@ export default function connectAdvanced( class Connect extends Component { constructor(props, context) { super(props, context) + this.version = version this.state = { storeUpdates: 0 } this.store = this.props[storeKey] || this.context[storeKey] @@ -79,7 +95,7 @@ export default function connectAdvanced( `or explicitly pass "${storeKey}" as a prop to "${Connect.displayName}".` ) - this.init() + this.initSelector() } componentDidMount() { @@ -101,13 +117,14 @@ export default function connectAdvanced( this.selector = null } - init() { - this.version = version - + initSelector() { this.selector = buildSelector({ displayName: Connect.displayName, store: this.store, + ref: withRef ? (ref => { this.wrappedInstance = ref }) : undefined, + recomputationsProp, selectorFactory, + shouldIncludeRecomputationsProp, shouldUseState }) } @@ -139,12 +156,7 @@ export default function connectAdvanced( render() { const { props, recomputations } = this.selector(this.props) this.recomputations = recomputations - - return createElement(WrappedComponent, { - ...props, - ref: withRef ? (c => { this.wrappedInstance = c }) : undefined, - [recomputationsProp]: shouldIncludeRecomputationsProp ? recomputations : undefined - }) + return createElement(WrappedComponent, props) } } @@ -161,7 +173,8 @@ export default function connectAdvanced( Connect.prototype.componentWillUpdate = function componentWillUpdate() { // We are hot reloading! if (this.version !== version) { - this.init() + this.version = version + this.initSelector() this.trySubscribe() } } From b769209bda51f8ff3837a2cb08cf51f70a992027 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Fri, 17 Jun 2016 13:39:18 -0400 Subject: [PATCH 23/76] refactor + comment advancedConnect --- src/components/connectAdvanced.js | 86 +++++++++++++++++++------------ 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index e2bf3c72c..d4e7ef231 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -11,31 +11,42 @@ function buildSelector({ ref, selectorFactory, recomputationsProp, - shouldIncludeRecomputationsProp, shouldUseState, store }) { // wrap the source selector in a shallow equals because props objects with same properties are - // symantically equal to React... no need to re-render. make a shallow copy of the result so that - // mutations for ref and recomputationsProp don't get leaked to the original result + // symantically equal to React... no need to re-render. const selector = createSelectorCreator(defaultMemoize, shallowEqual)( - selectorFactory({ displayName, dispatch: store.dispatch }), - result => ({ ...result }) + + // get the source selector from the factory + selectorFactory({ + // useful for selector factories to show in error messages + displayName, + // useful for selectors that want to bind action creators before returning their selector + dispatch: store.dispatch + }), + + // make a shallow copy so that mutations don't leak to the original selector. any additional + // mutations added to the mutateProps func below should be checked for here + ref || recomputationsProp + ? (result => ({ ...result })) + : (result => result) ) + function mutateProps(props, before, recomputations) { + if (before === recomputations) return + if (ref) props.ref = ref + if (recomputationsProp) props[recomputationsProp] = recomputations + } + return function runSelector(ownProps) { const before = selector.recomputations() const state = shouldUseState ? store.getState() : null const props = selector(state, ownProps, store.dispatch) const recomputations = selector.recomputations() - const shouldUpdate = before !== recomputations - if (shouldUpdate) { - props.ref = ref - if (shouldIncludeRecomputationsProp) props[recomputationsProp] = recomputations - } - - return { props, recomputations, shouldUpdate } + mutateProps(props, before, recomputations) + return { props, recomputations } } } @@ -54,22 +65,25 @@ export default function connectAdvanced( // options object: { // the func used to compute this HOC's displayName from the wrapped component's displayName. + // probably overridden by wrapper functions such as connect() getDisplayName = name => `ConnectAdvanced(${name})`, // shown in error messages + // probably overridden by wrapper functions such as connect() methodName = 'connectAdvanced', // if true, shouldComponentUpdate will only be true of the selector recomputes for nextProps. // if false, shouldComponentUpdate will always be true. pure = true, - // the name of the property passed to the wrapped element indicating the number of. - // recomputations since it was mounted. useful for watching for unnecessary re-renders. - recomputationsProp = '__recomputations', - shouldIncludeRecomputationsProp = process.env.NODE_ENV !== 'production', + // if defined, the name of the property passed to the wrapped element indicating the number of + // recomputations since it was mounted. useful for watching in react devtools for unnecessary + // re-renders. + recomputationsProp = undefined, // if true, the selector receieves the current store state as the first arg, and this HOC - // subscribes to store changes. if false, null is passed as the first arg of selector. + // subscribes to store changes during componentDidMount. if false, null is passed as the first + // arg of selector and store.subscribe() is never called. shouldUseState = true, // the key of props/context to get the store @@ -85,7 +99,7 @@ export default function connectAdvanced( constructor(props, context) { super(props, context) this.version = version - this.state = { storeUpdates: 0 } + this.state = {} this.store = this.props[storeKey] || this.context[storeKey] invariant(this.store, @@ -101,13 +115,13 @@ export default function connectAdvanced( componentDidMount() { this.trySubscribe() - if (this.recomputations !== this.selector(this.props).recomputations) { - this.forceUpdate() - } + // check for recomputations that happened after this component has rendered, such as + // when a child component dispatches an action in its componentWillMount + if (this.hasUnrenderedRecomputations(this.props)) this.forceUpdate() } shouldComponentUpdate(nextProps) { - return !pure || this.selector(nextProps).shouldUpdate + return !pure || this.hasUnrenderedRecomputations(nextProps) } componentWillUnmount() { @@ -118,27 +132,33 @@ export default function connectAdvanced( } initSelector() { + this.recomputationsDuringLastRender = null + this.selector = buildSelector({ displayName: Connect.displayName, store: this.store, ref: withRef ? (ref => { this.wrappedInstance = ref }) : undefined, recomputationsProp, selectorFactory, - shouldIncludeRecomputationsProp, shouldUseState }) } + hasUnrenderedRecomputations(props) { + return this.recomputationsDuringLastRender !== this.selector(props).recomputations + } + trySubscribe() { - if (shouldUseState) { - if (this.unsubscribe) this.unsubscribe() - - this.unsubscribe = this.store.subscribe(() => { - if (this.unsubscribe) { - this.setState(state => ({ storeUpdates: state.storeUpdates++ })) - } - }) - } + if (!shouldUseState) return + if (this.unsubscribe) this.unsubscribe() + + this.unsubscribe = this.store.subscribe(() => { + if (this.unsubscribe) { + // invoke setState() instead of forceUpdate() so that shouldComponentUpdate() + // gets a chance to prevent unneeded re-renders + this.setState({}) + } + }) } getWrappedInstance() { @@ -155,7 +175,7 @@ export default function connectAdvanced( render() { const { props, recomputations } = this.selector(this.props) - this.recomputations = recomputations + this.recomputationsDuringLastRender = recomputations return createElement(WrappedComponent, props) } } From 70395c972e5625868f981bc07b064ae7381decb5 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Fri, 17 Jun 2016 14:06:12 -0400 Subject: [PATCH 24/76] pull buildSelector func into own file --- src/components/connectAdvanced.js | 47 +------------------------------ src/utils/buildSelector.js | 47 +++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 46 deletions(-) create mode 100644 src/utils/buildSelector.js diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index d4e7ef231..da66fce0d 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -1,55 +1,10 @@ import hoistStatics from 'hoist-non-react-statics' import invariant from 'invariant' import { Component, createElement } from 'react' -import { createSelectorCreator, defaultMemoize } from 'reselect' -import shallowEqual from '../utils/shallowEqual' +import buildSelector from '../utils/buildSelector' import storeShape from '../utils/storeShape' -function buildSelector({ - displayName, - ref, - selectorFactory, - recomputationsProp, - shouldUseState, - store - }) { - // wrap the source selector in a shallow equals because props objects with same properties are - // symantically equal to React... no need to re-render. - const selector = createSelectorCreator(defaultMemoize, shallowEqual)( - - // get the source selector from the factory - selectorFactory({ - // useful for selector factories to show in error messages - displayName, - // useful for selectors that want to bind action creators before returning their selector - dispatch: store.dispatch - }), - - // make a shallow copy so that mutations don't leak to the original selector. any additional - // mutations added to the mutateProps func below should be checked for here - ref || recomputationsProp - ? (result => ({ ...result })) - : (result => result) - ) - - function mutateProps(props, before, recomputations) { - if (before === recomputations) return - if (ref) props.ref = ref - if (recomputationsProp) props[recomputationsProp] = recomputations - } - - return function runSelector(ownProps) { - const before = selector.recomputations() - const state = shouldUseState ? store.getState() : null - const props = selector(state, ownProps, store.dispatch) - const recomputations = selector.recomputations() - - mutateProps(props, before, recomputations) - return { props, recomputations } - } -} - let hotReloadingVersion = 0 export default function connectAdvanced( /* diff --git a/src/utils/buildSelector.js b/src/utils/buildSelector.js new file mode 100644 index 000000000..a29be8a10 --- /dev/null +++ b/src/utils/buildSelector.js @@ -0,0 +1,47 @@ +import { createSelectorCreator, defaultMemoize } from 'reselect' + +import shallowEqual from '../utils/shallowEqual' + +export default function buildSelector({ + displayName, + ref, + selectorFactory, + recomputationsProp, + shouldUseState, + store + }) { + // wrap the source selector in a shallow equals because props objects with same properties are + // symantically equal to React... no need to re-render. + const selector = createSelectorCreator(defaultMemoize, shallowEqual)( + + // get the source selector from the factory + selectorFactory({ + // useful for selector factories to show in error messages + displayName, + // useful for selectors that want to bind action creators before returning their selector + dispatch: store.dispatch + }), + + // make a shallow copy so that mutations don't leak to the original selector. any additional + // mutations added to the mutateProps func below should be checked for here + ref || recomputationsProp + ? (result => ({ ...result })) + : (result => result) + ) + + function mutateProps(props, before, recomputations) { + if (before === recomputations) return + if (ref) props.ref = ref + if (recomputationsProp) props[recomputationsProp] = recomputations + } + + return function runSelector(ownProps) { + const before = selector.recomputations() + const state = shouldUseState ? store.getState() : null + const props = selector(state, ownProps, store.dispatch) + const recomputations = selector.recomputations() + + mutateProps(props, before, recomputations) + return { props, recomputations } + } +} From 2ae0c3caac777705b8ab0885c2e8f1e43fc910bb Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Fri, 17 Jun 2016 16:14:38 -0400 Subject: [PATCH 25/76] Make buildSelector injectable into connectAdvanced as another option --- src/components/connectAdvanced.js | 19 +++++++--- src/index.js | 4 +++ src/utils/buildSelector.js | 58 +++++++++++++++++-------------- 3 files changed, 50 insertions(+), 31 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index da66fce0d..dd843cd11 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -2,7 +2,7 @@ import hoistStatics from 'hoist-non-react-statics' import invariant from 'invariant' import { Component, createElement } from 'react' -import buildSelector from '../utils/buildSelector' +import defaultBuildSelector from '../utils/buildSelector' import storeShape from '../utils/storeShape' let hotReloadingVersion = 0 @@ -19,6 +19,12 @@ export default function connectAdvanced( selectorFactory, // options object: { + // this is the function that invokes the selectorFactory and enhances it with a few important + // behaviors. you probably want to leave this alone unless you've read and understand the source + // for components/connectAdvanced.js and utils/buildSelector.js, then maybe you can use this as + // an injection point for custom behavior, for example hooking in some custom devtools. + buildSelector = defaultBuildSelector, + // the func used to compute this HOC's displayName from the wrapped component's displayName. // probably overridden by wrapper functions such as connect() getDisplayName = name => `ConnectAdvanced(${name})`, @@ -91,11 +97,16 @@ export default function connectAdvanced( this.selector = buildSelector({ displayName: Connect.displayName, - store: this.store, + dispatch: this.store.dispatch, + getState: shouldUseState ? this.store.getState : (() => null), ref: withRef ? (ref => { this.wrappedInstance = ref }) : undefined, - recomputationsProp, selectorFactory, - shouldUseState + recomputationsProp, + methodName, + pure, + shouldUseState, + storeKey, + withRef }) } diff --git a/src/index.js b/src/index.js index fd4592c60..dab737386 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,15 @@ import Provider from './components/Provider' import connect from './components/connect' import connectAdvanced from './components/connectAdvanced' + +import buildSelector from './utils/buildSelector' import shallowEqual from './utils/shallowEqual' export { Provider, connect, connectAdvanced, + + buildSelector, shallowEqual } diff --git a/src/utils/buildSelector.js b/src/utils/buildSelector.js index a29be8a10..f1054d7d0 100644 --- a/src/utils/buildSelector.js +++ b/src/utils/buildSelector.js @@ -3,45 +3,49 @@ import { createSelectorCreator, defaultMemoize } from 'reselect' import shallowEqual from '../utils/shallowEqual' export default function buildSelector({ - displayName, - ref, selectorFactory, + dispatch, + getState, + ref, recomputationsProp, - shouldUseState, - store + ...options }) { - // wrap the source selector in a shallow equals because props objects with same properties are - // symantically equal to React... no need to re-render. - const selector = createSelectorCreator(defaultMemoize, shallowEqual)( + // the final props obect is mutated directly instead of projecting into a new object to avoid + // some extra object creation and tracking. + const mightMutateProps = ref || recomputationsProp + function mutateProps(props, before, recomputations) { + if (before === recomputations) return + if (ref) props.ref = ref + if (recomputationsProp) props[recomputationsProp] = recomputations + } - // get the source selector from the factory + // wrap the source selector in a shallow equals because props objects with same properties are + // semantically equal to React... no need to re-render. + const masterSelector = createSelectorCreator(defaultMemoize, shallowEqual)( selectorFactory({ - // useful for selector factories to show in error messages - displayName, - // useful for selectors that want to bind action creators before returning their selector - dispatch: store.dispatch + // useful for selecto factories that want to bind action creators before returning + // their selector + dispatch, + // additional options passed to buildSelector are passed along to the selectorFactory + ...options }), - - // make a shallow copy so that mutations don't leak to the original selector. any additional - // mutations added to the mutateProps func below should be checked for here - ref || recomputationsProp + + // make a shallow copy so that fields added by mutateProps don't leak to the original selector. + // this is especially important for 'ref' since that's a reference back to the component + // instance. a singleton memoized selector would then be holding a reference to the instance, + // preventing the instance from being garbage collected + mightMutateProps ? (result => ({ ...result })) : (result => result) ) - function mutateProps(props, before, recomputations) { - if (before === recomputations) return - if (ref) props.ref = ref - if (recomputationsProp) props[recomputationsProp] = recomputations - } - return function runSelector(ownProps) { - const before = selector.recomputations() - const state = shouldUseState ? store.getState() : null - const props = selector(state, ownProps, store.dispatch) - const recomputations = selector.recomputations() + const before = masterSelector.recomputations() + const state = getState() + const props = masterSelector(state, ownProps, dispatch) + const recomputations = masterSelector.recomputations() - mutateProps(props, before, recomputations) + if (mightMutateProps) mutateProps(props, before, recomputations) return { props, recomputations } } } From e2e71085935a5eaca8235676eb31aa53465d8086 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Fri, 17 Jun 2016 20:16:04 -0400 Subject: [PATCH 26/76] refactor+comment connect.js --- src/components/connect.js | 201 +++++++++++++++++++-------------- src/utils/verifyPlainObject.js | 20 ++++ 2 files changed, 136 insertions(+), 85 deletions(-) create mode 100644 src/utils/verifyPlainObject.js diff --git a/src/components/connect.js b/src/components/connect.js index 10ba5db6f..361ec9744 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -1,118 +1,149 @@ -import isPlainObject from 'lodash/isPlainObject' import { bindActionCreators } from 'redux' import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect' import connectAdvanced from './connectAdvanced' import shallowEqual from '../utils/shallowEqual' -import warning from '../utils/warning' - -const defaultMergeProps = (stateProps, dispatchProps, ownProps) => ({ - ...ownProps, - ...stateProps, - ...dispatchProps -}) - -const empty = {} - -function verify(displayName, methodName, func) { - if (process.env.NODE_ENV === 'production') return func - let hasVerified = false - return (...args) => { - const props = func(...args) - if (!hasVerified && !isPlainObject(props)) { - warning( - `${methodName}() in ${displayName} must return a plain object. ` + - `Instead received ${props}.` - ) - } - hasVerified = true - return props - } +import verifyPlainObject from '../utils/verifyPlainObject' + +const createShallowSelector = createSelectorCreator(defaultMemoize, shallowEqual) + +export function getOwnPropsSelector(pure) { + return pure + ? createShallowSelector((_, props) => props, props => props) + : ((_, props) => props) } -export default function connect( - mapStateToProps, - mapDispatchToProps, - mergeProps, - { - pure = true, - ...options - } = {} + +// used by getStatePropsSelector and getDispatchPropsSelector to create a memoized selector function +// based on the given mapStateOrDispatchToProps function. It also detects if that function is a +// factory based on its first returned result. +// if not pure, then results should always be recomputed (except if it's ignoring prop changes) +export function createFactoryAwareSelector( + pure, + ownPropsSelector, + selectStateOrDispatch, + mapStateOrDispatchToProps ) { - function createFactoryAwareSelector(selectFirstArg, ownPropsSelector, mapSomethingToProps) { - const equalityCheck = pure - ? ((a, b) => a === b) - : ((a, b) => a === empty && b === empty) - - let map = mapSomethingToProps - let proxy = (...args) => { - const result = map(...args) - if (typeof result !== 'function') return result + // propsSelector. if the map function only takes 1 arg, it shouldn't recompute results for props + // changes. since this depends on map, which is mutable, propsSelector must be recomputed when + // map changes + const noProps = {} + const getPropsSelector = func => (func.length !== 1 ? ownPropsSelector : (() => noProps)) + let propsSelector = getPropsSelector(mapStateOrDispatchToProps) + + // factory detection. if the first result of mapSomethingToProps is a function, use that as the + // true mapSomethingToProps + let map = mapStateOrDispatchToProps + let mapProxy = (...args) => { + const result = map(...args) + if (typeof result === 'function') { map = result - proxy = map + propsSelector = getPropsSelector(map) + mapProxy = map return map(...args) + } else { + mapProxy = map + return result } - - return createSelectorCreator(defaultMemoize, equalityCheck)( - selectFirstArg, - (...args) => map && map.length !== 1 ? ownPropsSelector(...args) : empty, - (...args) => proxy(...args) + } + + if (pure) { + return createSelector( + selectStateOrDispatch, + propsSelector, + (...args) => mapProxy(...args) + ) + } else { + return (...args) => mapProxy( + selectStateOrDispatch(...args), + propsSelector(...args) ) } +} - function getStatePropsSelector(ownPropsSelector) { - if (!mapStateToProps) { - return () => empty - } - return createFactoryAwareSelector( - state => state, - ownPropsSelector, - mapStateToProps - ) +// normalizes the possible values of mapStateToProps into a selector +export function getStatePropsSelector(pure, ownPropsSelector, mapStateToProps) { + if (!mapStateToProps) { + const empty = {} + return () => empty } - function getDispatchPropsSelector(ownPropsSelector) { - if (!mapDispatchToProps) { - return (_, __, dispatch) => ({ dispatch }) - } + return createFactoryAwareSelector( + pure, + ownPropsSelector, + state => state, + mapStateToProps + ) +} - if (typeof mapDispatchToProps !== 'function') { - return createSelector( - (_, __, dispatch) => dispatch, - dispatch => bindActionCreators(mapDispatchToProps, dispatch) - ) - } - return createFactoryAwareSelector( - (_, __, dispatch) => dispatch, - ownPropsSelector, - mapDispatchToProps - ) +// normalizes the possible values of mapDispatchToProps into a selector +export function getDispatchPropsSelector(pure, ownPropsSelector, mapDispatchToProps, dispatch) { + if (!mapDispatchToProps) { + const dispatchProps = { dispatch } + return () => dispatchProps } - function selectorFactory({ displayName }) { - const ownPropsSelector = createSelectorCreator(defaultMemoize, shallowEqual)( - (_, props) => props, - props => props - ) + if (typeof mapDispatchToProps !== 'function') { + const bound = bindActionCreators(mapDispatchToProps, dispatch) + return () => bound + } + + return createFactoryAwareSelector( + pure, + ownPropsSelector, + () => dispatch, + mapDispatchToProps + ) +} + + +// merges the 3 props selectors into a final selector. +const defaultMergeProps = (state, dispatch, own) => ({ ...own, ...state, ...dispatch }) +export function getMergedPropsSelector( + displayName, + statePropsSelector, + dispatchPropsSelector, + ownPropsSelector, + mergeProps +) { + return createShallowSelector( + verifyPlainObject(displayName, 'mapStateToProps', statePropsSelector), + verifyPlainObject(displayName, 'mapDispatchToProps', dispatchPropsSelector), + ownPropsSelector, + mergeProps + ? verifyPlainObject(displayName, 'mergeProps', mergeProps) + : defaultMergeProps + ) +} - return createSelectorCreator(defaultMemoize, shallowEqual)( - verify(displayName, 'mapStateToProps', getStatePropsSelector(ownPropsSelector)), - verify(displayName, 'mapDispatchToProps', getDispatchPropsSelector(ownPropsSelector)), +// create a connectAdvanced-compatible selectorFactory function that applies the results of +// mapStateToProps, mapDispatchToProps, and mergeProps +export function makeSelectorFactory(mapStateToProps, mapDispatchToProps, mergeProps) { + return function selectorFactory({ dispatch, displayName, pure }) { + const ownPropsSelector = getOwnPropsSelector(pure) + + return getMergedPropsSelector( + displayName, + getStatePropsSelector(pure, ownPropsSelector, mapStateToProps), + getDispatchPropsSelector(pure, ownPropsSelector, mapDispatchToProps, dispatch), ownPropsSelector, mergeProps - ? verify(displayName, 'mergeProps', mergeProps) - : defaultMergeProps ) } +} +export default function connect( + mapStateToProps, + mapDispatchToProps, + mergeProps, + options +) { return connectAdvanced( - selectorFactory, + makeSelectorFactory(mapStateToProps, mapDispatchToProps, mergeProps), { - pure, getDisplayName: name => `Connect(${name})`, - shouldIncludeRecomputationsProp: false, ...options, methodName: 'connect', shouldUseState: Boolean(mapStateToProps) diff --git a/src/utils/verifyPlainObject.js b/src/utils/verifyPlainObject.js new file mode 100644 index 000000000..a7e71b9a2 --- /dev/null +++ b/src/utils/verifyPlainObject.js @@ -0,0 +1,20 @@ +import isPlainObject from 'lodash/isPlainObject' +import warning from './warning' + +// verifies that the first execution of func returns a plain object +export default function verifyPlainObject(displayName, methodName, func) { + if (process.env.NODE_ENV === 'production') return func + let hasVerified = false + return (...args) => { + const result = func(...args) + if (hasVerified) return result + hasVerified = true + if (!isPlainObject(result)) { + warning( + `${methodName}() in ${displayName} must return a plain object. ` + + `Instead received ${result}.` + ) + } + return result + } +} From 88e768b0bddb6fca7d6f04a123c0ea6c9a025379 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 18 Jun 2016 02:46:15 -0400 Subject: [PATCH 27/76] Change the way nested connected components subscribe so that parent components always subscribe before child components, so that parents always update before children. --- src/components/connectAdvanced.js | 52 ++++++++++++++++++++++--------- test/components/connect.spec.js | 6 ++-- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index dd843cd11..e3a0adbd8 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -1,6 +1,6 @@ import hoistStatics from 'hoist-non-react-statics' import invariant from 'invariant' -import { Component, createElement } from 'react' +import { Component, PropTypes, createElement } from 'react' import defaultBuildSelector from '../utils/buildSelector' import storeShape from '../utils/storeShape' @@ -54,6 +54,7 @@ export default function connectAdvanced( withRef = false } = {} ) { + const subscribeKey = storeKey + 'Subscribe' const version = hotReloadingVersion++ return function wrapWithConnect(WrappedComponent) { class Connect extends Component { @@ -73,6 +74,12 @@ export default function connectAdvanced( this.initSelector() } + getChildContext() { + return { + [subscribeKey]: listener => { this.trySubscribe(listener) } + } + } + componentDidMount() { this.trySubscribe() @@ -86,8 +93,7 @@ export default function connectAdvanced( } componentWillUnmount() { - if (this.unsubscribe) this.unsubscribe() - this.unsubscribe = null + this.tryUnsubscribe() this.store = null this.selector = null } @@ -114,17 +120,26 @@ export default function connectAdvanced( return this.recomputationsDuringLastRender !== this.selector(props).recomputations } - trySubscribe() { - if (!shouldUseState) return - if (this.unsubscribe) this.unsubscribe() + trySubscribe(childListener) { + const subscribe = this.context[subscribeKey] || this.store.subscribe - this.unsubscribe = this.store.subscribe(() => { - if (this.unsubscribe) { - // invoke setState() instead of forceUpdate() so that shouldComponentUpdate() - // gets a chance to prevent unneeded re-renders - this.setState({}) - } - }) + if (shouldUseState && !this.isSubscribed()) { + + this.unsubscribe = subscribe(() => { + if (this.unsubscribe && this.shouldComponentUpdate(this.props)) { + // invoke setState() instead of forceUpdate() so that shouldComponentUpdate() + // gets a chance to prevent unneeded re-renders + this.setState({}) + } + }) + } + + if (childListener) subscribe(childListener) + } + + tryUnsubscribe() { + if (this.unsubscribe) this.unsubscribe() + this.unsubscribe = null } getWrappedInstance() { @@ -152,15 +167,24 @@ export default function connectAdvanced( Connect.displayName = getDisplayName(wrappedComponentName) Connect.WrappedComponent = WrappedComponent - Connect.contextTypes = { [storeKey]: storeShape } Connect.propTypes = { [storeKey]: storeShape } + Connect.contextTypes = { + [storeKey]: storeShape, + [subscribeKey]: PropTypes.func + } + Connect.childContextTypes = { + [subscribeKey]: PropTypes.func + } + + if (process.env.NODE_ENV !== 'production') { Connect.prototype.componentWillUpdate = function componentWillUpdate() { // We are hot reloading! if (this.version !== version) { this.version = version this.initSelector() + this.tryUnsubscribe() this.trySubscribe() } } diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index bbd831502..5e494a951 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1611,17 +1611,17 @@ describe('React', () => { store.dispatch({ type: 'APPEND', body: 'a' }) expect(mapStateCalls).toBe(2) expect(renderCalls).toBe(1) - expect(spy.calls.length).toBe(1) + expect(spy.calls.length).toBe(0) store.dispatch({ type: 'APPEND', body: 'a' }) expect(mapStateCalls).toBe(3) expect(renderCalls).toBe(1) - expect(spy.calls.length).toBe(2) + expect(spy.calls.length).toBe(0) store.dispatch({ type: 'APPEND', body: 'a' }) expect(mapStateCalls).toBe(4) expect(renderCalls).toBe(2) - expect(spy.calls.length).toBe(3) + expect(spy.calls.length).toBe(1) spy.destroy() }) From d2b98445a53b1e06ff1874ca4210f8a2cebdf7ec Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 18 Jun 2016 08:48:40 -0400 Subject: [PATCH 28/76] Fix failing tests. Change the way nested components subscribe... instead of subscribing to the store directly they subscribe via their ancestor connected component. This ensures the ancestor gets to update before its children. --- src/components/connectAdvanced.js | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index e3a0adbd8..cb064f182 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -63,6 +63,7 @@ export default function connectAdvanced( this.version = version this.state = {} this.store = this.props[storeKey] || this.context[storeKey] + this.childSubs = {} invariant(this.store, `Could not find "${storeKey}" in either the context or ` + @@ -76,7 +77,7 @@ export default function connectAdvanced( getChildContext() { return { - [subscribeKey]: listener => { this.trySubscribe(listener) } + [subscribeKey]: listener => this.trySubscribe(listener) } } @@ -123,23 +124,41 @@ export default function connectAdvanced( trySubscribe(childListener) { const subscribe = this.context[subscribeKey] || this.store.subscribe - if (shouldUseState && !this.isSubscribed()) { - + if ((shouldUseState || childListener) && !this.isSubscribed()) { this.unsubscribe = subscribe(() => { if (this.unsubscribe && this.shouldComponentUpdate(this.props)) { // invoke setState() instead of forceUpdate() so that shouldComponentUpdate() // gets a chance to prevent unneeded re-renders - this.setState({}) + this.setState({}, () => { + const keys = Object.keys(this.childSubs) + for (let i = 0; i < keys.length; i++) { + this.childSubs[keys[i]].listener() + } + }) } }) } - if (childListener) subscribe(childListener) + if (childListener) { + //const unsubscribe = subscribe(childListener) + const id = this.lastChildSubId++ + this.childSubs[id] = { listener: childListener } + + return () => { + if (this.childSubs[id]) { + //this.childSubs[id].unsubscribe() + delete this.childSubs[id] + } + } + } + + return undefined } tryUnsubscribe() { if (this.unsubscribe) this.unsubscribe() this.unsubscribe = null + this.childSubs = {} } getWrappedInstance() { From 859d7954cca28a2cb1a78fd4e232a03a2523d151 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 18 Jun 2016 10:42:48 -0400 Subject: [PATCH 29/76] ensure nested subs are notified properly --- src/components/connectAdvanced.js | 68 +++++++++++++++---------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index cb064f182..ad58482b9 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -63,7 +63,7 @@ export default function connectAdvanced( this.version = version this.state = {} this.store = this.props[storeKey] || this.context[storeKey] - this.childSubs = {} + this.nestedSubs = {} invariant(this.store, `Could not find "${storeKey}" in either the context or ` + @@ -77,7 +77,7 @@ export default function connectAdvanced( getChildContext() { return { - [subscribeKey]: listener => this.trySubscribe(listener) + [subscribeKey]: listener => this.subscribeNestedListener(listener) } } @@ -96,7 +96,7 @@ export default function connectAdvanced( componentWillUnmount() { this.tryUnsubscribe() this.store = null - this.selector = null + this.selector = () => this.last } initSelector() { @@ -118,47 +118,48 @@ export default function connectAdvanced( } hasUnrenderedRecomputations(props) { - return this.recomputationsDuringLastRender !== this.selector(props).recomputations + return this.last.recomputations !== this.selector(props).recomputations } - trySubscribe(childListener) { + trySubscribe(force) { + if (!shouldUseState && !force) return + if (this.isSubscribed()) return + const subscribe = this.context[subscribeKey] || this.store.subscribe + this.unsubscribe = subscribe(() => { + if (!this.unsubscribe) return + if (this.shouldComponentUpdate(this.props)) { + this.setState({}, () => this.notifyNestedSubs()) + } + else { + this.notifyNestedSubs() + } + }) + } - if ((shouldUseState || childListener) && !this.isSubscribed()) { - this.unsubscribe = subscribe(() => { - if (this.unsubscribe && this.shouldComponentUpdate(this.props)) { - // invoke setState() instead of forceUpdate() so that shouldComponentUpdate() - // gets a chance to prevent unneeded re-renders - this.setState({}, () => { - const keys = Object.keys(this.childSubs) - for (let i = 0; i < keys.length; i++) { - this.childSubs[keys[i]].listener() - } - }) - } - }) + notifyNestedSubs() { + const keys = Object.keys(this.nestedSubs) + for (let i = 0; i < keys.length; i++) { + this.nestedSubs[keys[i]].listener() } + } - if (childListener) { - //const unsubscribe = subscribe(childListener) - const id = this.lastChildSubId++ - this.childSubs[id] = { listener: childListener } + subscribeNestedListener(listener) { + this.trySubscribe(true) - return () => { - if (this.childSubs[id]) { - //this.childSubs[id].unsubscribe() - delete this.childSubs[id] - } + const id = this.lastNestedSubId++ + this.nestedSubs[id] = { listener: listener } + return () => { + if (this.nestedSubs[id]) { + delete this.nestedSubs[id] } } - - return undefined } tryUnsubscribe() { if (this.unsubscribe) this.unsubscribe() this.unsubscribe = null - this.childSubs = {} + this.nestedSubs = {} } getWrappedInstance() { @@ -174,9 +175,9 @@ export default function connectAdvanced( } render() { - const { props, recomputations } = this.selector(this.props) - this.recomputationsDuringLastRender = recomputations - return createElement(WrappedComponent, props) + const results = this.selector(this.props) + this.last = results + return createElement(WrappedComponent, results.props) } } @@ -196,7 +197,6 @@ export default function connectAdvanced( [subscribeKey]: PropTypes.func } - if (process.env.NODE_ENV !== 'production') { Connect.prototype.componentWillUpdate = function componentWillUpdate() { // We are hot reloading! From ce499332978af63dec63d81e743171bd54a7a76c Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 18 Jun 2016 11:27:47 -0400 Subject: [PATCH 30/76] Eliminate extraneous tracking of recomputations count since we can just compare last rendered props to new selected props --- src/components/connectAdvanced.js | 19 ++++++++----------- src/utils/buildSelector.js | 2 +- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index ad58482b9..f08aa999e 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -86,21 +86,21 @@ export default function connectAdvanced( // check for recomputations that happened after this component has rendered, such as // when a child component dispatches an action in its componentWillMount - if (this.hasUnrenderedRecomputations(this.props)) this.forceUpdate() + if (this.lastRenderedProps !== this.selector(this.props)) this.forceUpdate() } shouldComponentUpdate(nextProps) { - return !pure || this.hasUnrenderedRecomputations(nextProps) + return !pure || this.lastRenderedProps !== this.selector(nextProps) } componentWillUnmount() { this.tryUnsubscribe() this.store = null - this.selector = () => this.last + this.selector = () => this.lastRenderedProps } initSelector() { - this.recomputationsDuringLastRender = null + this.lastRenderedProps = null this.selector = buildSelector({ displayName: Connect.displayName, @@ -117,10 +117,6 @@ export default function connectAdvanced( }) } - hasUnrenderedRecomputations(props) { - return this.last.recomputations !== this.selector(props).recomputations - } - trySubscribe(force) { if (!shouldUseState && !force) return if (this.isSubscribed()) return @@ -175,9 +171,10 @@ export default function connectAdvanced( } render() { - const results = this.selector(this.props) - this.last = results - return createElement(WrappedComponent, results.props) + return createElement( + WrappedComponent, + this.lastRenderedProps = this.selector(this.props) + ) } } diff --git a/src/utils/buildSelector.js b/src/utils/buildSelector.js index f1054d7d0..4b33215dd 100644 --- a/src/utils/buildSelector.js +++ b/src/utils/buildSelector.js @@ -46,6 +46,6 @@ export default function buildSelector({ const recomputations = masterSelector.recomputations() if (mightMutateProps) mutateProps(props, before, recomputations) - return { props, recomputations } + return props } } From c72bcc066ad98990bbf2dda7d00e535ec0bb8585 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 18 Jun 2016 11:58:19 -0400 Subject: [PATCH 31/76] simplify nested subs code. no need for extra object wrappers --- src/components/connectAdvanced.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index f08aa999e..b6334db77 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -72,13 +72,12 @@ export default function connectAdvanced( `or explicitly pass "${storeKey}" as a prop to "${Connect.displayName}".` ) + this.subscribeNestedListener = this.subscribeNestedListener.bind(this) this.initSelector() } getChildContext() { - return { - [subscribeKey]: listener => this.subscribeNestedListener(listener) - } + return { [subscribeKey]: this.subscribeNestedListener } } componentDidMount() { @@ -136,7 +135,7 @@ export default function connectAdvanced( notifyNestedSubs() { const keys = Object.keys(this.nestedSubs) for (let i = 0; i < keys.length; i++) { - this.nestedSubs[keys[i]].listener() + this.nestedSubs[keys[i]]() } } @@ -144,7 +143,7 @@ export default function connectAdvanced( this.trySubscribe(true) const id = this.lastNestedSubId++ - this.nestedSubs[id] = { listener: listener } + this.nestedSubs[id] = listener return () => { if (this.nestedSubs[id]) { delete this.nestedSubs[id] From 9fef522d5702d0398d68ce3da75535c71ce86979 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 18 Jun 2016 18:54:58 -0400 Subject: [PATCH 32/76] rename shouldUseState to dependsOnState to make its meaning more clear --- src/components/connect.js | 2 +- src/components/connectAdvanced.js | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/connect.js b/src/components/connect.js index 361ec9744..f14eca4d9 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -146,7 +146,7 @@ export default function connect( getDisplayName: name => `Connect(${name})`, ...options, methodName: 'connect', - shouldUseState: Boolean(mapStateToProps) + dependsOnState: Boolean(mapStateToProps) } ) } diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index b6334db77..e1a931f1f 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -25,6 +25,11 @@ export default function connectAdvanced( // an injection point for custom behavior, for example hooking in some custom devtools. buildSelector = defaultBuildSelector, + // if true, the selector receieves the current store state as the first arg, and this HOC + // subscribes to store changes during componentDidMount. if false, null is passed as the first + // arg of selector and store.subscribe() is never called. + dependsOnState = true, + // the func used to compute this HOC's displayName from the wrapped component's displayName. // probably overridden by wrapper functions such as connect() getDisplayName = name => `ConnectAdvanced(${name})`, @@ -42,11 +47,6 @@ export default function connectAdvanced( // re-renders. recomputationsProp = undefined, - // if true, the selector receieves the current store state as the first arg, and this HOC - // subscribes to store changes during componentDidMount. if false, null is passed as the first - // arg of selector and store.subscribe() is never called. - shouldUseState = true, - // the key of props/context to get the store storeKey = 'store', @@ -104,20 +104,20 @@ export default function connectAdvanced( this.selector = buildSelector({ displayName: Connect.displayName, dispatch: this.store.dispatch, - getState: shouldUseState ? this.store.getState : (() => null), + getState: dependsOnState ? this.store.getState : (() => null), ref: withRef ? (ref => { this.wrappedInstance = ref }) : undefined, selectorFactory, recomputationsProp, methodName, pure, - shouldUseState, + dependsOnState, storeKey, withRef }) } trySubscribe(force) { - if (!shouldUseState && !force) return + if (!dependsOnState && !force) return if (this.isSubscribed()) return const subscribe = this.context[subscribeKey] || this.store.subscribe From ed52f05e71a1c8ab17fa0aa1aa48ee0ecf27bb20 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 18 Jun 2016 19:41:26 -0400 Subject: [PATCH 33/76] prebind notifyNestedSubs to avoid extra lambda func --- src/components/connectAdvanced.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index e1a931f1f..30d2420e4 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -73,6 +73,7 @@ export default function connectAdvanced( ) this.subscribeNestedListener = this.subscribeNestedListener.bind(this) + this.notifyNestedSubs = this.notifyNestedSubs.bind(this) this.initSelector() } @@ -124,7 +125,7 @@ export default function connectAdvanced( this.unsubscribe = subscribe(() => { if (!this.unsubscribe) return if (this.shouldComponentUpdate(this.props)) { - this.setState({}, () => this.notifyNestedSubs()) + this.setState({}, this.notifyNestedSubs) } else { this.notifyNestedSubs() From 39fbe00e6953289dc6914007b29a003cd30381c4 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 18 Jun 2016 19:44:09 -0400 Subject: [PATCH 34/76] Add WrappedComponent as one of the option params passed to buildSelector(). Useful if one wanted to build a selector from the component's attributes, such as its propTypes, for example. --- src/components/connectAdvanced.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 30d2420e4..45f44d749 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -107,6 +107,7 @@ export default function connectAdvanced( dispatch: this.store.dispatch, getState: dependsOnState ? this.store.getState : (() => null), ref: withRef ? (ref => { this.wrappedInstance = ref }) : undefined, + WrappedComponent, selectorFactory, recomputationsProp, methodName, From 1b8fb69bca8ffaced11cf7857c9f891db50b1084 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sun, 19 Jun 2016 10:49:48 -0400 Subject: [PATCH 35/76] Extract Subscription class from connectAdvanced --- src/components/connectAdvanced.js | 110 +++++++++++++----------------- src/utils/Subscription.js | 56 +++++++++++++++ test/components/connect.spec.js | 4 +- 3 files changed, 106 insertions(+), 64 deletions(-) create mode 100644 src/utils/Subscription.js diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 45f44d749..6528e5f2c 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -2,6 +2,7 @@ import hoistStatics from 'hoist-non-react-statics' import invariant from 'invariant' import { Component, PropTypes, createElement } from 'react' +import Subscription from '../utils/Subscription' import defaultBuildSelector from '../utils/buildSelector' import storeShape from '../utils/storeShape' @@ -54,16 +55,18 @@ export default function connectAdvanced( withRef = false } = {} ) { - const subscribeKey = storeKey + 'Subscribe' + const subscriptionKey = storeKey + 'Subscription' const version = hotReloadingVersion++ + return function wrapWithConnect(WrappedComponent) { class Connect extends Component { constructor(props, context) { super(props, context) + this.version = version this.state = {} this.store = this.props[storeKey] || this.context[storeKey] - this.nestedSubs = {} + this.parentSub = this.props[subscriptionKey] || this.context[subscriptionKey] invariant(this.store, `Could not find "${storeKey}" in either the context or ` + @@ -72,21 +75,24 @@ export default function connectAdvanced( `or explicitly pass "${storeKey}" as a prop to "${Connect.displayName}".` ) - this.subscribeNestedListener = this.subscribeNestedListener.bind(this) - this.notifyNestedSubs = this.notifyNestedSubs.bind(this) this.initSelector() + this.initSubscription() } getChildContext() { - return { [subscribeKey]: this.subscribeNestedListener } + return { [subscriptionKey]: this.subscription } } componentDidMount() { - this.trySubscribe() + if (!dependsOnState) return + + this.subscription.trySubscribe() // check for recomputations that happened after this component has rendered, such as // when a child component dispatches an action in its componentWillMount - if (this.lastRenderedProps !== this.selector(this.props)) this.forceUpdate() + if (this.lastRenderedProps !== this.selector(this.props)) { + this.forceUpdate() + } } shouldComponentUpdate(nextProps) { @@ -94,11 +100,23 @@ export default function connectAdvanced( } componentWillUnmount() { - this.tryUnsubscribe() + this.subscription.tryUnsubscribe() + // these are just to guard against extra memory leakage if a parent element doesn't + // dereference this instance properly, such as an async callback that never finishes + this.subscription = { isSubscribed: () => false } this.store = null + this.parentSub = null this.selector = () => this.lastRenderedProps } + getWrappedInstance() { + invariant(withRef, + `To access the wrapped instance, you need to specify ` + + `{ withRef: true } in the options argument of the ${methodName}() call.` + ) + return this.wrappedInstance + } + initSelector() { this.lastRenderedProps = null @@ -118,57 +136,20 @@ export default function connectAdvanced( }) } - trySubscribe(force) { - if (!dependsOnState && !force) return - if (this.isSubscribed()) return - - const subscribe = this.context[subscribeKey] || this.store.subscribe - this.unsubscribe = subscribe(() => { - if (!this.unsubscribe) return - if (this.shouldComponentUpdate(this.props)) { - this.setState({}, this.notifyNestedSubs) - } - else { - this.notifyNestedSubs() - } - }) - } - - notifyNestedSubs() { - const keys = Object.keys(this.nestedSubs) - for (let i = 0; i < keys.length; i++) { - this.nestedSubs[keys[i]]() - } - } - - subscribeNestedListener(listener) { - this.trySubscribe(true) - - const id = this.lastNestedSubId++ - this.nestedSubs[id] = listener - return () => { - if (this.nestedSubs[id]) { - delete this.nestedSubs[id] - } - } - } - - tryUnsubscribe() { - if (this.unsubscribe) this.unsubscribe() - this.unsubscribe = null - this.nestedSubs = {} - } - - getWrappedInstance() { - invariant(withRef, - `To access the wrapped instance, you need to specify ` + - `{ withRef: true } in the options argument of the ${methodName}() call.` + initSubscription() { + this.subscription = new Subscription( + this.store, + this.parentSub, + this.onStateChange.bind(this) ) - return this.wrappedInstance } - isSubscribed() { - return typeof this.unsubscribe === 'function' + onStateChange(callback) { + if (dependsOnState && this.shouldComponentUpdate(this.props)) { + this.setState({}, callback) + } else { + callback() + } } render() { @@ -185,14 +166,17 @@ export default function connectAdvanced( Connect.displayName = getDisplayName(wrappedComponentName) Connect.WrappedComponent = WrappedComponent - Connect.propTypes = { [storeKey]: storeShape } - + + Connect.propTypes = { + [storeKey]: storeShape, + [subscriptionKey]: PropTypes.instanceOf(Subscription) + } Connect.contextTypes = { [storeKey]: storeShape, - [subscribeKey]: PropTypes.func + [subscriptionKey]: PropTypes.instanceOf(Subscription) } Connect.childContextTypes = { - [subscribeKey]: PropTypes.func + [subscriptionKey]: PropTypes.instanceOf(Subscription).isRequired } if (process.env.NODE_ENV !== 'production') { @@ -201,8 +185,10 @@ export default function connectAdvanced( if (this.version !== version) { this.version = version this.initSelector() - this.tryUnsubscribe() - this.trySubscribe() + + this.subscription.tryUnsubscribe() + this.initSubscription() + if (dependsOnState) this.subscription.trySubscribe() } } } diff --git a/src/utils/Subscription.js b/src/utils/Subscription.js new file mode 100644 index 000000000..399da51bc --- /dev/null +++ b/src/utils/Subscription.js @@ -0,0 +1,56 @@ + +// enapsulates the subscription logic for connecting a component to the redux store, as well as +// nesting subscriptions of decendant components, so that we can ensure the ancestor components +// re-render before descendants +export default class Subscription { + constructor(store, parentSub, onStateChange) { + this.subscribe = parentSub + ? parentSub.addNestedSub.bind(parentSub) + : store.subscribe + + this.onStateChange = onStateChange + this.lastNestedSubId = 0 + this.unsubscribe = null + this.nestedSubs = {} + + this.notifyNestedSubs = this.notifyNestedSubs.bind(this) + } + + addNestedSub(listener) { + this.trySubscribe() + + const id = this.lastNestedSubId++ + this.nestedSubs[id] = listener + return () => { + if (this.nestedSubs[id]) { + delete this.nestedSubs[id] + } + } + } + + isSubscribed() { + return Boolean(this.unsubscribe) + } + + notifyNestedSubs() { + const keys = Object.keys(this.nestedSubs) + for (let i = 0; i < keys.length; i++) { + this.nestedSubs[keys[i]]() + } + } + + trySubscribe() { + if (this.unsubscribe) return + + this.unsubscribe = this.subscribe(() => { + this.onStateChange(this.notifyNestedSubs) + }) + } + + tryUnsubscribe() { + if (this.unsubscribe) { + this.unsubscribe() + } + this.unsubscribe = null + } +} diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 5e494a951..ea517d506 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -456,7 +456,7 @@ describe('React', () => { TestUtils.findRenderedComponentWithType(container, Container) ).toNotThrow() const decorated = TestUtils.findRenderedComponentWithType(container, Container) - expect(decorated.isSubscribed()).toBe(true) + expect(decorated.subscription.isSubscribed()).toBe(true) }) it('should not invoke mapState when props change if it only has one argument', () => { @@ -781,7 +781,7 @@ describe('React', () => { TestUtils.findRenderedComponentWithType(container, Container) ).toNotThrow() const decorated = TestUtils.findRenderedComponentWithType(container, Container) - expect(decorated.isSubscribed()).toBe(false) + expect(decorated.subscription.isSubscribed()).toBe(false) } runCheck() From 5f0f85fbcf6f49a46c1a96d43918d3a1e3535a93 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sun, 19 Jun 2016 11:27:32 -0400 Subject: [PATCH 36/76] Replace reselect with manual memoization in buildSelector. Faster! --- src/utils/buildSelector.js | 55 +++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/src/utils/buildSelector.js b/src/utils/buildSelector.js index 4b33215dd..ed500a557 100644 --- a/src/utils/buildSelector.js +++ b/src/utils/buildSelector.js @@ -1,5 +1,3 @@ -import { createSelectorCreator, defaultMemoize } from 'reselect' - import shallowEqual from '../utils/shallowEqual' export default function buildSelector({ @@ -13,39 +11,42 @@ export default function buildSelector({ // the final props obect is mutated directly instead of projecting into a new object to avoid // some extra object creation and tracking. const mightMutateProps = ref || recomputationsProp - function mutateProps(props, before, recomputations) { - if (before === recomputations) return - if (ref) props.ref = ref - if (recomputationsProp) props[recomputationsProp] = recomputations - } - - // wrap the source selector in a shallow equals because props objects with same properties are - // semantically equal to React... no need to re-render. - const masterSelector = createSelectorCreator(defaultMemoize, shallowEqual)( - selectorFactory({ - // useful for selecto factories that want to bind action creators before returning - // their selector - dispatch, - // additional options passed to buildSelector are passed along to the selectorFactory - ...options - }), + function mutateProps(newProps, recomputations) { + if (!mightMutateProps) return newProps // make a shallow copy so that fields added by mutateProps don't leak to the original selector. // this is especially important for 'ref' since that's a reference back to the component // instance. a singleton memoized selector would then be holding a reference to the instance, // preventing the instance from being garbage collected - mightMutateProps - ? (result => ({ ...result })) - : (result => result) - ) + const props = { ...newProps } + if (ref) props.ref = ref + if (recomputationsProp) props[recomputationsProp] = recomputations + return props + } + const selector = selectorFactory({ + // useful for selecto factories that want to bind action creators before returning + // their selector + dispatch, + // additional options passed to buildSelector are passed along to the selectorFactory + ...options + }) + + let recomputations = 0 + let selectedProps = undefined + let finalProps = undefined return function runSelector(ownProps) { - const before = masterSelector.recomputations() const state = getState() - const props = masterSelector(state, ownProps, dispatch) - const recomputations = masterSelector.recomputations() + const newProps = selector(state, ownProps, dispatch) - if (mightMutateProps) mutateProps(props, before, recomputations) - return props + // wrap the source selector in a shallow equals because props objects with same properties are + // semantically equal to React... no need to re-render. + if (!selectedProps || !shallowEqual(selectedProps, newProps)) { + recomputations++ + selectedProps = newProps + finalProps = mutateProps(newProps, recomputations) + } + + return finalProps } } From 03c62ef1c187a9a8993679a61a24dbaf17875997 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sun, 19 Jun 2016 11:43:57 -0400 Subject: [PATCH 37/76] refactor buildSelector --- src/utils/buildSelector.js | 43 +++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/utils/buildSelector.js b/src/utils/buildSelector.js index ed500a557..854cd64ae 100644 --- a/src/utils/buildSelector.js +++ b/src/utils/buildSelector.js @@ -8,24 +8,8 @@ export default function buildSelector({ recomputationsProp, ...options }) { - // the final props obect is mutated directly instead of projecting into a new object to avoid - // some extra object creation and tracking. - const mightMutateProps = ref || recomputationsProp - function mutateProps(newProps, recomputations) { - if (!mightMutateProps) return newProps - - // make a shallow copy so that fields added by mutateProps don't leak to the original selector. - // this is especially important for 'ref' since that's a reference back to the component - // instance. a singleton memoized selector would then be holding a reference to the instance, - // preventing the instance from being garbage collected - const props = { ...newProps } - if (ref) props.ref = ref - if (recomputationsProp) props[recomputationsProp] = recomputations - return props - } - const selector = selectorFactory({ - // useful for selecto factories that want to bind action creators before returning + // useful for selector factories that want to bind action creators before returning // their selector dispatch, // additional options passed to buildSelector are passed along to the selectorFactory @@ -33,18 +17,33 @@ export default function buildSelector({ }) let recomputations = 0 - let selectedProps = undefined + let selectorProps = undefined let finalProps = undefined + + const mightAddProps = ref || recomputationsProp + function getFinalProps() { + if (!mightAddProps) return selectorProps + + // make a shallow copy so that fields added don't leak to the original selector. + // this is especially important for 'ref' since that's a reference back to the component + // instance. a singleton memoized selector would then be holding a reference to the instance, + // preventing the instance from being garbage collected, and that would be bad + const props = { ...selectorProps } + if (ref) props.ref = ref + if (recomputationsProp) props[recomputationsProp] = recomputations + return props + } + return function runSelector(ownProps) { const state = getState() const newProps = selector(state, ownProps, dispatch) // wrap the source selector in a shallow equals because props objects with same properties are - // semantically equal to React... no need to re-render. - if (!selectedProps || !shallowEqual(selectedProps, newProps)) { + // semantically equal to React... no need to return a new object. + if (!selectorProps || !shallowEqual(selectorProps, newProps)) { recomputations++ - selectedProps = newProps - finalProps = mutateProps(newProps, recomputations) + selectorProps = newProps + finalProps = getFinalProps() } return finalProps From 09069040b70a34eb26549981522a147a488b5152 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sun, 19 Jun 2016 21:48:41 -0400 Subject: [PATCH 38/76] refactor connect()... extract selectors, replace reselect with hand-rolled memoization --- package.json | 3 +- src/components/connect.js | 173 +++++++-------------- src/selectors/buildFactoryAwareSelector.js | 58 +++++++ src/selectors/dispatch.js | 44 ++++++ src/selectors/index.js | 3 + src/selectors/ownProps.js | 21 +++ src/selectors/state.js | 25 +++ src/utils/verifyPlainObject.js | 2 + 8 files changed, 211 insertions(+), 118 deletions(-) create mode 100644 src/selectors/buildFactoryAwareSelector.js create mode 100644 src/selectors/dispatch.js create mode 100644 src/selectors/index.js create mode 100644 src/selectors/ownProps.js create mode 100644 src/selectors/state.js diff --git a/package.json b/package.json index e0bac92c5..576522b83 100644 --- a/package.json +++ b/package.json @@ -92,8 +92,7 @@ "hoist-non-react-statics": "^1.0.3", "invariant": "^2.0.0", "lodash": "^4.2.0", - "loose-envify": "^1.1.0", - "reselect": "^2.5.1" + "loose-envify": "^1.1.0" }, "peerDependencies": { "react": "^0.14.0 || ^15.0.0-0", diff --git a/src/components/connect.js b/src/components/connect.js index f14eca4d9..c9ffff48b 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -1,136 +1,77 @@ -import { bindActionCreators } from 'redux' -import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect' - import connectAdvanced from './connectAdvanced' import shallowEqual from '../utils/shallowEqual' import verifyPlainObject from '../utils/verifyPlainObject' -const createShallowSelector = createSelectorCreator(defaultMemoize, shallowEqual) - -export function getOwnPropsSelector(pure) { - return pure - ? createShallowSelector((_, props) => props, props => props) - : ((_, props) => props) -} - - -// used by getStatePropsSelector and getDispatchPropsSelector to create a memoized selector function -// based on the given mapStateOrDispatchToProps function. It also detects if that function is a -// factory based on its first returned result. -// if not pure, then results should always be recomputed (except if it's ignoring prop changes) -export function createFactoryAwareSelector( - pure, - ownPropsSelector, - selectStateOrDispatch, - mapStateOrDispatchToProps -) { - // propsSelector. if the map function only takes 1 arg, it shouldn't recompute results for props - // changes. since this depends on map, which is mutable, propsSelector must be recomputed when - // map changes - const noProps = {} - const getPropsSelector = func => (func.length !== 1 ? ownPropsSelector : (() => noProps)) - let propsSelector = getPropsSelector(mapStateOrDispatchToProps) - - // factory detection. if the first result of mapSomethingToProps is a function, use that as the - // true mapSomethingToProps - let map = mapStateOrDispatchToProps - let mapProxy = (...args) => { - const result = map(...args) - if (typeof result === 'function') { - map = result - propsSelector = getPropsSelector(map) - mapProxy = map - return map(...args) - } else { - mapProxy = map - return result - } - } - - if (pure) { - return createSelector( - selectStateOrDispatch, - propsSelector, - (...args) => mapProxy(...args) - ) - } else { - return (...args) => mapProxy( - selectStateOrDispatch(...args), - propsSelector(...args) - ) +import { + buildDispatchPropsSelector, + buildOwnPropsSelector, + buildStatePropsSelector +} from '../selectors' + +export function defaultMergeProps(stateProps, dispatchProps, ownProps) { + return { + ...ownProps, + ...stateProps, + ...dispatchProps } } - -// normalizes the possible values of mapStateToProps into a selector -export function getStatePropsSelector(pure, ownPropsSelector, mapStateToProps) { - if (!mapStateToProps) { - const empty = {} - return () => empty +export function wrapWithVerify(displayName, { getState, getDispatch, getOwn, merge }) { + return { + getState: verifyPlainObject(displayName, 'mapStateToProps', getState), + getDispatch: verifyPlainObject(displayName, 'mapDispatchToProps', getDispatch), + getOwn, + merge: merge !== defaultMergeProps + ? verifyPlainObject(displayName, 'mergeProps', merge) + : merge } - - return createFactoryAwareSelector( - pure, - ownPropsSelector, - state => state, - mapStateToProps - ) } - -// normalizes the possible values of mapDispatchToProps into a selector -export function getDispatchPropsSelector(pure, ownPropsSelector, mapDispatchToProps, dispatch) { - if (!mapDispatchToProps) { - const dispatchProps = { dispatch } - return () => dispatchProps - } - - if (typeof mapDispatchToProps !== 'function') { - const bound = bindActionCreators(mapDispatchToProps, dispatch) - return () => bound +export function buildImpureSelector({ getState, getDispatch, getOwn, merge }) { + return function impureSelector(state, props, dispatch) { + return merge( + getState(state, props, dispatch), + getDispatch(state, props, dispatch), + getOwn(state, props, dispatch) + ) } - - return createFactoryAwareSelector( - pure, - ownPropsSelector, - () => dispatch, - mapDispatchToProps - ) } +export function buildPureSelector({ getState, getDispatch, getOwn, merge }) { + let lastOwn = undefined + let lastState = undefined + let lastDispatch = undefined + let lastResult = undefined + return function pureSelector(state, props, dispatch) { + const nextOwn = getOwn(state, props, dispatch) + const nextState = getState(state, props, dispatch) + const nextDispatch = getDispatch(state, props, dispatch) + + if (lastOwn !== nextOwn || lastState !== nextState || lastDispatch !== nextDispatch) { + lastOwn = nextOwn + lastState = nextState + lastDispatch = nextDispatch + + const nextResult = merge(nextState, nextDispatch, nextOwn) + if (!lastResult || !shallowEqual(lastResult, nextResult)) { + lastResult = nextResult + } + } -// merges the 3 props selectors into a final selector. -const defaultMergeProps = (state, dispatch, own) => ({ ...own, ...state, ...dispatch }) -export function getMergedPropsSelector( - displayName, - statePropsSelector, - dispatchPropsSelector, - ownPropsSelector, - mergeProps -) { - return createShallowSelector( - verifyPlainObject(displayName, 'mapStateToProps', statePropsSelector), - verifyPlainObject(displayName, 'mapDispatchToProps', dispatchPropsSelector), - ownPropsSelector, - mergeProps - ? verifyPlainObject(displayName, 'mergeProps', mergeProps) - : defaultMergeProps - ) + return lastResult + } } // create a connectAdvanced-compatible selectorFactory function that applies the results of // mapStateToProps, mapDispatchToProps, and mergeProps -export function makeSelectorFactory(mapStateToProps, mapDispatchToProps, mergeProps) { - return function selectorFactory({ dispatch, displayName, pure }) { - const ownPropsSelector = getOwnPropsSelector(pure) - - return getMergedPropsSelector( - displayName, - getStatePropsSelector(pure, ownPropsSelector, mapStateToProps), - getDispatchPropsSelector(pure, ownPropsSelector, mapDispatchToProps, dispatch), - ownPropsSelector, - mergeProps - ) +export function buildSelectorFactory(mapStateToProps, mapDispatchToProps, merge) { + return function selectorFactory({ displayName, pure }) { + const getOwn = buildOwnPropsSelector(pure) + const getState = buildStatePropsSelector(pure, getOwn, mapStateToProps) + const getDispatch = buildDispatchPropsSelector(pure, getOwn, mapDispatchToProps) + + const build = pure ? buildPureSelector : buildImpureSelector + return build(wrapWithVerify(displayName, { getState, getDispatch, getOwn, merge })) } } @@ -141,7 +82,7 @@ export default function connect( options ) { return connectAdvanced( - makeSelectorFactory(mapStateToProps, mapDispatchToProps, mergeProps), + buildSelectorFactory(mapStateToProps, mapDispatchToProps, mergeProps || defaultMergeProps), { getDisplayName: name => `Connect(${name})`, ...options, diff --git a/src/selectors/buildFactoryAwareSelector.js b/src/selectors/buildFactoryAwareSelector.js new file mode 100644 index 000000000..3b3289a9c --- /dev/null +++ b/src/selectors/buildFactoryAwareSelector.js @@ -0,0 +1,58 @@ +import shallowEqual from '../utils/shallowEqual' + +// used by getStatePropsSelector and getDispatchPropsSelector to create a memoized selector function +// based on the given mapStateOrDispatchToProps function. It also detects if that function is a +// factory based on its first returned result. +// if not pure, then results should always be recomputed (except if it's ignoring prop changes) +export default function buildFactoryAwareSelector( + pure, + ownPropsSelector, + selectStateOrDispatch, + mapStateOrDispatchToProps +) { + const noProps = {} + + // factory detection. if the first result of mapSomethingToProps is a function, use that as the + // true mapSomethingToProps + let map = mapStateOrDispatchToProps + let mapProxy = function initialMapProxy(...args) { + const result = map(...args) + if (typeof result === 'function') { + map = result + mapProxy = map + return map(...args) + } else { + mapProxy = map + return result + } + } + + if (!pure) { + return function impureFactoryAwareSelector(state, props, dispatch) { + return mapProxy( + selectStateOrDispatch(state, props, dispatch), + ownPropsSelector(state, props, dispatch) + ) + } + } + + let lastStateOrDispatch = undefined + let lastProps = undefined + let lastResult = undefined + + return function pureFactoryAwareSelector(state, props, dispatch) { + const nextStateOrDispatch = selectStateOrDispatch(state, props, dispatch) + const nextProps = map.length === 1 ? noProps : ownPropsSelector(state, props, dispatch) + + if (lastStateOrDispatch !== nextStateOrDispatch || lastProps !== nextProps) { + lastStateOrDispatch = nextStateOrDispatch + lastProps = nextProps + const nextResult = mapProxy(nextStateOrDispatch, nextProps) + + if (!lastResult || !shallowEqual(lastResult, nextResult)) { + lastResult = nextResult + } + } + return lastResult + } +} diff --git a/src/selectors/dispatch.js b/src/selectors/dispatch.js new file mode 100644 index 000000000..3f9d5d278 --- /dev/null +++ b/src/selectors/dispatch.js @@ -0,0 +1,44 @@ +import { bindActionCreators } from 'redux' + +import buildFactoryAwareSelector from './buildFactoryAwareSelector' + +export function buildMissingDispatchSelector() { + let dispatchProps = undefined + return function dispatchSelector(_, __, dispatch) { + if (!dispatchProps) { + dispatchProps = { dispatch } + } + return dispatchProps + } +} + +export function buildBoundActionCreatorsSelector(mapDispatchToProps) { + let bound = undefined + return function boundActionCreatorsSelector(_, __, dispatch) { + if (!bound) { + bound = bindActionCreators(mapDispatchToProps, dispatch) + } + return bound + } +} + +export function buildFactoryAwareDispatchSelector(pure, ownPropsSelector, mapDispatchToProps) { + return buildFactoryAwareSelector( + pure, + ownPropsSelector, + (_, __, dispatch) => dispatch, + mapDispatchToProps + ) +} + +export function buildDispatchPropsSelector(pure, ownPropsSelector, mapDispatchToProps) { + if (!mapDispatchToProps) { + return buildMissingDispatchSelector() + } + + if (typeof mapDispatchToProps !== 'function') { + return buildBoundActionCreatorsSelector(mapDispatchToProps) + } + + return buildFactoryAwareDispatchSelector(pure, ownPropsSelector, mapDispatchToProps) +} diff --git a/src/selectors/index.js b/src/selectors/index.js new file mode 100644 index 000000000..8f39723a6 --- /dev/null +++ b/src/selectors/index.js @@ -0,0 +1,3 @@ +export * from './dispatch' +export * from './ownProps' +export * from './state' diff --git a/src/selectors/ownProps.js b/src/selectors/ownProps.js new file mode 100644 index 000000000..2782fcdff --- /dev/null +++ b/src/selectors/ownProps.js @@ -0,0 +1,21 @@ +import shallowEqual from '../utils/shallowEqual' + +export function impureOwnPropsSelector(_, props) { + return props +} + +export function buildPureOwnPropsSelector() { + let lastProps = undefined + return function pureOwnPropsSelector(_, nextProps) { + if (!lastProps || !shallowEqual(lastProps, nextProps)) { + lastProps = nextProps + } + return lastProps + } +} + +export function buildOwnPropsSelector(pure) { + return pure + ? buildPureOwnPropsSelector() + : impureOwnPropsSelector +} diff --git a/src/selectors/state.js b/src/selectors/state.js new file mode 100644 index 000000000..8d15e8b43 --- /dev/null +++ b/src/selectors/state.js @@ -0,0 +1,25 @@ +import buildFactoryAwareSelector from './buildFactoryAwareSelector' + +export function buildMissingStateSelector() { + const empty = {} + return function missingStateSelector() { + return empty + } +} + +export function buildFactoryAwareStateSelector(pure, ownPropsSelector, mapStateToProps) { + return buildFactoryAwareSelector( + pure, + ownPropsSelector, + state => state, + mapStateToProps + ) +} + +export function buildStatePropsSelector(pure, ownPropsSelector, mapStateToProps) { + if (!mapStateToProps) { + return buildMissingStateSelector() + } + + return buildFactoryAwareStateSelector(pure, ownPropsSelector, mapStateToProps) +} diff --git a/src/utils/verifyPlainObject.js b/src/utils/verifyPlainObject.js index a7e71b9a2..a248d454e 100644 --- a/src/utils/verifyPlainObject.js +++ b/src/utils/verifyPlainObject.js @@ -4,6 +4,8 @@ import warning from './warning' // verifies that the first execution of func returns a plain object export default function verifyPlainObject(displayName, methodName, func) { if (process.env.NODE_ENV === 'production') return func + if (!func) throw new Error('Missing ' + methodName) + let hasVerified = false return (...args) => { const result = func(...args) From d4686d8a64ad638bc50a6c05c24fb51b24c86c8a Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sun, 19 Jun 2016 22:18:24 -0400 Subject: [PATCH 39/76] Re-add isSubscribed() to reduce test delta --- src/components/connectAdvanced.js | 4 ++++ test/components/connect.spec.js | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 6528e5f2c..d9cf27eb0 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -144,6 +144,10 @@ export default function connectAdvanced( ) } + isSubscribed() { + return this.subscription.isSubscribed() + } + onStateChange(callback) { if (dependsOnState && this.shouldComponentUpdate(this.props)) { this.setState({}, callback) diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index ea517d506..5e494a951 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -456,7 +456,7 @@ describe('React', () => { TestUtils.findRenderedComponentWithType(container, Container) ).toNotThrow() const decorated = TestUtils.findRenderedComponentWithType(container, Container) - expect(decorated.subscription.isSubscribed()).toBe(true) + expect(decorated.isSubscribed()).toBe(true) }) it('should not invoke mapState when props change if it only has one argument', () => { @@ -781,7 +781,7 @@ describe('React', () => { TestUtils.findRenderedComponentWithType(container, Container) ).toNotThrow() const decorated = TestUtils.findRenderedComponentWithType(container, Container) - expect(decorated.subscription.isSubscribed()).toBe(false) + expect(decorated.isSubscribed()).toBe(false) } runCheck() From a45ae2312c403779ebce9c513aab0b6937769578 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sun, 19 Jun 2016 23:02:29 -0400 Subject: [PATCH 40/76] refactor buildFactoryAwareSelector and remove extraneous selector index file --- src/components/connect.js | 9 +-- src/selectors/buildFactoryAwareSelector.js | 75 ++++++++++++---------- src/selectors/index.js | 3 - 3 files changed, 44 insertions(+), 43 deletions(-) delete mode 100644 src/selectors/index.js diff --git a/src/components/connect.js b/src/components/connect.js index c9ffff48b..70a432878 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -1,13 +1,10 @@ import connectAdvanced from './connectAdvanced' +import { buildDispatchPropsSelector } from '../selectors/dispatch' +import { buildOwnPropsSelector } from '../selectors/ownProps' +import { buildStatePropsSelector } from '../selectors/state' import shallowEqual from '../utils/shallowEqual' import verifyPlainObject from '../utils/verifyPlainObject' -import { - buildDispatchPropsSelector, - buildOwnPropsSelector, - buildStatePropsSelector -} from '../selectors' - export function defaultMergeProps(stateProps, dispatchProps, ownProps) { return { ...ownProps, diff --git a/src/selectors/buildFactoryAwareSelector.js b/src/selectors/buildFactoryAwareSelector.js index 3b3289a9c..60425042d 100644 --- a/src/selectors/buildFactoryAwareSelector.js +++ b/src/selectors/buildFactoryAwareSelector.js @@ -1,53 +1,55 @@ import shallowEqual from '../utils/shallowEqual' -// used by getStatePropsSelector and getDispatchPropsSelector to create a memoized selector function -// based on the given mapStateOrDispatchToProps function. It also detects if that function is a -// factory based on its first returned result. -// if not pure, then results should always be recomputed (except if it's ignoring prop changes) -export default function buildFactoryAwareSelector( - pure, - ownPropsSelector, - selectStateOrDispatch, - mapStateOrDispatchToProps -) { - const noProps = {} - - // factory detection. if the first result of mapSomethingToProps is a function, use that as the - // true mapSomethingToProps - let map = mapStateOrDispatchToProps - let mapProxy = function initialMapProxy(...args) { - const result = map(...args) +// factory detection. if the first result of mapToProps is a function, use that as the +// true mapToProps +export default function buildMapOrMapFactoryProxy(mapToProps) { + let map = undefined + function firstRun(storePart, props) { + const result = mapToProps(storePart, props) if (typeof result === 'function') { map = result - mapProxy = map - return map(...args) + return map(storePart, props) } else { - mapProxy = map + map = mapToProps return result } } - - if (!pure) { - return function impureFactoryAwareSelector(state, props, dispatch) { - return mapProxy( - selectStateOrDispatch(state, props, dispatch), - ownPropsSelector(state, props, dispatch) - ) - } + + function proxy(storePart, props) { + return (map || firstRun)(storePart, props) } + proxy.dependsOnProps = function dependsOnProps() { + return (map || mapToProps).length !== 1 + } + return proxy +} + +export function buildImpureFactoryAwareSelector(getOwnProps, getStorePart, mapToProps) { + const map = buildMapOrMapFactoryProxy(mapToProps) - let lastStateOrDispatch = undefined + return function impureFactoryAwareSelector(state, props, dispatch) { + return map( + getStorePart(state, props, dispatch), + getOwnProps(state, props, dispatch) + ) + } +} + +export function buildPureFactoryAwareSelector(getOwnProps, getStorePart, mapToProps) { + const map = buildMapOrMapFactoryProxy(mapToProps) + const noProps = {} + let lastStorePart = undefined let lastProps = undefined let lastResult = undefined return function pureFactoryAwareSelector(state, props, dispatch) { - const nextStateOrDispatch = selectStateOrDispatch(state, props, dispatch) - const nextProps = map.length === 1 ? noProps : ownPropsSelector(state, props, dispatch) + const nextStorePart = getStorePart(state, props, dispatch) + const nextProps = map.dependsOnProps() ? getOwnProps(state, props, dispatch) : noProps - if (lastStateOrDispatch !== nextStateOrDispatch || lastProps !== nextProps) { - lastStateOrDispatch = nextStateOrDispatch + if (lastStorePart !== nextStorePart || lastProps !== nextProps) { + lastStorePart = nextStorePart lastProps = nextProps - const nextResult = mapProxy(nextStateOrDispatch, nextProps) + const nextResult = map(nextStorePart, nextProps) if (!lastResult || !shallowEqual(lastResult, nextResult)) { lastResult = nextResult @@ -56,3 +58,8 @@ export default function buildFactoryAwareSelector( return lastResult } } + +export default function buildFactoryAwareSelector(pure, getOwnProps, getStorePart, mapToProps) { + const build = pure ? buildPureFactoryAwareSelector : buildImpureFactoryAwareSelector + return build(getOwnProps, getStorePart, mapToProps) +} diff --git a/src/selectors/index.js b/src/selectors/index.js deleted file mode 100644 index 8f39723a6..000000000 --- a/src/selectors/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export * from './dispatch' -export * from './ownProps' -export * from './state' From e51315ffc75d5eb5fccdeabc781e69eb9a7d0c4e Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Mon, 20 Jun 2016 00:42:42 -0400 Subject: [PATCH 41/76] improve memoization perf --- src/components/connect.js | 16 ++++++++-------- src/selectors/buildFactoryAwareSelector.js | 4 ++-- src/selectors/ownProps.js | 6 ++++-- src/utils/shallowEqual.js | 12 +++++++----- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/components/connect.js b/src/components/connect.js index 70a432878..edde64977 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -38,6 +38,7 @@ export function buildPureSelector({ getState, getDispatch, getOwn, merge }) { let lastOwn = undefined let lastState = undefined let lastDispatch = undefined + let lastMerged = undefined let lastResult = undefined return function pureSelector(state, props, dispatch) { const nextOwn = getOwn(state, props, dispatch) @@ -45,16 +46,15 @@ export function buildPureSelector({ getState, getDispatch, getOwn, merge }) { const nextDispatch = getDispatch(state, props, dispatch) if (lastOwn !== nextOwn || lastState !== nextState || lastDispatch !== nextDispatch) { - lastOwn = nextOwn - lastState = nextState - lastDispatch = nextDispatch - - const nextResult = merge(nextState, nextDispatch, nextOwn) - if (!lastResult || !shallowEqual(lastResult, nextResult)) { - lastResult = nextResult + const nextMerged = merge(nextState, nextDispatch, nextOwn) + if (!lastMerged || !shallowEqual(lastMerged, nextMerged)) { + lastResult = nextMerged } + lastMerged = nextMerged } - + lastOwn = nextOwn + lastState = nextState + lastDispatch = nextDispatch return lastResult } } diff --git a/src/selectors/buildFactoryAwareSelector.js b/src/selectors/buildFactoryAwareSelector.js index 60425042d..84c6a69e1 100644 --- a/src/selectors/buildFactoryAwareSelector.js +++ b/src/selectors/buildFactoryAwareSelector.js @@ -47,14 +47,14 @@ export function buildPureFactoryAwareSelector(getOwnProps, getStorePart, mapToPr const nextProps = map.dependsOnProps() ? getOwnProps(state, props, dispatch) : noProps if (lastStorePart !== nextStorePart || lastProps !== nextProps) { - lastStorePart = nextStorePart - lastProps = nextProps const nextResult = map(nextStorePart, nextProps) if (!lastResult || !shallowEqual(lastResult, nextResult)) { lastResult = nextResult } } + lastStorePart = nextStorePart + lastProps = nextProps return lastResult } } diff --git a/src/selectors/ownProps.js b/src/selectors/ownProps.js index 2782fcdff..497f7a542 100644 --- a/src/selectors/ownProps.js +++ b/src/selectors/ownProps.js @@ -6,11 +6,13 @@ export function impureOwnPropsSelector(_, props) { export function buildPureOwnPropsSelector() { let lastProps = undefined + let lastResult = undefined return function pureOwnPropsSelector(_, nextProps) { if (!lastProps || !shallowEqual(lastProps, nextProps)) { - lastProps = nextProps + lastResult = nextProps } - return lastProps + lastProps = nextProps + return lastResult } } diff --git a/src/utils/shallowEqual.js b/src/utils/shallowEqual.js index 76df37841..5a5bc4185 100644 --- a/src/utils/shallowEqual.js +++ b/src/utils/shallowEqual.js @@ -1,3 +1,5 @@ +const hasOwn = Object.prototype.hasOwnProperty + export default function shallowEqual(objA, objB) { if (objA === objB) { return true @@ -5,16 +7,16 @@ export default function shallowEqual(objA, objB) { const keysA = Object.keys(objA) const keysB = Object.keys(objB) + const lengthA = keysA.length - if (keysA.length !== keysB.length) { + if (lengthA !== keysB.length) { return false } // Test for A's keys different from B. - const hasOwn = Object.prototype.hasOwnProperty - for (let i = 0; i < keysA.length; i++) { - if (!hasOwn.call(objB, keysA[i]) || - objA[keysA[i]] !== objB[keysA[i]]) { + for (let i = 0; i < lengthA; i++) { + const key = keysA[i] + if (!hasOwn.call(objB, key) || objA[key] !== objB[key]) { return false } } From 4969368fe8a442fb60cd95de24a9d411b2086cd2 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Mon, 20 Jun 2016 23:58:08 -0400 Subject: [PATCH 42/76] refactor connect + selectors for clarity --- src/components/connect.js | 99 +++++-------------- src/components/connectAdvanced.js | 5 +- ...ector.js => createFactoryAwareSelector.js} | 16 +-- src/selectors/createMatchingSelector.js | 8 ++ src/selectors/dispatch.js | 44 --------- src/selectors/getFinalProps.js | 66 +++++++++++++ src/selectors/{ownProps.js => getOwnProps.js} | 6 +- src/selectors/mapDispatch.js | 40 ++++++++ src/selectors/mapState.js | 30 ++++++ src/selectors/state.js | 25 ----- 10 files changed, 186 insertions(+), 153 deletions(-) rename src/selectors/{buildFactoryAwareSelector.js => createFactoryAwareSelector.js} (71%) create mode 100644 src/selectors/createMatchingSelector.js delete mode 100644 src/selectors/dispatch.js create mode 100644 src/selectors/getFinalProps.js rename src/selectors/{ownProps.js => getOwnProps.js} (76%) create mode 100644 src/selectors/mapDispatch.js create mode 100644 src/selectors/mapState.js delete mode 100644 src/selectors/state.js diff --git a/src/components/connect.js b/src/components/connect.js index edde64977..aacb36625 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -1,90 +1,45 @@ import connectAdvanced from './connectAdvanced' -import { buildDispatchPropsSelector } from '../selectors/dispatch' -import { buildOwnPropsSelector } from '../selectors/ownProps' -import { buildStatePropsSelector } from '../selectors/state' -import shallowEqual from '../utils/shallowEqual' import verifyPlainObject from '../utils/verifyPlainObject' +import { createFinalPropsComponents, createFinalPropsSelector } from '../selectors/getFinalProps' -export function defaultMergeProps(stateProps, dispatchProps, ownProps) { - return { - ...ownProps, - ...stateProps, - ...dispatchProps - } -} - -export function wrapWithVerify(displayName, { getState, getDispatch, getOwn, merge }) { +export function wrapWithVerify(displayName, { getState, getDispatch, getOwnProps, mergeProps }) { return { getState: verifyPlainObject(displayName, 'mapStateToProps', getState), getDispatch: verifyPlainObject(displayName, 'mapDispatchToProps', getDispatch), - getOwn, - merge: merge !== defaultMergeProps - ? verifyPlainObject(displayName, 'mergeProps', merge) - : merge - } -} - -export function buildImpureSelector({ getState, getDispatch, getOwn, merge }) { - return function impureSelector(state, props, dispatch) { - return merge( - getState(state, props, dispatch), - getDispatch(state, props, dispatch), - getOwn(state, props, dispatch) - ) - } -} - -export function buildPureSelector({ getState, getDispatch, getOwn, merge }) { - let lastOwn = undefined - let lastState = undefined - let lastDispatch = undefined - let lastMerged = undefined - let lastResult = undefined - return function pureSelector(state, props, dispatch) { - const nextOwn = getOwn(state, props, dispatch) - const nextState = getState(state, props, dispatch) - const nextDispatch = getDispatch(state, props, dispatch) - - if (lastOwn !== nextOwn || lastState !== nextState || lastDispatch !== nextDispatch) { - const nextMerged = merge(nextState, nextDispatch, nextOwn) - if (!lastMerged || !shallowEqual(lastMerged, nextMerged)) { - lastResult = nextMerged - } - lastMerged = nextMerged - } - lastOwn = nextOwn - lastState = nextState - lastDispatch = nextDispatch - return lastResult + getOwnProps, + mergeProps: !mergeProps.isDefault + ? verifyPlainObject(displayName, 'mergeProps', mergeProps) + : mergeProps } } // create a connectAdvanced-compatible selectorFactory function that applies the results of // mapStateToProps, mapDispatchToProps, and mergeProps -export function buildSelectorFactory(mapStateToProps, mapDispatchToProps, merge) { - return function selectorFactory({ displayName, pure }) { - const getOwn = buildOwnPropsSelector(pure) - const getState = buildStatePropsSelector(pure, getOwn, mapStateToProps) - const getDispatch = buildDispatchPropsSelector(pure, getOwn, mapDispatchToProps) +export function selectorFactory(options) { + return createFinalPropsSelector( + options.pure, + wrapWithVerify( + options.displayName, + createFinalPropsComponents(options) + ) + ) +} - const build = pure ? buildPureSelector : buildImpureSelector - return build(wrapWithVerify(displayName, { getState, getDispatch, getOwn, merge })) +export function buildOptions(mapStateToProps, mapDispatchToProps, mergeProps, options) { + return { + getDisplayName: name => `Connect(${name})`, + ...options, + mapStateToProps, + mapDispatchToProps, + mergeProps, + methodName: 'connect', + dependsOnState: Boolean(mapStateToProps) } } -export default function connect( - mapStateToProps, - mapDispatchToProps, - mergeProps, - options -) { +export default function connect() { return connectAdvanced( - buildSelectorFactory(mapStateToProps, mapDispatchToProps, mergeProps || defaultMergeProps), - { - getDisplayName: name => `Connect(${name})`, - ...options, - methodName: 'connect', - dependsOnState: Boolean(mapStateToProps) - } + selectorFactory, + buildOptions.apply(undefined, arguments) ) } diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index d9cf27eb0..83c78234d 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -52,7 +52,9 @@ export default function connectAdvanced( storeKey = 'store', // if true, the wrapped element is exposed by this HOC via the getWrappedInstance() function. - withRef = false + withRef = false, + + ...connectOptions } = {} ) { const subscriptionKey = storeKey + 'Subscription' @@ -121,6 +123,7 @@ export default function connectAdvanced( this.lastRenderedProps = null this.selector = buildSelector({ + ...connectOptions, displayName: Connect.displayName, dispatch: this.store.dispatch, getState: dependsOnState ? this.store.getState : (() => null), diff --git a/src/selectors/buildFactoryAwareSelector.js b/src/selectors/createFactoryAwareSelector.js similarity index 71% rename from src/selectors/buildFactoryAwareSelector.js rename to src/selectors/createFactoryAwareSelector.js index 84c6a69e1..65b93a141 100644 --- a/src/selectors/buildFactoryAwareSelector.js +++ b/src/selectors/createFactoryAwareSelector.js @@ -2,7 +2,7 @@ import shallowEqual from '../utils/shallowEqual' // factory detection. if the first result of mapToProps is a function, use that as the // true mapToProps -export default function buildMapOrMapFactoryProxy(mapToProps) { +export default function createMapOrMapFactoryProxy(mapToProps) { let map = undefined function firstRun(storePart, props) { const result = mapToProps(storePart, props) @@ -24,8 +24,8 @@ export default function buildMapOrMapFactoryProxy(mapToProps) { return proxy } -export function buildImpureFactoryAwareSelector(getOwnProps, getStorePart, mapToProps) { - const map = buildMapOrMapFactoryProxy(mapToProps) +export function createImpureFactoryAwareSelector(getOwnProps, getStorePart, mapToProps) { + const map = createMapOrMapFactoryProxy(mapToProps) return function impureFactoryAwareSelector(state, props, dispatch) { return map( @@ -35,8 +35,8 @@ export function buildImpureFactoryAwareSelector(getOwnProps, getStorePart, mapTo } } -export function buildPureFactoryAwareSelector(getOwnProps, getStorePart, mapToProps) { - const map = buildMapOrMapFactoryProxy(mapToProps) +export function createPureFactoryAwareSelector(getOwnProps, getStorePart, mapToProps) { + const map = createMapOrMapFactoryProxy(mapToProps) const noProps = {} let lastStorePart = undefined let lastProps = undefined @@ -59,7 +59,7 @@ export function buildPureFactoryAwareSelector(getOwnProps, getStorePart, mapToPr } } -export default function buildFactoryAwareSelector(pure, getOwnProps, getStorePart, mapToProps) { - const build = pure ? buildPureFactoryAwareSelector : buildImpureFactoryAwareSelector - return build(getOwnProps, getStorePart, mapToProps) +export default function createFactoryAwareSelector(pure, getOwnProps, getStorePart, mapToProps) { + const create = pure ? createPureFactoryAwareSelector : createImpureFactoryAwareSelector + return create(getOwnProps, getStorePart, mapToProps) } diff --git a/src/selectors/createMatchingSelector.js b/src/selectors/createMatchingSelector.js new file mode 100644 index 000000000..cf6e1f7c9 --- /dev/null +++ b/src/selectors/createMatchingSelector.js @@ -0,0 +1,8 @@ +export default function createMatchingSelector(factories, options, getOwnProps) { + for (let i = factories.length - 1; i >= 0; i--) { + const selector = factories[i](options, getOwnProps) + if (selector) return selector + } + + return undefined +} diff --git a/src/selectors/dispatch.js b/src/selectors/dispatch.js deleted file mode 100644 index 3f9d5d278..000000000 --- a/src/selectors/dispatch.js +++ /dev/null @@ -1,44 +0,0 @@ -import { bindActionCreators } from 'redux' - -import buildFactoryAwareSelector from './buildFactoryAwareSelector' - -export function buildMissingDispatchSelector() { - let dispatchProps = undefined - return function dispatchSelector(_, __, dispatch) { - if (!dispatchProps) { - dispatchProps = { dispatch } - } - return dispatchProps - } -} - -export function buildBoundActionCreatorsSelector(mapDispatchToProps) { - let bound = undefined - return function boundActionCreatorsSelector(_, __, dispatch) { - if (!bound) { - bound = bindActionCreators(mapDispatchToProps, dispatch) - } - return bound - } -} - -export function buildFactoryAwareDispatchSelector(pure, ownPropsSelector, mapDispatchToProps) { - return buildFactoryAwareSelector( - pure, - ownPropsSelector, - (_, __, dispatch) => dispatch, - mapDispatchToProps - ) -} - -export function buildDispatchPropsSelector(pure, ownPropsSelector, mapDispatchToProps) { - if (!mapDispatchToProps) { - return buildMissingDispatchSelector() - } - - if (typeof mapDispatchToProps !== 'function') { - return buildBoundActionCreatorsSelector(mapDispatchToProps) - } - - return buildFactoryAwareDispatchSelector(pure, ownPropsSelector, mapDispatchToProps) -} diff --git a/src/selectors/getFinalProps.js b/src/selectors/getFinalProps.js new file mode 100644 index 000000000..e6f1d91b3 --- /dev/null +++ b/src/selectors/getFinalProps.js @@ -0,0 +1,66 @@ +import { createOwnPropsSelector } from '../selectors/getOwnProps' +import { createMapDispatchSelector } from '../selectors/mapDispatch' +import { createMapStateSelector } from '../selectors/mapState' +import shallowEqual from '../utils/shallowEqual' + +export function defaultMergeProps(stateProps, dispatchProps, ownProps) { + return { + ...ownProps, + ...stateProps, + ...dispatchProps + } +} +defaultMergeProps.isDefault = true + +export function createFinalPropsComponents({ mergeProps, ...options }) { + const getOwnProps = createOwnPropsSelector(options.pure) + const getState = createMapStateSelector(options, getOwnProps) + const getDispatch = createMapDispatchSelector(options, getOwnProps) + return { + getState, + getDispatch, + getOwnProps, + mergeProps: mergeProps || defaultMergeProps + } +} + +export function createImpureFinalPropsSelector({ getState, getDispatch, getOwnProps, mergeProps }) { + return function impureSelector(state, props, dispatch) { + return mergeProps( + getState(state, props, dispatch), + getDispatch(state, props, dispatch), + getOwnProps(state, props, dispatch) + ) + } +} + +export function createPureFinalPropsSelector({ getState, getDispatch, getOwnProps, mergeProps }) { + let lastOwn = undefined + let lastState = undefined + let lastDispatch = undefined + let lastMerged = undefined + let lastResult = undefined + return function pureSelector(state, props, dispatch) { + const nextOwn = getOwnProps(state, props, dispatch) + const nextState = getState(state, props, dispatch) + const nextDispatch = getDispatch(state, props, dispatch) + + if (lastOwn !== nextOwn || lastState !== nextState || lastDispatch !== nextDispatch) { + const nextMerged = mergeProps(nextState, nextDispatch, nextOwn) + if (!lastMerged || !shallowEqual(lastMerged, nextMerged)) { + lastResult = nextMerged + } + lastMerged = nextMerged + } + lastOwn = nextOwn + lastState = nextState + lastDispatch = nextDispatch + return lastResult + } +} + +export function createFinalPropsSelector(pure, components) { + return pure + ? createPureFinalPropsSelector(components) + : createImpureFinalPropsSelector(components) +} diff --git a/src/selectors/ownProps.js b/src/selectors/getOwnProps.js similarity index 76% rename from src/selectors/ownProps.js rename to src/selectors/getOwnProps.js index 497f7a542..1988af5b3 100644 --- a/src/selectors/ownProps.js +++ b/src/selectors/getOwnProps.js @@ -4,7 +4,7 @@ export function impureOwnPropsSelector(_, props) { return props } -export function buildPureOwnPropsSelector() { +export function createPureOwnPropsSelector() { let lastProps = undefined let lastResult = undefined return function pureOwnPropsSelector(_, nextProps) { @@ -16,8 +16,8 @@ export function buildPureOwnPropsSelector() { } } -export function buildOwnPropsSelector(pure) { +export function createOwnPropsSelector(pure) { return pure - ? buildPureOwnPropsSelector() + ? createPureOwnPropsSelector() : impureOwnPropsSelector } diff --git a/src/selectors/mapDispatch.js b/src/selectors/mapDispatch.js new file mode 100644 index 000000000..1f33f4f31 --- /dev/null +++ b/src/selectors/mapDispatch.js @@ -0,0 +1,40 @@ +import { bindActionCreators } from 'redux' + +import createFactoryAwareSelector from './createFactoryAwareSelector' +import createMatchingSelector from '../selectors/createMatchingSelector' + +export function whenMapDispatchIsMissing({ mapDispatchToProps, dispatch }) { + if (!mapDispatchToProps) { + const dispatchProp = { dispatch } + return () => dispatchProp + } +} + +export function whenMapDispatchIsObject({ mapDispatchToProps, dispatch }) { + if (mapDispatchToProps && typeof mapDispatchToProps === 'object') { + const bound = bindActionCreators(mapDispatchToProps, dispatch) + return () => bound + } +} + +export function whenMapDispatchIsFunction({ mapDispatchToProps, pure, dispatch }, getOwnProps) { + if (typeof mapDispatchToProps === 'function') { + return createFactoryAwareSelector(pure, getOwnProps, () => dispatch, mapDispatchToProps) + } +} + +export function getDefaultMapDispatchFactories() { + return [ + whenMapDispatchIsMissing, + whenMapDispatchIsFunction, + whenMapDispatchIsObject + ] +} + +export function createMapDispatchSelector({ mapDispatchFactories, ...options }, getOwnProps) { + return createMatchingSelector( + mapDispatchFactories || getDefaultMapDispatchFactories(), + options, + getOwnProps + ) +} diff --git a/src/selectors/mapState.js b/src/selectors/mapState.js new file mode 100644 index 000000000..99ececd65 --- /dev/null +++ b/src/selectors/mapState.js @@ -0,0 +1,30 @@ +import createFactoryAwareSelector from './createFactoryAwareSelector' +import createMatchingSelector from '../selectors/createMatchingSelector' + +export function whenMapStateIsMissing({ mapStateToProps }) { + if (!mapStateToProps) { + const empty = {} + return () => empty + } +} + +export function whenMapStateIsFunction({ mapStateToProps, pure }, getOwnProps) { + if (typeof mapStateToProps === 'function') { + return createFactoryAwareSelector(pure, getOwnProps, state => state, mapStateToProps) + } +} + +export function getDefaultMapStateFactories() { + return [ + whenMapStateIsMissing, + whenMapStateIsFunction + ] +} + +export function createMapStateSelector({ mapStateFactories, ...options }, getOwnProps) { + return createMatchingSelector( + mapStateFactories || getDefaultMapStateFactories(), + options, + getOwnProps + ) +} diff --git a/src/selectors/state.js b/src/selectors/state.js deleted file mode 100644 index 8d15e8b43..000000000 --- a/src/selectors/state.js +++ /dev/null @@ -1,25 +0,0 @@ -import buildFactoryAwareSelector from './buildFactoryAwareSelector' - -export function buildMissingStateSelector() { - const empty = {} - return function missingStateSelector() { - return empty - } -} - -export function buildFactoryAwareStateSelector(pure, ownPropsSelector, mapStateToProps) { - return buildFactoryAwareSelector( - pure, - ownPropsSelector, - state => state, - mapStateToProps - ) -} - -export function buildStatePropsSelector(pure, ownPropsSelector, mapStateToProps) { - if (!mapStateToProps) { - return buildMissingStateSelector() - } - - return buildFactoryAwareStateSelector(pure, ownPropsSelector, mapStateToProps) -} From b1cc3d34c1c871925afb1dd28aad5e11bb1a2afc Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Wed, 22 Jun 2016 20:51:41 -0400 Subject: [PATCH 43/76] refactor connect + selectors --- src/components/connect.js | 49 ++++++++++++++++++---------------- src/selectors/getFinalProps.js | 32 +++------------------- src/selectors/getOwnProps.js | 7 ++++- src/selectors/mapDispatch.js | 15 ++++++----- src/selectors/mapState.js | 15 ++++++----- src/selectors/mergeProps.js | 8 ++++++ 6 files changed, 60 insertions(+), 66 deletions(-) create mode 100644 src/selectors/mergeProps.js diff --git a/src/components/connect.js b/src/components/connect.js index aacb36625..9d3fbcc05 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -1,45 +1,48 @@ +import { flow } from 'lodash' + import connectAdvanced from './connectAdvanced' import verifyPlainObject from '../utils/verifyPlainObject' -import { createFinalPropsComponents, createFinalPropsSelector } from '../selectors/getFinalProps' +import { createFinalPropsSelector } from '../selectors/getFinalProps' +import { addGetOwnProps } from '../selectors/getOwnProps' +import { addGetDispatch, getDefaultMapDispatchFactories } from '../selectors/mapDispatch' +import { addGetState, getDefaultMapStateFactories } from '../selectors/mapState' +import { defaultMergeProps } from '../selectors/mergeProps' -export function wrapWithVerify(displayName, { getState, getDispatch, getOwnProps, mergeProps }) { +export function wrapWithVerify({ getState, getDispatch, mergeProps, ...options }) { + const verify = (methodName, func) => verifyPlainObject(options.displayName, methodName, func) return { - getState: verifyPlainObject(displayName, 'mapStateToProps', getState), - getDispatch: verifyPlainObject(displayName, 'mapDispatchToProps', getDispatch), - getOwnProps, - mergeProps: !mergeProps.isDefault - ? verifyPlainObject(displayName, 'mergeProps', mergeProps) - : mergeProps + ...options, + getState: verify('mapStateToProps', getState), + getDispatch: verify('mapDispatchToProps', getDispatch), + mergeProps: verify('mergeProps', mergeProps) } } -// create a connectAdvanced-compatible selectorFactory function that applies the results of -// mapStateToProps, mapDispatchToProps, and mergeProps export function selectorFactory(options) { - return createFinalPropsSelector( - options.pure, - wrapWithVerify( - options.displayName, - createFinalPropsComponents(options) - ) - ) + return flow( + addGetOwnProps, + addGetState, + addGetDispatch, + wrapWithVerify, + createFinalPropsSelector + )(options) } export function buildOptions(mapStateToProps, mapDispatchToProps, mergeProps, options) { return { getDisplayName: name => `Connect(${name})`, + mapDispatchFactories: getDefaultMapDispatchFactories(), + mapStateFactories: getDefaultMapStateFactories(), ...options, mapStateToProps, mapDispatchToProps, - mergeProps, + mergeProps: mergeProps || defaultMergeProps, methodName: 'connect', dependsOnState: Boolean(mapStateToProps) } } -export default function connect() { - return connectAdvanced( - selectorFactory, - buildOptions.apply(undefined, arguments) - ) +export default function connect(...args) { + const options = buildOptions(...args) + return connectAdvanced(selectorFactory, options) } diff --git a/src/selectors/getFinalProps.js b/src/selectors/getFinalProps.js index e6f1d91b3..25446f32c 100644 --- a/src/selectors/getFinalProps.js +++ b/src/selectors/getFinalProps.js @@ -1,29 +1,5 @@ -import { createOwnPropsSelector } from '../selectors/getOwnProps' -import { createMapDispatchSelector } from '../selectors/mapDispatch' -import { createMapStateSelector } from '../selectors/mapState' import shallowEqual from '../utils/shallowEqual' -export function defaultMergeProps(stateProps, dispatchProps, ownProps) { - return { - ...ownProps, - ...stateProps, - ...dispatchProps - } -} -defaultMergeProps.isDefault = true - -export function createFinalPropsComponents({ mergeProps, ...options }) { - const getOwnProps = createOwnPropsSelector(options.pure) - const getState = createMapStateSelector(options, getOwnProps) - const getDispatch = createMapDispatchSelector(options, getOwnProps) - return { - getState, - getDispatch, - getOwnProps, - mergeProps: mergeProps || defaultMergeProps - } -} - export function createImpureFinalPropsSelector({ getState, getDispatch, getOwnProps, mergeProps }) { return function impureSelector(state, props, dispatch) { return mergeProps( @@ -59,8 +35,8 @@ export function createPureFinalPropsSelector({ getState, getDispatch, getOwnProp } } -export function createFinalPropsSelector(pure, components) { - return pure - ? createPureFinalPropsSelector(components) - : createImpureFinalPropsSelector(components) +export function createFinalPropsSelector(options) { + return options.pure + ? createPureFinalPropsSelector(options) + : createImpureFinalPropsSelector(options) } diff --git a/src/selectors/getOwnProps.js b/src/selectors/getOwnProps.js index 1988af5b3..cdf670090 100644 --- a/src/selectors/getOwnProps.js +++ b/src/selectors/getOwnProps.js @@ -16,8 +16,13 @@ export function createPureOwnPropsSelector() { } } -export function createOwnPropsSelector(pure) { +export function createOwnPropsSelector({ pure }) { return pure ? createPureOwnPropsSelector() : impureOwnPropsSelector } + +export function addGetOwnProps(options) { + const getOwnProps = createOwnPropsSelector(options) + return { getOwnProps, ...options } +} diff --git a/src/selectors/mapDispatch.js b/src/selectors/mapDispatch.js index 1f33f4f31..436d6ef7f 100644 --- a/src/selectors/mapDispatch.js +++ b/src/selectors/mapDispatch.js @@ -17,7 +17,7 @@ export function whenMapDispatchIsObject({ mapDispatchToProps, dispatch }) { } } -export function whenMapDispatchIsFunction({ mapDispatchToProps, pure, dispatch }, getOwnProps) { +export function whenMapDispatchIsFunction({ mapDispatchToProps, pure, dispatch, getOwnProps }) { if (typeof mapDispatchToProps === 'function') { return createFactoryAwareSelector(pure, getOwnProps, () => dispatch, mapDispatchToProps) } @@ -31,10 +31,11 @@ export function getDefaultMapDispatchFactories() { ] } -export function createMapDispatchSelector({ mapDispatchFactories, ...options }, getOwnProps) { - return createMatchingSelector( - mapDispatchFactories || getDefaultMapDispatchFactories(), - options, - getOwnProps - ) +export function createMapDispatchSelector(options) { + return createMatchingSelector(options.mapDispatchFactories, options) +} + +export function addGetDispatch(options) { + const getDispatch = createMapDispatchSelector(options) + return { getDispatch, ...options } } diff --git a/src/selectors/mapState.js b/src/selectors/mapState.js index 99ececd65..670b37e46 100644 --- a/src/selectors/mapState.js +++ b/src/selectors/mapState.js @@ -8,7 +8,7 @@ export function whenMapStateIsMissing({ mapStateToProps }) { } } -export function whenMapStateIsFunction({ mapStateToProps, pure }, getOwnProps) { +export function whenMapStateIsFunction({ mapStateToProps, pure, getOwnProps }) { if (typeof mapStateToProps === 'function') { return createFactoryAwareSelector(pure, getOwnProps, state => state, mapStateToProps) } @@ -21,10 +21,11 @@ export function getDefaultMapStateFactories() { ] } -export function createMapStateSelector({ mapStateFactories, ...options }, getOwnProps) { - return createMatchingSelector( - mapStateFactories || getDefaultMapStateFactories(), - options, - getOwnProps - ) +export function createMapStateSelector(options) { + return createMatchingSelector(options.mapStateFactories, options) +} + +export function addGetState(options) { + const getState = createMapStateSelector(options) + return { getState, ...options } } diff --git a/src/selectors/mergeProps.js b/src/selectors/mergeProps.js new file mode 100644 index 000000000..4eed9c058 --- /dev/null +++ b/src/selectors/mergeProps.js @@ -0,0 +1,8 @@ + +export function defaultMergeProps(stateProps, dispatchProps, ownProps) { + return { + ...ownProps, + ...stateProps, + ...dispatchProps + } +} From 78045dc4c1ad61bffc309120effa01e052e3e478 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Wed, 22 Jun 2016 23:44:22 -0400 Subject: [PATCH 44/76] pull apart buildSelector into its pieces and move them into getFinalProps and connectAdvanced because it wasn't clear what it did as it was --- src/components/connectAdvanced.js | 75 +++++++++++++++++++------------ src/index.js | 12 +---- src/selectors/getFinalProps.js | 18 ++++++++ src/utils/buildSelector.js | 51 --------------------- 4 files changed, 65 insertions(+), 91 deletions(-) delete mode 100644 src/utils/buildSelector.js diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 83c78234d..4ab287dd5 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -2,8 +2,8 @@ import hoistStatics from 'hoist-non-react-statics' import invariant from 'invariant' import { Component, PropTypes, createElement } from 'react' +import { memoizeFinalPropsSelector } from '../selectors/getFinalProps' import Subscription from '../utils/Subscription' -import defaultBuildSelector from '../utils/buildSelector' import storeShape from '../utils/storeShape' let hotReloadingVersion = 0 @@ -20,12 +20,6 @@ export default function connectAdvanced( selectorFactory, // options object: { - // this is the function that invokes the selectorFactory and enhances it with a few important - // behaviors. you probably want to leave this alone unless you've read and understand the source - // for components/connectAdvanced.js and utils/buildSelector.js, then maybe you can use this as - // an injection point for custom behavior, for example hooking in some custom devtools. - buildSelector = defaultBuildSelector, - // if true, the selector receieves the current store state as the first arg, and this HOC // subscribes to store changes during componentDidMount. if false, null is passed as the first // arg of selector and store.subscribe() is never called. @@ -69,6 +63,7 @@ export default function connectAdvanced( this.state = {} this.store = this.props[storeKey] || this.context[storeKey] this.parentSub = this.props[subscriptionKey] || this.context[subscriptionKey] + this.setWrappedInstance = this.setWrappedInstance.bind(this) invariant(this.store, `Could not find "${storeKey}" in either the context or ` + @@ -118,32 +113,62 @@ export default function connectAdvanced( ) return this.wrappedInstance } + setWrappedInstance(ref) { + this.wrappedInstance = ref + } initSelector() { this.lastRenderedProps = null + this.recomputations = 0 + + function addExtraProps(props) { + if (!withRef && !recomputationsProp) return props + // make a shallow copy so that fields added don't leak to the original selector. + // this is especially important for 'ref' since that's a reference back to the component + // instance. a singleton memoized selector would then be holding a reference to the + // instance, preventing the instance from being garbage collected, and that would be bad + const result = { ...props } + if (withRef) result.ref = this.setWrappedInstance + if (recomputationsProp) result[recomputationsProp] = this.recomputations++ + return result + } - this.selector = buildSelector({ - ...connectOptions, - displayName: Connect.displayName, + const sourceSelector = selectorFactory({ + // most options passed to connectAdvanced are passed along to the selectorFactory + dependsOnState, methodName, pure, storeKey, withRef, ...connectOptions, + // useful for factories that want to bind action creators outside the selector dispatch: this.store.dispatch, - getState: dependsOnState ? this.store.getState : (() => null), - ref: withRef ? (ref => { this.wrappedInstance = ref }) : undefined, - WrappedComponent, - selectorFactory, - recomputationsProp, - methodName, - pure, - dependsOnState, - storeKey, - withRef + // useful for error messages + displayName: Connect.displayName, + // useful if a factory wants to use attributes of the component to build the selector, + // for example: one could use its propTypes as a props whitelist + WrappedComponent }) + + const memoizedSelector = memoizeFinalPropsSelector( + sourceSelector, + addExtraProps.bind(this) + ) + + this.selector = function selector(ownProps) { + const state = dependsOnState ? this.store.getState() : null + return memoizedSelector(state, ownProps, this.store.dispatch) + } } initSubscription() { + function onStoreStateChange(notifyNestedSubs) { + if (dependsOnState && this.shouldComponentUpdate(this.props)) { + this.setState({}, notifyNestedSubs) + } else { + notifyNestedSubs() + } + } + this.subscription = new Subscription( this.store, this.parentSub, - this.onStateChange.bind(this) + onStoreStateChange.bind(this) ) } @@ -151,14 +176,6 @@ export default function connectAdvanced( return this.subscription.isSubscribed() } - onStateChange(callback) { - if (dependsOnState && this.shouldComponentUpdate(this.props)) { - this.setState({}, callback) - } else { - callback() - } - } - render() { return createElement( WrappedComponent, diff --git a/src/index.js b/src/index.js index dab737386..6e4773373 100644 --- a/src/index.js +++ b/src/index.js @@ -2,14 +2,4 @@ import Provider from './components/Provider' import connect from './components/connect' import connectAdvanced from './components/connectAdvanced' -import buildSelector from './utils/buildSelector' -import shallowEqual from './utils/shallowEqual' - -export { - Provider, - connect, - connectAdvanced, - - buildSelector, - shallowEqual -} +export { Provider, connect, connectAdvanced } diff --git a/src/selectors/getFinalProps.js b/src/selectors/getFinalProps.js index 25446f32c..7850994a8 100644 --- a/src/selectors/getFinalProps.js +++ b/src/selectors/getFinalProps.js @@ -40,3 +40,21 @@ export function createFinalPropsSelector(options) { ? createPureFinalPropsSelector(options) : createImpureFinalPropsSelector(options) } + +export function memoizeFinalPropsSelector(selector, enhance) { + let lastProps = undefined + let lastResult = undefined + + return function memoize(state, ownProps, dispatch) { + const nextProps = selector(state, ownProps, dispatch) + + // wrap the source selector in a shallow equals because props objects with same properties are + // semantically equal to React... no need to return a new object. + if (!lastProps || !shallowEqual(lastProps, nextProps)) { + lastResult = enhance(nextProps) + } + lastProps = nextProps + + return lastResult + } +} diff --git a/src/utils/buildSelector.js b/src/utils/buildSelector.js deleted file mode 100644 index 854cd64ae..000000000 --- a/src/utils/buildSelector.js +++ /dev/null @@ -1,51 +0,0 @@ -import shallowEqual from '../utils/shallowEqual' - -export default function buildSelector({ - selectorFactory, - dispatch, - getState, - ref, - recomputationsProp, - ...options - }) { - const selector = selectorFactory({ - // useful for selector factories that want to bind action creators before returning - // their selector - dispatch, - // additional options passed to buildSelector are passed along to the selectorFactory - ...options - }) - - let recomputations = 0 - let selectorProps = undefined - let finalProps = undefined - - const mightAddProps = ref || recomputationsProp - function getFinalProps() { - if (!mightAddProps) return selectorProps - - // make a shallow copy so that fields added don't leak to the original selector. - // this is especially important for 'ref' since that's a reference back to the component - // instance. a singleton memoized selector would then be holding a reference to the instance, - // preventing the instance from being garbage collected, and that would be bad - const props = { ...selectorProps } - if (ref) props.ref = ref - if (recomputationsProp) props[recomputationsProp] = recomputations - return props - } - - return function runSelector(ownProps) { - const state = getState() - const newProps = selector(state, ownProps, dispatch) - - // wrap the source selector in a shallow equals because props objects with same properties are - // semantically equal to React... no need to return a new object. - if (!selectorProps || !shallowEqual(selectorProps, newProps)) { - recomputations++ - selectorProps = newProps - finalProps = getFinalProps() - } - - return finalProps - } -} From 846c09bd70cbfa0c4d6d363c30358a207987e54e Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 23 Jun 2016 09:42:37 -0400 Subject: [PATCH 45/76] Move where 'extra props' (ref and recomputations) are added to the final props to simplify memoization functions --- src/components/connectAdvanced.js | 35 +++++++++++++++---------------- src/selectors/getFinalProps.js | 4 ++-- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 4ab287dd5..9ac0f0086 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -61,6 +61,7 @@ export default function connectAdvanced( this.version = version this.state = {} + this.recomputations = 0 this.store = this.props[storeKey] || this.context[storeKey] this.parentSub = this.props[subscriptionKey] || this.context[subscriptionKey] this.setWrappedInstance = this.setWrappedInstance.bind(this) @@ -119,19 +120,6 @@ export default function connectAdvanced( initSelector() { this.lastRenderedProps = null - this.recomputations = 0 - - function addExtraProps(props) { - if (!withRef && !recomputationsProp) return props - // make a shallow copy so that fields added don't leak to the original selector. - // this is especially important for 'ref' since that's a reference back to the component - // instance. a singleton memoized selector would then be holding a reference to the - // instance, preventing the instance from being garbage collected, and that would be bad - const result = { ...props } - if (withRef) result.ref = this.setWrappedInstance - if (recomputationsProp) result[recomputationsProp] = this.recomputations++ - return result - } const sourceSelector = selectorFactory({ // most options passed to connectAdvanced are passed along to the selectorFactory @@ -145,10 +133,7 @@ export default function connectAdvanced( WrappedComponent }) - const memoizedSelector = memoizeFinalPropsSelector( - sourceSelector, - addExtraProps.bind(this) - ) + const memoizedSelector = memoizeFinalPropsSelector(sourceSelector) this.selector = function selector(ownProps) { const state = dependsOnState ? this.store.getState() : null @@ -176,10 +161,24 @@ export default function connectAdvanced( return this.subscription.isSubscribed() } + addExtraProps(props) { + if (!withRef && !recomputationsProp) return props + // make a shallow copy so that fields added don't leak to the original selector. + // this is especially important for 'ref' since that's a reference back to the component + // instance. a singleton memoized selector would then be holding a reference to the + // instance, preventing the instance from being garbage collected, and that would be bad + const withExtras = { ...props } + if (withRef) withExtras.ref = this.setWrappedInstance + if (recomputationsProp) withExtras[recomputationsProp] = this.recomputations++ + return withExtras + } + render() { + this.lastRenderedProps = this.selector(this.props) + return createElement( WrappedComponent, - this.lastRenderedProps = this.selector(this.props) + this.addExtraProps(this.lastRenderedProps) ) } } diff --git a/src/selectors/getFinalProps.js b/src/selectors/getFinalProps.js index 7850994a8..958209784 100644 --- a/src/selectors/getFinalProps.js +++ b/src/selectors/getFinalProps.js @@ -41,7 +41,7 @@ export function createFinalPropsSelector(options) { : createImpureFinalPropsSelector(options) } -export function memoizeFinalPropsSelector(selector, enhance) { +export function memoizeFinalPropsSelector(selector) { let lastProps = undefined let lastResult = undefined @@ -51,7 +51,7 @@ export function memoizeFinalPropsSelector(selector, enhance) { // wrap the source selector in a shallow equals because props objects with same properties are // semantically equal to React... no need to return a new object. if (!lastProps || !shallowEqual(lastProps, nextProps)) { - lastResult = enhance(nextProps) + lastResult = nextProps } lastProps = nextProps From ee77f3f8bd74c4e92f8e65502fd1402a57d83283 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 23 Jun 2016 11:17:01 -0400 Subject: [PATCH 46/76] Refactor props memoization --- src/components/connectAdvanced.js | 4 ++-- src/selectors/getFinalProps.js | 35 +++++-------------------------- src/selectors/getOwnProps.js | 12 ++--------- src/utils/memoizeProps.js | 22 +++++++++++++++++++ 4 files changed, 31 insertions(+), 42 deletions(-) create mode 100644 src/utils/memoizeProps.js diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 9ac0f0086..0d0c1a234 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -2,7 +2,7 @@ import hoistStatics from 'hoist-non-react-statics' import invariant from 'invariant' import { Component, PropTypes, createElement } from 'react' -import { memoizeFinalPropsSelector } from '../selectors/getFinalProps' +import memoizeProps from '../utils/memoizeProps' import Subscription from '../utils/Subscription' import storeShape from '../utils/storeShape' @@ -133,7 +133,7 @@ export default function connectAdvanced( WrappedComponent }) - const memoizedSelector = memoizeFinalPropsSelector(sourceSelector) + const memoizedSelector = memoizeProps(sourceSelector) this.selector = function selector(ownProps) { const state = dependsOnState ? this.store.getState() : null diff --git a/src/selectors/getFinalProps.js b/src/selectors/getFinalProps.js index 958209784..67e211603 100644 --- a/src/selectors/getFinalProps.js +++ b/src/selectors/getFinalProps.js @@ -1,5 +1,3 @@ -import shallowEqual from '../utils/shallowEqual' - export function createImpureFinalPropsSelector({ getState, getDispatch, getOwnProps, mergeProps }) { return function impureSelector(state, props, dispatch) { return mergeProps( @@ -15,23 +13,18 @@ export function createPureFinalPropsSelector({ getState, getDispatch, getOwnProp let lastState = undefined let lastDispatch = undefined let lastMerged = undefined - let lastResult = undefined return function pureSelector(state, props, dispatch) { const nextOwn = getOwnProps(state, props, dispatch) const nextState = getState(state, props, dispatch) const nextDispatch = getDispatch(state, props, dispatch) if (lastOwn !== nextOwn || lastState !== nextState || lastDispatch !== nextDispatch) { - const nextMerged = mergeProps(nextState, nextDispatch, nextOwn) - if (!lastMerged || !shallowEqual(lastMerged, nextMerged)) { - lastResult = nextMerged - } - lastMerged = nextMerged + lastMerged = mergeProps(nextState, nextDispatch, nextOwn) + lastOwn = nextOwn + lastState = nextState + lastDispatch = nextDispatch } - lastOwn = nextOwn - lastState = nextState - lastDispatch = nextDispatch - return lastResult + return lastMerged } } @@ -40,21 +33,3 @@ export function createFinalPropsSelector(options) { ? createPureFinalPropsSelector(options) : createImpureFinalPropsSelector(options) } - -export function memoizeFinalPropsSelector(selector) { - let lastProps = undefined - let lastResult = undefined - - return function memoize(state, ownProps, dispatch) { - const nextProps = selector(state, ownProps, dispatch) - - // wrap the source selector in a shallow equals because props objects with same properties are - // semantically equal to React... no need to return a new object. - if (!lastProps || !shallowEqual(lastProps, nextProps)) { - lastResult = nextProps - } - lastProps = nextProps - - return lastResult - } -} diff --git a/src/selectors/getOwnProps.js b/src/selectors/getOwnProps.js index cdf670090..e73757664 100644 --- a/src/selectors/getOwnProps.js +++ b/src/selectors/getOwnProps.js @@ -1,19 +1,11 @@ -import shallowEqual from '../utils/shallowEqual' +import memoizeProps from '../utils/memoizeProps' export function impureOwnPropsSelector(_, props) { return props } export function createPureOwnPropsSelector() { - let lastProps = undefined - let lastResult = undefined - return function pureOwnPropsSelector(_, nextProps) { - if (!lastProps || !shallowEqual(lastProps, nextProps)) { - lastResult = nextProps - } - lastProps = nextProps - return lastResult - } + return memoizeProps((_, props) => props) } export function createOwnPropsSelector({ pure }) { diff --git a/src/utils/memoizeProps.js b/src/utils/memoizeProps.js new file mode 100644 index 000000000..f2151050d --- /dev/null +++ b/src/utils/memoizeProps.js @@ -0,0 +1,22 @@ +import shallowEqual from '../utils/shallowEqual' + +// wrap the source getProps func in a shallow equals because props objects with same properties are +// semantically equal in the eyes of React... no need to return a new object. +export default function memoizeProps(getProps) { + let lastValue = undefined + let lastResult = undefined + + return function memoize(...args) { + const nextValue = getProps(...args) + if (!lastValue) { + lastValue = nextValue + lastResult = nextValue + } else if (lastValue !== nextValue) { + if (!shallowEqual(lastValue, nextValue)) { + lastResult = nextValue + } + lastValue = nextValue + } + return lastResult + } +} From 8170f160d4246d5ece4c96eb87fba8541edf1860 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 23 Jun 2016 12:20:03 -0400 Subject: [PATCH 47/76] Move ownProps memoization into connectAdvanced to simplify selectors. --- src/components/connect.js | 2 -- src/components/connectAdvanced.js | 6 ++++-- src/selectors/createFactoryAwareSelector.js | 12 +++++------ src/selectors/createMatchingSelector.js | 4 ++-- src/selectors/getFinalProps.js | 8 +++---- src/selectors/getOwnProps.js | 20 ----------------- src/selectors/mapDispatch.js | 4 ++-- src/selectors/mapState.js | 4 ++-- src/utils/memoizeProps.js | 24 +++++++++------------ 9 files changed, 30 insertions(+), 54 deletions(-) delete mode 100644 src/selectors/getOwnProps.js diff --git a/src/components/connect.js b/src/components/connect.js index 9d3fbcc05..2ccaf41c9 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -3,7 +3,6 @@ import { flow } from 'lodash' import connectAdvanced from './connectAdvanced' import verifyPlainObject from '../utils/verifyPlainObject' import { createFinalPropsSelector } from '../selectors/getFinalProps' -import { addGetOwnProps } from '../selectors/getOwnProps' import { addGetDispatch, getDefaultMapDispatchFactories } from '../selectors/mapDispatch' import { addGetState, getDefaultMapStateFactories } from '../selectors/mapState' import { defaultMergeProps } from '../selectors/mergeProps' @@ -20,7 +19,6 @@ export function wrapWithVerify({ getState, getDispatch, mergeProps, ...options } export function selectorFactory(options) { return flow( - addGetOwnProps, addGetState, addGetDispatch, wrapWithVerify, diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 0d0c1a234..6815d20bf 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -133,11 +133,13 @@ export default function connectAdvanced( WrappedComponent }) - const memoizedSelector = memoizeProps(sourceSelector) + const memoizeOwn = pure ? memoizeProps() : (props => props) + const memoizeFinal = memoizeProps() this.selector = function selector(ownProps) { const state = dependsOnState ? this.store.getState() : null - return memoizedSelector(state, ownProps, this.store.dispatch) + const props = memoizeOwn(ownProps) + return memoizeFinal(sourceSelector(state, props, this.store.dispatch)) } } diff --git a/src/selectors/createFactoryAwareSelector.js b/src/selectors/createFactoryAwareSelector.js index 65b93a141..1f5d5f75c 100644 --- a/src/selectors/createFactoryAwareSelector.js +++ b/src/selectors/createFactoryAwareSelector.js @@ -24,18 +24,18 @@ export default function createMapOrMapFactoryProxy(mapToProps) { return proxy } -export function createImpureFactoryAwareSelector(getOwnProps, getStorePart, mapToProps) { +export function createImpureFactoryAwareSelector(getStorePart, mapToProps) { const map = createMapOrMapFactoryProxy(mapToProps) return function impureFactoryAwareSelector(state, props, dispatch) { return map( getStorePart(state, props, dispatch), - getOwnProps(state, props, dispatch) + props ) } } -export function createPureFactoryAwareSelector(getOwnProps, getStorePart, mapToProps) { +export function createPureFactoryAwareSelector(getStorePart, mapToProps) { const map = createMapOrMapFactoryProxy(mapToProps) const noProps = {} let lastStorePart = undefined @@ -44,7 +44,7 @@ export function createPureFactoryAwareSelector(getOwnProps, getStorePart, mapToP return function pureFactoryAwareSelector(state, props, dispatch) { const nextStorePart = getStorePart(state, props, dispatch) - const nextProps = map.dependsOnProps() ? getOwnProps(state, props, dispatch) : noProps + const nextProps = map.dependsOnProps() ? props : noProps if (lastStorePart !== nextStorePart || lastProps !== nextProps) { const nextResult = map(nextStorePart, nextProps) @@ -59,7 +59,7 @@ export function createPureFactoryAwareSelector(getOwnProps, getStorePart, mapToP } } -export default function createFactoryAwareSelector(pure, getOwnProps, getStorePart, mapToProps) { +export default function createFactoryAwareSelector(pure, getStorePart, mapToProps) { const create = pure ? createPureFactoryAwareSelector : createImpureFactoryAwareSelector - return create(getOwnProps, getStorePart, mapToProps) + return create(getStorePart, mapToProps) } diff --git a/src/selectors/createMatchingSelector.js b/src/selectors/createMatchingSelector.js index cf6e1f7c9..4756fb794 100644 --- a/src/selectors/createMatchingSelector.js +++ b/src/selectors/createMatchingSelector.js @@ -1,6 +1,6 @@ -export default function createMatchingSelector(factories, options, getOwnProps) { +export default function createMatchingSelector(factories, options) { for (let i = factories.length - 1; i >= 0; i--) { - const selector = factories[i](options, getOwnProps) + const selector = factories[i](options) if (selector) return selector } diff --git a/src/selectors/getFinalProps.js b/src/selectors/getFinalProps.js index 67e211603..6e3c8ed0c 100644 --- a/src/selectors/getFinalProps.js +++ b/src/selectors/getFinalProps.js @@ -1,20 +1,20 @@ -export function createImpureFinalPropsSelector({ getState, getDispatch, getOwnProps, mergeProps }) { +export function createImpureFinalPropsSelector({ getState, getDispatch, mergeProps }) { return function impureSelector(state, props, dispatch) { return mergeProps( getState(state, props, dispatch), getDispatch(state, props, dispatch), - getOwnProps(state, props, dispatch) + props ) } } -export function createPureFinalPropsSelector({ getState, getDispatch, getOwnProps, mergeProps }) { +export function createPureFinalPropsSelector({ getState, getDispatch, mergeProps }) { let lastOwn = undefined let lastState = undefined let lastDispatch = undefined let lastMerged = undefined return function pureSelector(state, props, dispatch) { - const nextOwn = getOwnProps(state, props, dispatch) + const nextOwn = props const nextState = getState(state, props, dispatch) const nextDispatch = getDispatch(state, props, dispatch) diff --git a/src/selectors/getOwnProps.js b/src/selectors/getOwnProps.js deleted file mode 100644 index e73757664..000000000 --- a/src/selectors/getOwnProps.js +++ /dev/null @@ -1,20 +0,0 @@ -import memoizeProps from '../utils/memoizeProps' - -export function impureOwnPropsSelector(_, props) { - return props -} - -export function createPureOwnPropsSelector() { - return memoizeProps((_, props) => props) -} - -export function createOwnPropsSelector({ pure }) { - return pure - ? createPureOwnPropsSelector() - : impureOwnPropsSelector -} - -export function addGetOwnProps(options) { - const getOwnProps = createOwnPropsSelector(options) - return { getOwnProps, ...options } -} diff --git a/src/selectors/mapDispatch.js b/src/selectors/mapDispatch.js index 436d6ef7f..8514b4a5a 100644 --- a/src/selectors/mapDispatch.js +++ b/src/selectors/mapDispatch.js @@ -17,9 +17,9 @@ export function whenMapDispatchIsObject({ mapDispatchToProps, dispatch }) { } } -export function whenMapDispatchIsFunction({ mapDispatchToProps, pure, dispatch, getOwnProps }) { +export function whenMapDispatchIsFunction({ mapDispatchToProps, pure, dispatch }) { if (typeof mapDispatchToProps === 'function') { - return createFactoryAwareSelector(pure, getOwnProps, () => dispatch, mapDispatchToProps) + return createFactoryAwareSelector(pure, () => dispatch, mapDispatchToProps) } } diff --git a/src/selectors/mapState.js b/src/selectors/mapState.js index 670b37e46..126e87cdb 100644 --- a/src/selectors/mapState.js +++ b/src/selectors/mapState.js @@ -8,9 +8,9 @@ export function whenMapStateIsMissing({ mapStateToProps }) { } } -export function whenMapStateIsFunction({ mapStateToProps, pure, getOwnProps }) { +export function whenMapStateIsFunction({ mapStateToProps, pure }) { if (typeof mapStateToProps === 'function') { - return createFactoryAwareSelector(pure, getOwnProps, state => state, mapStateToProps) + return createFactoryAwareSelector(pure, state => state, mapStateToProps) } } diff --git a/src/utils/memoizeProps.js b/src/utils/memoizeProps.js index f2151050d..d5b293e50 100644 --- a/src/utils/memoizeProps.js +++ b/src/utils/memoizeProps.js @@ -1,22 +1,18 @@ import shallowEqual from '../utils/shallowEqual' -// wrap the source getProps func in a shallow equals because props objects with same properties are +// wrap the source props in a shallow equals because props objects with same properties are // semantically equal in the eyes of React... no need to return a new object. -export default function memoizeProps(getProps) { - let lastValue = undefined - let lastResult = undefined +export default function memoizeProps() { + let lastProps = undefined + let result = undefined - return function memoize(...args) { - const nextValue = getProps(...args) - if (!lastValue) { - lastValue = nextValue - lastResult = nextValue - } else if (lastValue !== nextValue) { - if (!shallowEqual(lastValue, nextValue)) { - lastResult = nextValue + return function memoize(nextProps) { + if (lastProps !== nextProps) { + if (!lastProps || !shallowEqual(lastProps, nextProps)) { + result = nextProps } - lastValue = nextValue + lastProps = nextProps } - return lastResult + return result } } From 3287546d28e2d88f97218f1aa03e6db522fef749 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 23 Jun 2016 12:30:25 -0400 Subject: [PATCH 48/76] utilize memoizeProps in createFactoryAwareSelector --- src/selectors/createFactoryAwareSelector.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/selectors/createFactoryAwareSelector.js b/src/selectors/createFactoryAwareSelector.js index 1f5d5f75c..1590b3a22 100644 --- a/src/selectors/createFactoryAwareSelector.js +++ b/src/selectors/createFactoryAwareSelector.js @@ -1,4 +1,4 @@ -import shallowEqual from '../utils/shallowEqual' +import memoizeProps from '../utils/memoizeProps' // factory detection. if the first result of mapToProps is a function, use that as the // true mapToProps @@ -37,25 +37,22 @@ export function createImpureFactoryAwareSelector(getStorePart, mapToProps) { export function createPureFactoryAwareSelector(getStorePart, mapToProps) { const map = createMapOrMapFactoryProxy(mapToProps) + const memoizeMapResult = memoizeProps() const noProps = {} let lastStorePart = undefined let lastProps = undefined - let lastResult = undefined + let result = undefined return function pureFactoryAwareSelector(state, props, dispatch) { const nextStorePart = getStorePart(state, props, dispatch) const nextProps = map.dependsOnProps() ? props : noProps if (lastStorePart !== nextStorePart || lastProps !== nextProps) { - const nextResult = map(nextStorePart, nextProps) - - if (!lastResult || !shallowEqual(lastResult, nextResult)) { - lastResult = nextResult - } + result = memoizeMapResult(map(nextStorePart, nextProps)) + lastStorePart = nextStorePart + lastProps = nextProps } - lastStorePart = nextStorePart - lastProps = nextProps - return lastResult + return result } } From 0b9fd955d5dd6954050b1a672e05fc20b10b4e74 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 23 Jun 2016 22:51:35 -0400 Subject: [PATCH 49/76] rename recomputations to renderCount to make its intent clearer --- src/components/connectAdvanced.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 6815d20bf..5648e22d3 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -38,9 +38,8 @@ export default function connectAdvanced( pure = true, // if defined, the name of the property passed to the wrapped element indicating the number of - // recomputations since it was mounted. useful for watching in react devtools for unnecessary - // re-renders. - recomputationsProp = undefined, + // calls to render. useful for watching in react devtools for unnecessary re-renders. + renderCountProp = undefined, // the key of props/context to get the store storeKey = 'store', @@ -61,7 +60,7 @@ export default function connectAdvanced( this.version = version this.state = {} - this.recomputations = 0 + this.renderCount = 0 this.store = this.props[storeKey] || this.context[storeKey] this.parentSub = this.props[subscriptionKey] || this.context[subscriptionKey] this.setWrappedInstance = this.setWrappedInstance.bind(this) @@ -164,14 +163,14 @@ export default function connectAdvanced( } addExtraProps(props) { - if (!withRef && !recomputationsProp) return props + if (!withRef && !renderCountProp) return props // make a shallow copy so that fields added don't leak to the original selector. // this is especially important for 'ref' since that's a reference back to the component // instance. a singleton memoized selector would then be holding a reference to the // instance, preventing the instance from being garbage collected, and that would be bad const withExtras = { ...props } if (withRef) withExtras.ref = this.setWrappedInstance - if (recomputationsProp) withExtras[recomputationsProp] = this.recomputations++ + if (renderCountProp) withExtras[renderCountProp] = this.renderCount++ return withExtras } From 30857f4e23e7c75ea7737132ac3ac7982171c8c5 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Fri, 24 Jun 2016 08:09:22 -0400 Subject: [PATCH 50/76] refactoring selectors + connect for clarity --- src/components/connect.js | 29 ++++++++++++++++----- src/selectors/createFactoryAwareSelector.js | 2 +- src/selectors/createMatchingSelector.js | 8 ------ src/selectors/mapDispatch.js | 23 ++++------------ src/selectors/mapState.js | 20 +++----------- 5 files changed, 32 insertions(+), 50 deletions(-) delete mode 100644 src/selectors/createMatchingSelector.js diff --git a/src/components/connect.js b/src/components/connect.js index 2ccaf41c9..689f1ef4a 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -1,12 +1,28 @@ -import { flow } from 'lodash' +import flow from 'lodash/flow' import connectAdvanced from './connectAdvanced' import verifyPlainObject from '../utils/verifyPlainObject' import { createFinalPropsSelector } from '../selectors/getFinalProps' -import { addGetDispatch, getDefaultMapDispatchFactories } from '../selectors/mapDispatch' -import { addGetState, getDefaultMapStateFactories } from '../selectors/mapState' +import defaultMapDispatchFactories from '../selectors/mapDispatch' +import defaultMapStateFactories from '../selectors/mapState' import { defaultMergeProps } from '../selectors/mergeProps' +export function addStateAndDispatchSelectors(options) { + function match(factories) { + for (let i = factories.length - 1; i >= 0; i--) { + const selector = factories[i](options) + if (selector) return selector + } + return undefined + } + + return { + ...options, + getState: match(options.mapStateFactories), + getDispatch: match(options.mapDispatchFactories) + } +} + export function wrapWithVerify({ getState, getDispatch, mergeProps, ...options }) { const verify = (methodName, func) => verifyPlainObject(options.displayName, methodName, func) return { @@ -19,8 +35,7 @@ export function wrapWithVerify({ getState, getDispatch, mergeProps, ...options } export function selectorFactory(options) { return flow( - addGetState, - addGetDispatch, + addStateAndDispatchSelectors, wrapWithVerify, createFinalPropsSelector )(options) @@ -29,8 +44,8 @@ export function selectorFactory(options) { export function buildOptions(mapStateToProps, mapDispatchToProps, mergeProps, options) { return { getDisplayName: name => `Connect(${name})`, - mapDispatchFactories: getDefaultMapDispatchFactories(), - mapStateFactories: getDefaultMapStateFactories(), + mapDispatchFactories: defaultMapDispatchFactories, + mapStateFactories: defaultMapStateFactories, ...options, mapStateToProps, mapDispatchToProps, diff --git a/src/selectors/createFactoryAwareSelector.js b/src/selectors/createFactoryAwareSelector.js index 1590b3a22..301ae23c4 100644 --- a/src/selectors/createFactoryAwareSelector.js +++ b/src/selectors/createFactoryAwareSelector.js @@ -2,7 +2,7 @@ import memoizeProps from '../utils/memoizeProps' // factory detection. if the first result of mapToProps is a function, use that as the // true mapToProps -export default function createMapOrMapFactoryProxy(mapToProps) { +export function createMapOrMapFactoryProxy(mapToProps) { let map = undefined function firstRun(storePart, props) { const result = mapToProps(storePart, props) diff --git a/src/selectors/createMatchingSelector.js b/src/selectors/createMatchingSelector.js deleted file mode 100644 index 4756fb794..000000000 --- a/src/selectors/createMatchingSelector.js +++ /dev/null @@ -1,8 +0,0 @@ -export default function createMatchingSelector(factories, options) { - for (let i = factories.length - 1; i >= 0; i--) { - const selector = factories[i](options) - if (selector) return selector - } - - return undefined -} diff --git a/src/selectors/mapDispatch.js b/src/selectors/mapDispatch.js index 8514b4a5a..811ffe04b 100644 --- a/src/selectors/mapDispatch.js +++ b/src/selectors/mapDispatch.js @@ -1,7 +1,5 @@ import { bindActionCreators } from 'redux' - import createFactoryAwareSelector from './createFactoryAwareSelector' -import createMatchingSelector from '../selectors/createMatchingSelector' export function whenMapDispatchIsMissing({ mapDispatchToProps, dispatch }) { if (!mapDispatchToProps) { @@ -23,19 +21,8 @@ export function whenMapDispatchIsFunction({ mapDispatchToProps, pure, dispatch } } } -export function getDefaultMapDispatchFactories() { - return [ - whenMapDispatchIsMissing, - whenMapDispatchIsFunction, - whenMapDispatchIsObject - ] -} - -export function createMapDispatchSelector(options) { - return createMatchingSelector(options.mapDispatchFactories, options) -} - -export function addGetDispatch(options) { - const getDispatch = createMapDispatchSelector(options) - return { getDispatch, ...options } -} +export default [ + whenMapDispatchIsMissing, + whenMapDispatchIsFunction, + whenMapDispatchIsObject +] diff --git a/src/selectors/mapState.js b/src/selectors/mapState.js index 126e87cdb..33f4ac0ec 100644 --- a/src/selectors/mapState.js +++ b/src/selectors/mapState.js @@ -1,5 +1,4 @@ import createFactoryAwareSelector from './createFactoryAwareSelector' -import createMatchingSelector from '../selectors/createMatchingSelector' export function whenMapStateIsMissing({ mapStateToProps }) { if (!mapStateToProps) { @@ -14,18 +13,7 @@ export function whenMapStateIsFunction({ mapStateToProps, pure }) { } } -export function getDefaultMapStateFactories() { - return [ - whenMapStateIsMissing, - whenMapStateIsFunction - ] -} - -export function createMapStateSelector(options) { - return createMatchingSelector(options.mapStateFactories, options) -} - -export function addGetState(options) { - const getState = createMapStateSelector(options) - return { getState, ...options } -} +export default [ + whenMapStateIsMissing, + whenMapStateIsFunction +] From 1e0a66ceccceb1fe46e253a1484d7764b1f9845c Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Fri, 24 Jun 2016 14:27:55 -0400 Subject: [PATCH 51/76] Hide connectAdvanced and advancedOptions --- src/components/connect.js | 5 +++-- src/index.js | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/connect.js b/src/components/connect.js index 689f1ef4a..9857623c4 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -41,12 +41,13 @@ export function selectorFactory(options) { )(options) } -export function buildOptions(mapStateToProps, mapDispatchToProps, mergeProps, options) { +export function buildOptions(mapStateToProps, mapDispatchToProps, mergeProps, { pure, withRef } = {}) { return { getDisplayName: name => `Connect(${name})`, mapDispatchFactories: defaultMapDispatchFactories, mapStateFactories: defaultMapStateFactories, - ...options, + withRef, + pure, mapStateToProps, mapDispatchToProps, mergeProps: mergeProps || defaultMergeProps, diff --git a/src/index.js b/src/index.js index 6e4773373..ad89eec2d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,4 @@ import Provider from './components/Provider' import connect from './components/connect' -import connectAdvanced from './components/connectAdvanced' -export { Provider, connect, connectAdvanced } +export { Provider, connect } From 5bc7b83c7f5bcd5fa2460432f47a4da5dba3cf95 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sun, 26 Jun 2016 13:54:44 -0400 Subject: [PATCH 52/76] fix nits --- src/utils/Subscription.js | 2 +- src/utils/memoizeProps.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/Subscription.js b/src/utils/Subscription.js index 399da51bc..a10f9963d 100644 --- a/src/utils/Subscription.js +++ b/src/utils/Subscription.js @@ -1,5 +1,5 @@ -// enapsulates the subscription logic for connecting a component to the redux store, as well as +// encapsulates the subscription logic for connecting a component to the redux store, as well as // nesting subscriptions of decendant components, so that we can ensure the ancestor components // re-render before descendants export default class Subscription { diff --git a/src/utils/memoizeProps.js b/src/utils/memoizeProps.js index d5b293e50..7de68ef90 100644 --- a/src/utils/memoizeProps.js +++ b/src/utils/memoizeProps.js @@ -1,4 +1,4 @@ -import shallowEqual from '../utils/shallowEqual' +import shallowEqual from '../shallowEqual' // wrap the source props in a shallow equals because props objects with same properties are // semantically equal in the eyes of React... no need to return a new object. From 1e7f2d75bb9b780c66d1c3b05ade5558e01bf91e Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sun, 26 Jun 2016 16:55:24 -0400 Subject: [PATCH 53/76] fix messed up path --- src/utils/memoizeProps.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/memoizeProps.js b/src/utils/memoizeProps.js index 7de68ef90..a7d18a664 100644 --- a/src/utils/memoizeProps.js +++ b/src/utils/memoizeProps.js @@ -1,4 +1,4 @@ -import shallowEqual from '../shallowEqual' +import shallowEqual from './shallowEqual' // wrap the source props in a shallow equals because props objects with same properties are // semantically equal in the eyes of React... no need to return a new object. From e284d8b561917dd7b22d8d03bce92ddce08afd41 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 2 Jul 2016 17:54:43 -0400 Subject: [PATCH 54/76] Add guard for the case of a component causing its siblings to unsubscribe in the middle of the notification loop. --- src/utils/Subscription.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/Subscription.js b/src/utils/Subscription.js index a10f9963d..7012cca9a 100644 --- a/src/utils/Subscription.js +++ b/src/utils/Subscription.js @@ -35,7 +35,8 @@ export default class Subscription { notifyNestedSubs() { const keys = Object.keys(this.nestedSubs) for (let i = 0; i < keys.length; i++) { - this.nestedSubs[keys[i]]() + const sub = this.nestedSubs[keys[i]] + if (sub) sub() } } From 395c4ee5ef6471430b318d4b2d13f1cde9b75f53 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 2 Jul 2016 20:05:05 -0400 Subject: [PATCH 55/76] Add comments to connect.js --- src/components/connect.js | 96 +++++++++++++++++++++++++++++++++------ 1 file changed, 81 insertions(+), 15 deletions(-) diff --git a/src/components/connect.js b/src/components/connect.js index 9857623c4..e93aed78a 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -7,6 +7,87 @@ import defaultMapDispatchFactories from '../selectors/mapDispatch' import defaultMapStateFactories from '../selectors/mapState' import { defaultMergeProps } from '../selectors/mergeProps' +/* + connect combines mapStateToProps, mapDispatchToProps, and mergeProps into a final selector that + is compatible with connectAdvanced. The functions in the selectors folder are the individual + pieces of that final selector. + + First, buildOptions combines its args with some meta into an options object that's passed to + connectAdvanced, which will pass a modified version of that options object to selectorFactory. + + options modifications: + added: dispatch, displayName, WrappedComponent + removed: getDisplayName, renderCountProp + + Each time selectorFactory is called (whenever an instance of the component in connectAdvanced is + constructed or hot reloaded), it uses the modified options object to build a selector function: + + 1. Create the getState selector by matching mapStateToProps to the mapStateFactories array + passed in from buildOptions. + + The default behaviors (from mapState.js) check mapStateToProps for a function or missing + value. This could be overridden by supplying a custom value to the mapStateFactories + property of connect's options argument + + 2. Create the getDispatch selector by matching mapDispatchToProps to the mapDispatchFactories + array passed in from buildOptions. + + The default behaviors (from mapDispatch.js) check mapDispatchToProps for a function, object, + or missing value. The could be overridden by supplying a custom value to the + mapDispatchFactories property of connect's options argument. + + 3. Wrap the getState, getDispatch, and mergeProps selectors in functions that check that their + return values are plain objects (on their first invocation.) + + This is to inform the developer that they made a mistake coding the function the supplied + for any of mapStateToProps, mapDispatchToProps, or mergeProps. + + 4. Combine getState, getDispatch, and mergeProps selectors into the final props selector, by + passing the getState's results, getDispatch's results, and original props to mergeProps. + (See getFinalProps.js) + + The resulting final props selector is called by the component instance whenever it receives new + props or is notified by the store subscription. + */ + +export function buildOptions(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { + return { + // used to compute the Connect component's displayName from the wrapped component's displayName. + getDisplayName: name => `Connect(${name})`, + + // passed through to selectorFactory. defaults to the array of funcs returned in mapDispatch.js + // that determine the appropriate sub-selector to use for mapDispatchToProps, depending on + // whether it's a function, object, or missing. + mapDispatchFactories: defaultMapDispatchFactories, + + // passed through to selectorFactory. defaults to the array of funcs returned in mapState.js + // that determine the appropriate sub-selector to use for mapStateToProps, depending on + // whether it's a function or missing. + mapStateFactories: defaultMapStateFactories, + + // in addition to setting withRef, pure, storeKey, and renderCountProp, options can override + // getDisplayName, mapDispatchFactories, or mapStateFactories. + // TODO: REPLACE WITH ...OPTIONS ONCE IT'S OK TO EXPOSE NEW FUNCTIONALITY. + withRef: options.withRef, pure: options.pure, + + // passed through to selectorFactory + mapStateToProps, + + // passed through to selectorFactory + mapDispatchToProps, + + // passed through to selectorFactory + mergeProps: mergeProps || defaultMergeProps, + + // used in error messages + methodName: 'connect', + + // if mapStateToProps is not given a value, the Connect component doesn't subscribe to the store + // or pass state to the selector returned by selectorFactory. + dependsOnState: Boolean(mapStateToProps) + } +} + export function addStateAndDispatchSelectors(options) { function match(factories) { for (let i = factories.length - 1; i >= 0; i--) { @@ -41,21 +122,6 @@ export function selectorFactory(options) { )(options) } -export function buildOptions(mapStateToProps, mapDispatchToProps, mergeProps, { pure, withRef } = {}) { - return { - getDisplayName: name => `Connect(${name})`, - mapDispatchFactories: defaultMapDispatchFactories, - mapStateFactories: defaultMapStateFactories, - withRef, - pure, - mapStateToProps, - mapDispatchToProps, - mergeProps: mergeProps || defaultMergeProps, - methodName: 'connect', - dependsOnState: Boolean(mapStateToProps) - } -} - export default function connect(...args) { const options = buildOptions(...args) return connectAdvanced(selectorFactory, options) From dab9c85b4b6480f9962688c3ba9ec426508d2879 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Fri, 8 Jul 2016 21:52:47 -0400 Subject: [PATCH 56/76] refactoring + some perf opimizations --- src/components/connect.js | 17 ++++++- src/components/connectAdvanced.js | 53 ++++++++++----------- src/selectors/createFactoryAwareSelector.js | 43 +++++++++-------- src/selectors/getFinalProps.js | 20 ++++++-- src/utils/Subscription.js | 27 ++++++----- src/utils/memoizeProps.js | 3 +- src/utils/shallowEqual.js | 28 +++++------ 7 files changed, 103 insertions(+), 88 deletions(-) diff --git a/src/components/connect.js b/src/components/connect.js index e93aed78a..ca69cef0d 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -50,7 +50,14 @@ import { defaultMergeProps } from '../selectors/mergeProps' props or is notified by the store subscription. */ -export function buildOptions(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { +export function buildOptions( + mapStateToProps, + mapDispatchToProps, + mergeProps, + { + pure = true, + withRef // ...options + } = {}) { return { // used to compute the Connect component's displayName from the wrapped component's displayName. getDisplayName: name => `Connect(${name})`, @@ -65,10 +72,16 @@ export function buildOptions(mapStateToProps, mapDispatchToProps, mergeProps, op // whether it's a function or missing. mapStateFactories: defaultMapStateFactories, + // if true, the selector returned by selectorFactory will memoize its results, allowing + // connectAdvanced's shouldComponentUpdate to return false if final props have not changed. + // if false, the selector will always return a new object and shouldComponentUpdate will always + // return true. + pure, + // in addition to setting withRef, pure, storeKey, and renderCountProp, options can override // getDisplayName, mapDispatchFactories, or mapStateFactories. // TODO: REPLACE WITH ...OPTIONS ONCE IT'S OK TO EXPOSE NEW FUNCTIONALITY. - withRef: options.withRef, pure: options.pure, + withRef, // passed through to selectorFactory mapStateToProps, diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 5648e22d3..f9e44d11a 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -2,7 +2,6 @@ import hoistStatics from 'hoist-non-react-statics' import invariant from 'invariant' import { Component, PropTypes, createElement } from 'react' -import memoizeProps from '../utils/memoizeProps' import Subscription from '../utils/Subscription' import storeShape from '../utils/storeShape' @@ -33,10 +32,6 @@ export default function connectAdvanced( // probably overridden by wrapper functions such as connect() methodName = 'connectAdvanced', - // if true, shouldComponentUpdate will only be true of the selector recomputes for nextProps. - // if false, shouldComponentUpdate will always be true. - pure = true, - // if defined, the name of the property passed to the wrapped element indicating the number of // calls to render. useful for watching in react devtools for unnecessary re-renders. renderCountProp = undefined, @@ -84,16 +79,18 @@ export default function connectAdvanced( if (!dependsOnState) return this.subscription.trySubscribe() - // check for recomputations that happened after this component has rendered, such as // when a child component dispatches an action in its componentWillMount - if (this.lastRenderedProps !== this.selector(this.props)) { - this.forceUpdate() - } + this.runSelector(this.props) + if (this.shouldComponentUpdate()) this.forceUpdate() } - shouldComponentUpdate(nextProps) { - return !pure || this.lastRenderedProps !== this.selector(nextProps) + componentWillReceiveProps(nextProps) { + this.runSelector(nextProps) + } + + shouldComponentUpdate() { + return this.lastRenderedProps !== this.nextRenderedProps } componentWillUnmount() { @@ -103,7 +100,7 @@ export default function connectAdvanced( this.subscription = { isSubscribed: () => false } this.store = null this.parentSub = null - this.selector = () => this.lastRenderedProps + this.runSelector = () => {} } getWrappedInstance() { @@ -113,16 +110,15 @@ export default function connectAdvanced( ) return this.wrappedInstance } + setWrappedInstance(ref) { this.wrappedInstance = ref } initSelector() { - this.lastRenderedProps = null - - const sourceSelector = selectorFactory({ + const selector = selectorFactory({ // most options passed to connectAdvanced are passed along to the selectorFactory - dependsOnState, methodName, pure, storeKey, withRef, ...connectOptions, + dependsOnState, methodName, storeKey, withRef, ...connectOptions, // useful for factories that want to bind action creators outside the selector dispatch: this.store.dispatch, // useful for error messages @@ -132,30 +128,29 @@ export default function connectAdvanced( WrappedComponent }) - const memoizeOwn = pure ? memoizeProps() : (props => props) - const memoizeFinal = memoizeProps() - - this.selector = function selector(ownProps) { + this.runSelector = function runSelector(ownProps) { const state = dependsOnState ? this.store.getState() : null - const props = memoizeOwn(ownProps) - return memoizeFinal(sourceSelector(state, props, this.store.dispatch)) + this.nextRenderedProps = selector(state, ownProps, this.store.dispatch) } + this.lastRenderedProps = null + this.runSelector(this.props) } initSubscription() { function onStoreStateChange(notifyNestedSubs) { - if (dependsOnState && this.shouldComponentUpdate(this.props)) { + this.runSelector(this.props) + if (this.shouldComponentUpdate()) { this.setState({}, notifyNestedSubs) } else { notifyNestedSubs() } } - this.subscription = new Subscription( - this.store, - this.parentSub, - onStoreStateChange.bind(this) - ) + const onChange = dependsOnState + ? onStoreStateChange.bind(this) + : (notifyNestedSubs => notifyNestedSubs()) + + this.subscription = new Subscription(this.store, this.parentSub, onChange) } isSubscribed() { @@ -175,7 +170,7 @@ export default function connectAdvanced( } render() { - this.lastRenderedProps = this.selector(this.props) + this.lastRenderedProps = this.nextRenderedProps return createElement( WrappedComponent, diff --git a/src/selectors/createFactoryAwareSelector.js b/src/selectors/createFactoryAwareSelector.js index 301ae23c4..542103e4e 100644 --- a/src/selectors/createFactoryAwareSelector.js +++ b/src/selectors/createFactoryAwareSelector.js @@ -1,34 +1,32 @@ -import memoizeProps from '../utils/memoizeProps' // factory detection. if the first result of mapToProps is a function, use that as the // true mapToProps export function createMapOrMapFactoryProxy(mapToProps) { - let map = undefined + const proxy = { + mapToProps: firstRun, + dependsOnProps: mapToProps.length !== 1 + } + function firstRun(storePart, props) { - const result = mapToProps(storePart, props) - if (typeof result === 'function') { - map = result - return map(storePart, props) + const firstResult = mapToProps(storePart, props) + if (typeof firstResult === 'function') { + proxy.mapToProps = firstResult + proxy.dependsOnProps = firstResult.length !== 1 + return firstResult(storePart, props) } else { - map = mapToProps - return result + proxy.mapToProps = mapToProps + return firstResult } } - function proxy(storePart, props) { - return (map || firstRun)(storePart, props) - } - proxy.dependsOnProps = function dependsOnProps() { - return (map || mapToProps).length !== 1 - } return proxy } export function createImpureFactoryAwareSelector(getStorePart, mapToProps) { - const map = createMapOrMapFactoryProxy(mapToProps) + const proxy = createMapOrMapFactoryProxy(mapToProps) return function impureFactoryAwareSelector(state, props, dispatch) { - return map( + return proxy.mapToProps( getStorePart(state, props, dispatch), props ) @@ -36,24 +34,27 @@ export function createImpureFactoryAwareSelector(getStorePart, mapToProps) { } export function createPureFactoryAwareSelector(getStorePart, mapToProps) { - const map = createMapOrMapFactoryProxy(mapToProps) - const memoizeMapResult = memoizeProps() + const proxy = createMapOrMapFactoryProxy(mapToProps) const noProps = {} let lastStorePart = undefined let lastProps = undefined let result = undefined - return function pureFactoryAwareSelector(state, props, dispatch) { + function pureFactoryAwareSelector(state, props, dispatch) { const nextStorePart = getStorePart(state, props, dispatch) - const nextProps = map.dependsOnProps() ? props : noProps + const nextProps = proxy.dependsOnProps ? props : noProps if (lastStorePart !== nextStorePart || lastProps !== nextProps) { - result = memoizeMapResult(map(nextStorePart, nextProps)) + result = proxy.mapToProps(nextStorePart, nextProps) lastStorePart = nextStorePart lastProps = nextProps + pureFactoryAwareSelector.dependsOnProps = proxy.dependsOnProps } return result } + + pureFactoryAwareSelector.dependsOnProps = proxy.dependsOnProps + return pureFactoryAwareSelector } export default function createFactoryAwareSelector(pure, getStorePart, mapToProps) { diff --git a/src/selectors/getFinalProps.js b/src/selectors/getFinalProps.js index 6e3c8ed0c..18f10596c 100644 --- a/src/selectors/getFinalProps.js +++ b/src/selectors/getFinalProps.js @@ -1,3 +1,5 @@ +import memoizeProps from '../utils/memoizeProps' + export function createImpureFinalPropsSelector({ getState, getDispatch, mergeProps }) { return function impureSelector(state, props, dispatch) { return mergeProps( @@ -9,21 +11,29 @@ export function createImpureFinalPropsSelector({ getState, getDispatch, mergePro } export function createPureFinalPropsSelector({ getState, getDispatch, mergeProps }) { + const memoizeOwn = memoizeProps() + const memoizeGetState = memoizeProps() + const memoizeGetDispatch = memoizeProps() + const memoizeFinal = memoizeProps() let lastOwn = undefined let lastState = undefined let lastDispatch = undefined let lastMerged = undefined + return function pureSelector(state, props, dispatch) { - const nextOwn = props - const nextState = getState(state, props, dispatch) - const nextDispatch = getDispatch(state, props, dispatch) + const nextOwn = memoizeOwn(props) + const nextState = memoizeGetState(getState(state, nextOwn, dispatch)) + const nextDispatch = memoizeGetDispatch(getDispatch(state, nextOwn, dispatch)) - if (lastOwn !== nextOwn || lastState !== nextState || lastDispatch !== nextDispatch) { - lastMerged = mergeProps(nextState, nextDispatch, nextOwn) + if (lastOwn !== nextOwn + || lastState !== nextState + || lastDispatch !== nextDispatch) { + lastMerged = memoizeFinal(mergeProps(nextState, nextDispatch, nextOwn)) lastOwn = nextOwn lastState = nextState lastDispatch = nextDispatch } + return lastMerged } } diff --git a/src/utils/Subscription.js b/src/utils/Subscription.js index 7012cca9a..093519a17 100644 --- a/src/utils/Subscription.js +++ b/src/utils/Subscription.js @@ -1,7 +1,7 @@ - // encapsulates the subscription logic for connecting a component to the redux store, as well as // nesting subscriptions of decendant components, so that we can ensure the ancestor components // re-render before descendants + export default class Subscription { constructor(store, parentSub, onStateChange) { this.subscribe = parentSub @@ -9,22 +9,24 @@ export default class Subscription { : store.subscribe this.onStateChange = onStateChange - this.lastNestedSubId = 0 this.unsubscribe = null - this.nestedSubs = {} - + this.nestedSubs = [] this.notifyNestedSubs = this.notifyNestedSubs.bind(this) } addNestedSub(listener) { this.trySubscribe() + this.nestedSubs = this.nestedSubs.concat(listener) - const id = this.lastNestedSubId++ - this.nestedSubs[id] = listener + let subscribed = true return () => { - if (this.nestedSubs[id]) { - delete this.nestedSubs[id] - } + if (!subscribed) return + subscribed = false + + const subs = this.nestedSubs.slice() + const index = subs.indexOf(listener) + subs.splice(index, 1) + this.nestedSubs = subs } } @@ -33,10 +35,9 @@ export default class Subscription { } notifyNestedSubs() { - const keys = Object.keys(this.nestedSubs) - for (let i = 0; i < keys.length; i++) { - const sub = this.nestedSubs[keys[i]] - if (sub) sub() + const subs = this.nestedSubs + for (let i = subs.length - 1; i >= 0; i--) { + subs[i]() } } diff --git a/src/utils/memoizeProps.js b/src/utils/memoizeProps.js index a7d18a664..769950908 100644 --- a/src/utils/memoizeProps.js +++ b/src/utils/memoizeProps.js @@ -1,5 +1,6 @@ import shallowEqual from './shallowEqual' +const equal = shallowEqual // wrap the source props in a shallow equals because props objects with same properties are // semantically equal in the eyes of React... no need to return a new object. export default function memoizeProps() { @@ -8,7 +9,7 @@ export default function memoizeProps() { return function memoize(nextProps) { if (lastProps !== nextProps) { - if (!lastProps || !shallowEqual(lastProps, nextProps)) { + if (!lastProps || !equal(lastProps, nextProps)) { result = nextProps } lastProps = nextProps diff --git a/src/utils/shallowEqual.js b/src/utils/shallowEqual.js index 5a5bc4185..9980e83f5 100644 --- a/src/utils/shallowEqual.js +++ b/src/utils/shallowEqual.js @@ -1,25 +1,19 @@ const hasOwn = Object.prototype.hasOwnProperty -export default function shallowEqual(objA, objB) { - if (objA === objB) { - return true - } - - const keysA = Object.keys(objA) - const keysB = Object.keys(objB) - const lengthA = keysA.length +export default function shallowEqual(a, b) { + if (a === b) return true - if (lengthA !== keysB.length) { - return false + let countA = 0 + let countB = 0 + + for (let key in a) { + if (hasOwn.call(a, key) && a[key] !== b[key]) return false + countA++ } - // Test for A's keys different from B. - for (let i = 0; i < lengthA; i++) { - const key = keysA[i] - if (!hasOwn.call(objB, key) || objA[key] !== objB[key]) { - return false - } + for (let key in b) { + if (hasOwn.call(b, key)) countB++ } - return true + return countA === countB } From cabd376b28c996c4dd6cfccd9b21459b437ac808 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 9 Jul 2016 10:23:02 -0400 Subject: [PATCH 57/76] refactoring+comments for clarity --- src/components/connect.js | 13 +- src/components/connectAdvanced.js | 129 +++++++++++--------- src/selectors/createFactoryAwareSelector.js | 20 ++- src/selectors/getFinalProps.js | 48 ++++---- src/selectors/mapDispatch.js | 4 +- src/selectors/mapState.js | 2 +- 6 files changed, 114 insertions(+), 102 deletions(-) diff --git a/src/components/connect.js b/src/components/connect.js index ca69cef0d..6a0b2e2b4 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -13,11 +13,9 @@ import { defaultMergeProps } from '../selectors/mergeProps' pieces of that final selector. First, buildOptions combines its args with some meta into an options object that's passed to - connectAdvanced, which will pass a modified version of that options object to selectorFactory. + connectAdvanced, which will pass a modified* version of that options object to selectorFactory. - options modifications: - added: dispatch, displayName, WrappedComponent - removed: getDisplayName, renderCountProp + *values added to options: dispatch, displayName, WrappedComponent Each time selectorFactory is called (whenever an instance of the component in connectAdvanced is constructed or hot reloaded), it uses the modified options object to build a selector function: @@ -96,8 +94,7 @@ export function buildOptions( methodName: 'connect', // if mapStateToProps is not given a value, the Connect component doesn't subscribe to the store - // or pass state to the selector returned by selectorFactory. - dependsOnState: Boolean(mapStateToProps) + shouldHandleStateChanges: Boolean(mapStateToProps) } } @@ -127,12 +124,12 @@ export function wrapWithVerify({ getState, getDispatch, mergeProps, ...options } } } -export function selectorFactory(options) { +export function selectorFactory(dispatch, options) { return flow( addStateAndDispatchSelectors, wrapWithVerify, createFinalPropsSelector - )(options) + )({ ...options, dispatch }) } export default function connect(...args) { diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index f9e44d11a..1c44bf542 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -11,19 +11,23 @@ export default function connectAdvanced( selectorFactory is a func is responsible for returning the selector function used to compute new props from state, props, and dispatch. For example: - export default connectAdvanced(() => (state, props, dispatch) => ({ + export default connectAdvanced((dispatch, options) => (state, props) => ({ thing: state.things[props.thingId], saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)), }))(YourComponent) + + Access to dispatch is provided to the factory. selectorFactories can bind actionCreators + outside of their selector as an optimization + options passed to connectAdvanced are passed to the selectorFactory, along with displayName + and WrappedComponent. + + Note that selectorFactory is responisible for all caching/memoization of inbound and outbound + props. Do not use connectAdvanced directly without memoizing results between calls to your + selector, otherwise the Connect component will re-render on every state or props change. */ selectorFactory, // options object: { - // if true, the selector receieves the current store state as the first arg, and this HOC - // subscribes to store changes during componentDidMount. if false, null is passed as the first - // arg of selector and store.subscribe() is never called. - dependsOnState = true, - // the func used to compute this HOC's displayName from the wrapped component's displayName. // probably overridden by wrapper functions such as connect() getDisplayName = name => `ConnectAdvanced(${name})`, @@ -36,19 +40,49 @@ export default function connectAdvanced( // calls to render. useful for watching in react devtools for unnecessary re-renders. renderCountProp = undefined, + // determines whether this HOC subscribes to store changes + shouldHandleStateChanges = true, + // the key of props/context to get the store storeKey = 'store', // if true, the wrapped element is exposed by this HOC via the getWrappedInstance() function. withRef = false, + // additional options are passed through to the selectorFactory ...connectOptions } = {} ) { const subscriptionKey = storeKey + 'Subscription' const version = hotReloadingVersion++ + const contextTypes = { + [storeKey]: storeShape, + [subscriptionKey]: PropTypes.instanceOf(Subscription) + } + const childContextTypes = { + [subscriptionKey]: PropTypes.instanceOf(Subscription).isRequired + } + return function wrapWithConnect(WrappedComponent) { + const wrappedComponentName = WrappedComponent.displayName + || WrappedComponent.name + || 'Component' + + const displayName = getDisplayName(wrappedComponentName) + + const selectorFactoryOptions = { + ...connectOptions, + getDisplayName, + methodName, + renderCountProp, + shouldHandleStateChanges, + storeKey, + withRef, + displayName, + WrappedComponent + } + class Connect extends Component { constructor(props, context) { super(props, context) @@ -62,9 +96,9 @@ export default function connectAdvanced( invariant(this.store, `Could not find "${storeKey}" in either the context or ` + - `props of "${Connect.displayName}". ` + + `props of "${displayName}". ` + `Either wrap the root component in a , ` + - `or explicitly pass "${storeKey}" as a prop to "${Connect.displayName}".` + `or explicitly pass "${storeKey}" as a prop to "${displayName}".` ) this.initSelector() @@ -76,21 +110,25 @@ export default function connectAdvanced( } componentDidMount() { - if (!dependsOnState) return - + if (!shouldHandleStateChanges) return + + // componentWillMount fires during server side rendering, but componentDidMount and + // componentWillUnmount do not. Because of this, trySubscribe happens during ...didMount. + // Otherwise, unsubscription would never take place during SSR, causing a memory leak. + // To handle the case where a child component may have triggered a state change by + // dispatching an action in its componentWillMount, we have to re-run the select and maybe + // re-render. this.subscription.trySubscribe() - // check for recomputations that happened after this component has rendered, such as - // when a child component dispatches an action in its componentWillMount - this.runSelector(this.props) + this.selector.run(this.props) if (this.shouldComponentUpdate()) this.forceUpdate() } componentWillReceiveProps(nextProps) { - this.runSelector(nextProps) + this.selector.run(nextProps) } shouldComponentUpdate() { - return this.lastRenderedProps !== this.nextRenderedProps + return this.selector.lastProps !== this.selector.nextProps } componentWillUnmount() { @@ -100,7 +138,7 @@ export default function connectAdvanced( this.subscription = { isSubscribed: () => false } this.store = null this.parentSub = null - this.runSelector = () => {} + this.selector.run = () => {} } getWrappedInstance() { @@ -116,29 +154,23 @@ export default function connectAdvanced( } initSelector() { - const selector = selectorFactory({ - // most options passed to connectAdvanced are passed along to the selectorFactory - dependsOnState, methodName, storeKey, withRef, ...connectOptions, - // useful for factories that want to bind action creators outside the selector - dispatch: this.store.dispatch, - // useful for error messages - displayName: Connect.displayName, - // useful if a factory wants to use attributes of the component to build the selector, - // for example: one could use its propTypes as a props whitelist - WrappedComponent - }) - - this.runSelector = function runSelector(ownProps) { - const state = dependsOnState ? this.store.getState() : null - this.nextRenderedProps = selector(state, ownProps, this.store.dispatch) + const store = this.store + const selector = selectorFactory(store.dispatch, selectorFactoryOptions) + + // wrap the selector in an object that tracks its results between runs + const wrapper = { + lastProps: null, + nextProps: selector(store.getState(), this.props), + run(props) { + wrapper.nextProps = selector(store.getState(), props) + } } - this.lastRenderedProps = null - this.runSelector(this.props) + this.selector = wrapper } initSubscription() { function onStoreStateChange(notifyNestedSubs) { - this.runSelector(this.props) + this.selector.run(this.props) if (this.shouldComponentUpdate()) { this.setState({}, notifyNestedSubs) } else { @@ -146,7 +178,7 @@ export default function connectAdvanced( } } - const onChange = dependsOnState + const onChange = shouldHandleStateChanges ? onStoreStateChange.bind(this) : (notifyNestedSubs => notifyNestedSubs()) @@ -170,33 +202,18 @@ export default function connectAdvanced( } render() { - this.lastRenderedProps = this.nextRenderedProps - return createElement( WrappedComponent, - this.addExtraProps(this.lastRenderedProps) + this.addExtraProps(this.selector.lastProps = this.selector.nextProps) ) } } - const wrappedComponentName = WrappedComponent.displayName - || WrappedComponent.name - || 'Component' - - Connect.displayName = getDisplayName(wrappedComponentName) Connect.WrappedComponent = WrappedComponent - - Connect.propTypes = { - [storeKey]: storeShape, - [subscriptionKey]: PropTypes.instanceOf(Subscription) - } - Connect.contextTypes = { - [storeKey]: storeShape, - [subscriptionKey]: PropTypes.instanceOf(Subscription) - } - Connect.childContextTypes = { - [subscriptionKey]: PropTypes.instanceOf(Subscription).isRequired - } + Connect.displayName = displayName + Connect.childContextTypes = childContextTypes + Connect.contextTypes = contextTypes + Connect.propTypes = contextTypes if (process.env.NODE_ENV !== 'production') { Connect.prototype.componentWillUpdate = function componentWillUpdate() { @@ -207,7 +224,7 @@ export default function connectAdvanced( this.subscription.tryUnsubscribe() this.initSubscription() - if (dependsOnState) this.subscription.trySubscribe() + if (shouldHandleStateChanges) this.subscription.trySubscribe() } } } diff --git a/src/selectors/createFactoryAwareSelector.js b/src/selectors/createFactoryAwareSelector.js index 542103e4e..1ea272eec 100644 --- a/src/selectors/createFactoryAwareSelector.js +++ b/src/selectors/createFactoryAwareSelector.js @@ -9,6 +9,7 @@ export function createMapOrMapFactoryProxy(mapToProps) { function firstRun(storePart, props) { const firstResult = mapToProps(storePart, props) + if (typeof firstResult === 'function') { proxy.mapToProps = firstResult proxy.dependsOnProps = firstResult.length !== 1 @@ -22,26 +23,23 @@ export function createMapOrMapFactoryProxy(mapToProps) { return proxy } -export function createImpureFactoryAwareSelector(getStorePart, mapToProps) { +export function createImpureFactoryAwareSelector(mapToProps) { const proxy = createMapOrMapFactoryProxy(mapToProps) - return function impureFactoryAwareSelector(state, props, dispatch) { - return proxy.mapToProps( - getStorePart(state, props, dispatch), - props - ) + return function impureFactoryAwareSelector(storePart, props) { + return proxy.mapToProps(storePart, props) } } -export function createPureFactoryAwareSelector(getStorePart, mapToProps) { +export function createPureFactoryAwareSelector(mapToProps) { const proxy = createMapOrMapFactoryProxy(mapToProps) const noProps = {} let lastStorePart = undefined let lastProps = undefined let result = undefined - function pureFactoryAwareSelector(state, props, dispatch) { - const nextStorePart = getStorePart(state, props, dispatch) + function pureFactoryAwareSelector(storePart, props) { + const nextStorePart = storePart const nextProps = proxy.dependsOnProps ? props : noProps if (lastStorePart !== nextStorePart || lastProps !== nextProps) { @@ -57,7 +55,7 @@ export function createPureFactoryAwareSelector(getStorePart, mapToProps) { return pureFactoryAwareSelector } -export default function createFactoryAwareSelector(pure, getStorePart, mapToProps) { +export default function createFactoryAwareSelector(pure, mapToProps) { const create = pure ? createPureFactoryAwareSelector : createImpureFactoryAwareSelector - return create(getStorePart, mapToProps) + return create(mapToProps) } diff --git a/src/selectors/getFinalProps.js b/src/selectors/getFinalProps.js index 18f10596c..8e2746854 100644 --- a/src/selectors/getFinalProps.js +++ b/src/selectors/getFinalProps.js @@ -1,40 +1,40 @@ import memoizeProps from '../utils/memoizeProps' -export function createImpureFinalPropsSelector({ getState, getDispatch, mergeProps }) { - return function impureSelector(state, props, dispatch) { +export function createImpureFinalPropsSelector({ dispatch, getState, getDispatch, mergeProps }) { + return function impureSelector(state, props) { return mergeProps( - getState(state, props, dispatch), - getDispatch(state, props, dispatch), + getState(state, props), + getDispatch(dispatch, props), props ) } } -export function createPureFinalPropsSelector({ getState, getDispatch, mergeProps }) { - const memoizeOwn = memoizeProps() - const memoizeGetState = memoizeProps() - const memoizeGetDispatch = memoizeProps() +export function createPureFinalPropsSelector({ dispatch, getState, getDispatch, mergeProps }) { + const memoizeOwnProps = memoizeProps() + const memoizeStateProps = memoizeProps() + const memoizeDispatchProps = memoizeProps() const memoizeFinal = memoizeProps() - let lastOwn = undefined - let lastState = undefined - let lastDispatch = undefined - let lastMerged = undefined + let lastOwnProps = undefined + let lastStateProps = undefined + let lastDispatchProps = undefined + let lastFinalProps = undefined - return function pureSelector(state, props, dispatch) { - const nextOwn = memoizeOwn(props) - const nextState = memoizeGetState(getState(state, nextOwn, dispatch)) - const nextDispatch = memoizeGetDispatch(getDispatch(state, nextOwn, dispatch)) + return function pureSelector(state, props) { + const nextOwnProps = memoizeOwnProps(props) + const nextStateProps = memoizeStateProps(getState(state, nextOwnProps)) + const nextDispatchProps = memoizeDispatchProps(getDispatch(dispatch, nextOwnProps)) - if (lastOwn !== nextOwn - || lastState !== nextState - || lastDispatch !== nextDispatch) { - lastMerged = memoizeFinal(mergeProps(nextState, nextDispatch, nextOwn)) - lastOwn = nextOwn - lastState = nextState - lastDispatch = nextDispatch + if (lastOwnProps !== nextOwnProps + || lastStateProps !== nextStateProps + || lastDispatchProps !== nextDispatchProps) { + lastFinalProps = memoizeFinal(mergeProps(nextStateProps, nextDispatchProps, nextOwnProps)) + lastOwnProps = nextOwnProps + lastStateProps = nextStateProps + lastDispatchProps = nextDispatchProps } - return lastMerged + return lastFinalProps } } diff --git a/src/selectors/mapDispatch.js b/src/selectors/mapDispatch.js index 811ffe04b..2817bdfdb 100644 --- a/src/selectors/mapDispatch.js +++ b/src/selectors/mapDispatch.js @@ -15,9 +15,9 @@ export function whenMapDispatchIsObject({ mapDispatchToProps, dispatch }) { } } -export function whenMapDispatchIsFunction({ mapDispatchToProps, pure, dispatch }) { +export function whenMapDispatchIsFunction({ mapDispatchToProps, pure }) { if (typeof mapDispatchToProps === 'function') { - return createFactoryAwareSelector(pure, () => dispatch, mapDispatchToProps) + return createFactoryAwareSelector(pure, mapDispatchToProps) } } diff --git a/src/selectors/mapState.js b/src/selectors/mapState.js index 33f4ac0ec..dc510a22f 100644 --- a/src/selectors/mapState.js +++ b/src/selectors/mapState.js @@ -9,7 +9,7 @@ export function whenMapStateIsMissing({ mapStateToProps }) { export function whenMapStateIsFunction({ mapStateToProps, pure }) { if (typeof mapStateToProps === 'function') { - return createFactoryAwareSelector(pure, state => state, mapStateToProps) + return createFactoryAwareSelector(pure, mapStateToProps) } } From 35d175f9447fea419d151306d714c8fff266a9f2 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 9 Jul 2016 16:08:58 -0400 Subject: [PATCH 58/76] refactoring --- src/components/connectAdvanced.js | 3 ++- src/selectors/createFactoryAwareSelector.js | 23 +++++++++------------ src/selectors/getFinalProps.js | 8 ++++--- src/selectors/mapDispatch.js | 2 +- src/selectors/mapState.js | 2 +- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 1c44bf542..ca366b94b 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -169,10 +169,11 @@ export default function connectAdvanced( } initSubscription() { + const dummyState = {} function onStoreStateChange(notifyNestedSubs) { this.selector.run(this.props) if (this.shouldComponentUpdate()) { - this.setState({}, notifyNestedSubs) + this.setState(dummyState, notifyNestedSubs) } else { notifyNestedSubs() } diff --git a/src/selectors/createFactoryAwareSelector.js b/src/selectors/createFactoryAwareSelector.js index 1ea272eec..4619a82f2 100644 --- a/src/selectors/createFactoryAwareSelector.js +++ b/src/selectors/createFactoryAwareSelector.js @@ -33,29 +33,26 @@ export function createImpureFactoryAwareSelector(mapToProps) { export function createPureFactoryAwareSelector(mapToProps) { const proxy = createMapOrMapFactoryProxy(mapToProps) - const noProps = {} let lastStorePart = undefined let lastProps = undefined let result = undefined - function pureFactoryAwareSelector(storePart, props) { - const nextStorePart = storePart - const nextProps = proxy.dependsOnProps ? props : noProps - - if (lastStorePart !== nextStorePart || lastProps !== nextProps) { + return function pureFactoryAwareSelector(nextStorePart, nextProps) { + if ( + lastStorePart !== nextStorePart || + (proxy.dependsOnProps && lastProps !== nextProps) + ) { result = proxy.mapToProps(nextStorePart, nextProps) lastStorePart = nextStorePart lastProps = nextProps - pureFactoryAwareSelector.dependsOnProps = proxy.dependsOnProps } + return result } - - pureFactoryAwareSelector.dependsOnProps = proxy.dependsOnProps - return pureFactoryAwareSelector } -export default function createFactoryAwareSelector(pure, mapToProps) { - const create = pure ? createPureFactoryAwareSelector : createImpureFactoryAwareSelector - return create(mapToProps) +export default function createFactoryAwareSelector(mapToProps, pure) { + return pure + ? createPureFactoryAwareSelector(mapToProps) + : createImpureFactoryAwareSelector(mapToProps) } diff --git a/src/selectors/getFinalProps.js b/src/selectors/getFinalProps.js index 8e2746854..8edf50f55 100644 --- a/src/selectors/getFinalProps.js +++ b/src/selectors/getFinalProps.js @@ -25,9 +25,11 @@ export function createPureFinalPropsSelector({ dispatch, getState, getDispatch, const nextStateProps = memoizeStateProps(getState(state, nextOwnProps)) const nextDispatchProps = memoizeDispatchProps(getDispatch(dispatch, nextOwnProps)) - if (lastOwnProps !== nextOwnProps - || lastStateProps !== nextStateProps - || lastDispatchProps !== nextDispatchProps) { + if ( + lastOwnProps !== nextOwnProps || + lastStateProps !== nextStateProps || + lastDispatchProps !== nextDispatchProps + ) { lastFinalProps = memoizeFinal(mergeProps(nextStateProps, nextDispatchProps, nextOwnProps)) lastOwnProps = nextOwnProps lastStateProps = nextStateProps diff --git a/src/selectors/mapDispatch.js b/src/selectors/mapDispatch.js index 2817bdfdb..1308db924 100644 --- a/src/selectors/mapDispatch.js +++ b/src/selectors/mapDispatch.js @@ -17,7 +17,7 @@ export function whenMapDispatchIsObject({ mapDispatchToProps, dispatch }) { export function whenMapDispatchIsFunction({ mapDispatchToProps, pure }) { if (typeof mapDispatchToProps === 'function') { - return createFactoryAwareSelector(pure, mapDispatchToProps) + return createFactoryAwareSelector(mapDispatchToProps, pure) } } diff --git a/src/selectors/mapState.js b/src/selectors/mapState.js index dc510a22f..94367747a 100644 --- a/src/selectors/mapState.js +++ b/src/selectors/mapState.js @@ -9,7 +9,7 @@ export function whenMapStateIsMissing({ mapStateToProps }) { export function whenMapStateIsFunction({ mapStateToProps, pure }) { if (typeof mapStateToProps === 'function') { - return createFactoryAwareSelector(pure, mapStateToProps) + return createFactoryAwareSelector(mapStateToProps, pure) } } From 67d6e13e8a82ba5b5d0dd0af966dc77526217770 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 9 Jul 2016 18:10:15 -0400 Subject: [PATCH 59/76] Optimize Subscription subscribe/unsubscribe --- src/utils/Subscription.js | 66 ++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/src/utils/Subscription.js b/src/utils/Subscription.js index 093519a17..5c8fd67ec 100644 --- a/src/utils/Subscription.js +++ b/src/utils/Subscription.js @@ -1,7 +1,42 @@ -// encapsulates the subscription logic for connecting a component to the redux store, as well as -// nesting subscriptions of decendant components, so that we can ensure the ancestor components -// re-render before descendants +// a linked list of nest subscription listeners. Implementing as a LL instead of an array +// makes for cheaper subscriptions & unsubscriptions vs cloning/mutating an array. Also, it +// was nice to implement a linked list for the first time in like 15 years. +function createNestedSubList() { + const head = {} + + return { + subscribe(listener) { + const first = head.next + let current = head.next = { listener, prev: head, next: first } + if (first) first.prev = current + + return function unsubscribe() { + if (!current) return + + // unsubscribe takes itself out of the list, by updating its neighbors to point to + // each other + const { next, prev } = current + if (next) next.prev = prev + prev.next = next + current = null + } + }, + notifyAll() { + let current = head.next + + while (current) { + current.listener() + current = current.next + } + } + } +} + + +// encapsulates the subscription logic for connecting a component to the redux store, as +// well as nesting subscriptions of descendant components, so that we can ensure the +// ancestor components re-render before descendants export default class Subscription { constructor(store, parentSub, onStateChange) { this.subscribe = parentSub @@ -10,42 +45,23 @@ export default class Subscription { this.onStateChange = onStateChange this.unsubscribe = null - this.nestedSubs = [] - this.notifyNestedSubs = this.notifyNestedSubs.bind(this) + this.nestedSubs = createNestedSubList() } addNestedSub(listener) { this.trySubscribe() - this.nestedSubs = this.nestedSubs.concat(listener) - - let subscribed = true - return () => { - if (!subscribed) return - subscribed = false - - const subs = this.nestedSubs.slice() - const index = subs.indexOf(listener) - subs.splice(index, 1) - this.nestedSubs = subs - } + return this.nestedSubs.subscribe(listener) } isSubscribed() { return Boolean(this.unsubscribe) } - notifyNestedSubs() { - const subs = this.nestedSubs - for (let i = subs.length - 1; i >= 0; i--) { - subs[i]() - } - } - trySubscribe() { if (this.unsubscribe) return this.unsubscribe = this.subscribe(() => { - this.onStateChange(this.notifyNestedSubs) + this.onStateChange(this.nestedSubs.notifyAll) }) } From a604856c8164d1c55aa66357ad015c950b573f19 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 9 Jul 2016 18:17:21 -0400 Subject: [PATCH 60/76] comments --- src/components/connectAdvanced.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index ca366b94b..60bb47a78 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -8,18 +8,17 @@ import storeShape from '../utils/storeShape' let hotReloadingVersion = 0 export default function connectAdvanced( /* - selectorFactory is a func is responsible for returning the selector function used to compute new - props from state, props, and dispatch. For example: + selectorFactory is a func that is responsible for returning the selector function used to + compute new props from state, props, and dispatch. For example: export default connectAdvanced((dispatch, options) => (state, props) => ({ thing: state.things[props.thingId], saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)), }))(YourComponent) - Access to dispatch is provided to the factory. selectorFactories can bind actionCreators - outside of their selector as an optimization - options passed to connectAdvanced are passed to the selectorFactory, along with displayName - and WrappedComponent. + Access to dispatch is provided to the factory so selectorFactories can bind actionCreators + outside of their selector as an optimization. Options passed to connectAdvanced are passed to + the selectorFactory, along with displayName and WrappedComponent, as the second argument. Note that selectorFactory is responisible for all caching/memoization of inbound and outbound props. Do not use connectAdvanced directly without memoizing results between calls to your From 652d85cf28f5ce4f375a116f50595855f0ad0f45 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sun, 10 Jul 2016 09:28:14 -0400 Subject: [PATCH 61/76] refactor selectors --- src/selectors/createFactoryAwareSelector.js | 58 --------------------- src/selectors/createMapOrMapFactoryProxy.js | 23 ++++++++ src/selectors/getFinalProps.js | 8 +-- src/selectors/mapDispatch.js | 33 +++++++++--- src/selectors/mapState.js | 33 +++++++++--- src/utils/memoizeProps.js | 8 ++- 6 files changed, 83 insertions(+), 80 deletions(-) delete mode 100644 src/selectors/createFactoryAwareSelector.js create mode 100644 src/selectors/createMapOrMapFactoryProxy.js diff --git a/src/selectors/createFactoryAwareSelector.js b/src/selectors/createFactoryAwareSelector.js deleted file mode 100644 index 4619a82f2..000000000 --- a/src/selectors/createFactoryAwareSelector.js +++ /dev/null @@ -1,58 +0,0 @@ - -// factory detection. if the first result of mapToProps is a function, use that as the -// true mapToProps -export function createMapOrMapFactoryProxy(mapToProps) { - const proxy = { - mapToProps: firstRun, - dependsOnProps: mapToProps.length !== 1 - } - - function firstRun(storePart, props) { - const firstResult = mapToProps(storePart, props) - - if (typeof firstResult === 'function') { - proxy.mapToProps = firstResult - proxy.dependsOnProps = firstResult.length !== 1 - return firstResult(storePart, props) - } else { - proxy.mapToProps = mapToProps - return firstResult - } - } - - return proxy -} - -export function createImpureFactoryAwareSelector(mapToProps) { - const proxy = createMapOrMapFactoryProxy(mapToProps) - - return function impureFactoryAwareSelector(storePart, props) { - return proxy.mapToProps(storePart, props) - } -} - -export function createPureFactoryAwareSelector(mapToProps) { - const proxy = createMapOrMapFactoryProxy(mapToProps) - let lastStorePart = undefined - let lastProps = undefined - let result = undefined - - return function pureFactoryAwareSelector(nextStorePart, nextProps) { - if ( - lastStorePart !== nextStorePart || - (proxy.dependsOnProps && lastProps !== nextProps) - ) { - result = proxy.mapToProps(nextStorePart, nextProps) - lastStorePart = nextStorePart - lastProps = nextProps - } - - return result - } -} - -export default function createFactoryAwareSelector(mapToProps, pure) { - return pure - ? createPureFactoryAwareSelector(mapToProps) - : createImpureFactoryAwareSelector(mapToProps) -} diff --git a/src/selectors/createMapOrMapFactoryProxy.js b/src/selectors/createMapOrMapFactoryProxy.js new file mode 100644 index 000000000..73f691657 --- /dev/null +++ b/src/selectors/createMapOrMapFactoryProxy.js @@ -0,0 +1,23 @@ +// Detects if the first result of mapStateToProps or mapDispatchToProps is a +// function, and uses that as the real function on subsequent calls. +export default function createMapOrMapFactoryProxy(mapToProps) { + const proxy = { + mapToProps: firstRun, + dependsOnProps: mapToProps.length !== 1 + } + + function firstRun(storePart, props) { + const firstResult = mapToProps(storePart, props) + + if (typeof firstResult === 'function') { + proxy.mapToProps = firstResult + proxy.dependsOnProps = firstResult.length !== 1 + return firstResult(storePart, props) + } else { + proxy.mapToProps = mapToProps + return firstResult + } + } + + return proxy +} diff --git a/src/selectors/getFinalProps.js b/src/selectors/getFinalProps.js index 8edf50f55..54e078cfa 100644 --- a/src/selectors/getFinalProps.js +++ b/src/selectors/getFinalProps.js @@ -1,16 +1,16 @@ import memoizeProps from '../utils/memoizeProps' -export function createImpureFinalPropsSelector({ dispatch, getState, getDispatch, mergeProps }) { +export function createImpureFinalPropsSelector({ getState, getDispatch, mergeProps }) { return function impureSelector(state, props) { return mergeProps( getState(state, props), - getDispatch(dispatch, props), + getDispatch(props), props ) } } -export function createPureFinalPropsSelector({ dispatch, getState, getDispatch, mergeProps }) { +export function createPureFinalPropsSelector({ getState, getDispatch, mergeProps }) { const memoizeOwnProps = memoizeProps() const memoizeStateProps = memoizeProps() const memoizeDispatchProps = memoizeProps() @@ -23,7 +23,7 @@ export function createPureFinalPropsSelector({ dispatch, getState, getDispatch, return function pureSelector(state, props) { const nextOwnProps = memoizeOwnProps(props) const nextStateProps = memoizeStateProps(getState(state, nextOwnProps)) - const nextDispatchProps = memoizeDispatchProps(getDispatch(dispatch, nextOwnProps)) + const nextDispatchProps = memoizeDispatchProps(getDispatch(nextOwnProps)) if ( lastOwnProps !== nextOwnProps || diff --git a/src/selectors/mapDispatch.js b/src/selectors/mapDispatch.js index 1308db924..53458fa19 100644 --- a/src/selectors/mapDispatch.js +++ b/src/selectors/mapDispatch.js @@ -1,28 +1,47 @@ import { bindActionCreators } from 'redux' -import createFactoryAwareSelector from './createFactoryAwareSelector' +import createMapOrMapFactoryProxy from './createMapOrMapFactoryProxy' export function whenMapDispatchIsMissing({ mapDispatchToProps, dispatch }) { if (!mapDispatchToProps) { const dispatchProp = { dispatch } - return () => dispatchProp + return function justDispatch() { return dispatchProp } } } export function whenMapDispatchIsObject({ mapDispatchToProps, dispatch }) { if (mapDispatchToProps && typeof mapDispatchToProps === 'object') { const bound = bindActionCreators(mapDispatchToProps, dispatch) - return () => bound + return function boundAcitonCreators() { return bound } } } -export function whenMapDispatchIsFunction({ mapDispatchToProps, pure }) { - if (typeof mapDispatchToProps === 'function') { - return createFactoryAwareSelector(mapDispatchToProps, pure) +export function whenMapDispatchIsFunctionAndNotPure({ mapDispatchToProps, dispatch, pure }) { + if (!pure && typeof mapDispatchToProps === 'function') { + const proxy = createMapOrMapFactoryProxy(mapDispatchToProps) + return function impureMapDispatchToProps(props) { return proxy.mapToProps(dispatch, props) } + } +} + +export function whenMapDispatchIsFunctionAndPure({ mapDispatchToProps, dispatch, pure }) { + if (pure && typeof mapDispatchToProps === 'function') { + const proxy = createMapOrMapFactoryProxy(mapDispatchToProps) + let lastProps = undefined + let result = undefined + + return function pureMapDispatchToProps(nextProps) { + if (!lastProps || (proxy.dependsOnProps && lastProps !== nextProps)) { + result = proxy.mapToProps(dispatch, nextProps) + lastProps = nextProps + } + + return result + } } } export default [ whenMapDispatchIsMissing, - whenMapDispatchIsFunction, + whenMapDispatchIsFunctionAndNotPure, + whenMapDispatchIsFunctionAndPure, whenMapDispatchIsObject ] diff --git a/src/selectors/mapState.js b/src/selectors/mapState.js index 94367747a..038980a33 100644 --- a/src/selectors/mapState.js +++ b/src/selectors/mapState.js @@ -1,19 +1,40 @@ -import createFactoryAwareSelector from './createFactoryAwareSelector' +import createMapOrMapFactoryProxy from './createMapOrMapFactoryProxy' export function whenMapStateIsMissing({ mapStateToProps }) { if (!mapStateToProps) { const empty = {} - return () => empty + return function emptyState() { return empty } } } -export function whenMapStateIsFunction({ mapStateToProps, pure }) { - if (typeof mapStateToProps === 'function') { - return createFactoryAwareSelector(mapStateToProps, pure) +export function whenMapStateIsFunctionAndNotPure({ mapStateToProps, pure }) { + if (!pure && typeof mapStateToProps === 'function') { + const proxy = createMapOrMapFactoryProxy(mapStateToProps) + return function impureMapStateToProps(state, props) { return proxy.mapToProps(state, props) } + } +} + +export function whenMapStateIsFunctionAndPure({ mapStateToProps, pure }) { + if (pure && typeof mapStateToProps === 'function') { + const proxy = createMapOrMapFactoryProxy(mapStateToProps) + let lastState = undefined + let lastProps = undefined + let result = undefined + + return function pureMapStateToProps(nextState, nextProps) { + if (lastState !== nextState || (proxy.dependsOnProps && lastProps !== nextProps)) { + result = proxy.mapToProps(nextState, nextProps) + lastState = nextState + lastProps = nextProps + } + + return result + } } } export default [ whenMapStateIsMissing, - whenMapStateIsFunction + whenMapStateIsFunctionAndNotPure, + whenMapStateIsFunctionAndPure ] diff --git a/src/utils/memoizeProps.js b/src/utils/memoizeProps.js index 769950908..e8946afc6 100644 --- a/src/utils/memoizeProps.js +++ b/src/utils/memoizeProps.js @@ -8,12 +8,10 @@ export default function memoizeProps() { let result = undefined return function memoize(nextProps) { - if (lastProps !== nextProps) { - if (!lastProps || !equal(lastProps, nextProps)) { - result = nextProps - } - lastProps = nextProps + if (!lastProps || !equal(lastProps, nextProps)) { + result = nextProps } + lastProps = nextProps return result } } From d56c36d3d9891f62a9de9342c925ef1d5cd91846 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sun, 10 Jul 2016 09:32:33 -0400 Subject: [PATCH 62/76] rename files --- src/components/connect.js | 16 ++++++++-------- .../{mapDispatch.js => mapDispatchToProps.js} | 0 .../{mapState.js => mapStateToProps.js} | 0 3 files changed, 8 insertions(+), 8 deletions(-) rename src/selectors/{mapDispatch.js => mapDispatchToProps.js} (100%) rename src/selectors/{mapState.js => mapStateToProps.js} (100%) diff --git a/src/components/connect.js b/src/components/connect.js index 6a0b2e2b4..f08a822f1 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -3,8 +3,8 @@ import flow from 'lodash/flow' import connectAdvanced from './connectAdvanced' import verifyPlainObject from '../utils/verifyPlainObject' import { createFinalPropsSelector } from '../selectors/getFinalProps' -import defaultMapDispatchFactories from '../selectors/mapDispatch' -import defaultMapStateFactories from '../selectors/mapState' +import defaultMapDispatchFactories from '../selectors/mapDispatchToProps' +import defaultMapStateFactories from '../selectors/mapStateToProps' import { defaultMergeProps } from '../selectors/mergeProps' /* @@ -23,16 +23,16 @@ import { defaultMergeProps } from '../selectors/mergeProps' 1. Create the getState selector by matching mapStateToProps to the mapStateFactories array passed in from buildOptions. - The default behaviors (from mapState.js) check mapStateToProps for a function or missing - value. This could be overridden by supplying a custom value to the mapStateFactories - property of connect's options argument + The default behaviors (from mapStateToProps.js) check mapStateToProps for a function + or missing value. This could be overridden by supplying a custom value to the + mapStateFactories property of connect's options argument 2. Create the getDispatch selector by matching mapDispatchToProps to the mapDispatchFactories array passed in from buildOptions. - The default behaviors (from mapDispatch.js) check mapDispatchToProps for a function, object, - or missing value. The could be overridden by supplying a custom value to the - mapDispatchFactories property of connect's options argument. + The default behaviors (from mapDispatchToProps.js) check mapDispatchToProps for a + function, object, or missing value. The could be overridden by supplying a custom + value to the mapDispatchFactories property of connect's options argument. 3. Wrap the getState, getDispatch, and mergeProps selectors in functions that check that their return values are plain objects (on their first invocation.) diff --git a/src/selectors/mapDispatch.js b/src/selectors/mapDispatchToProps.js similarity index 100% rename from src/selectors/mapDispatch.js rename to src/selectors/mapDispatchToProps.js diff --git a/src/selectors/mapState.js b/src/selectors/mapStateToProps.js similarity index 100% rename from src/selectors/mapState.js rename to src/selectors/mapStateToProps.js From 418b6596293f1a126a718cd3a2025d495095d3a5 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sun, 10 Jul 2016 21:47:35 -0400 Subject: [PATCH 63/76] Change Subscription to follow same subscribe/unsubscribe/notify mechanics as redux's createStore --- src/utils/Subscription.js | 73 +++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/src/utils/Subscription.js b/src/utils/Subscription.js index 5c8fd67ec..a06c103ee 100644 --- a/src/utils/Subscription.js +++ b/src/utils/Subscription.js @@ -1,39 +1,3 @@ - -// a linked list of nest subscription listeners. Implementing as a LL instead of an array -// makes for cheaper subscriptions & unsubscriptions vs cloning/mutating an array. Also, it -// was nice to implement a linked list for the first time in like 15 years. -function createNestedSubList() { - const head = {} - - return { - subscribe(listener) { - const first = head.next - let current = head.next = { listener, prev: head, next: first } - if (first) first.prev = current - - return function unsubscribe() { - if (!current) return - - // unsubscribe takes itself out of the list, by updating its neighbors to point to - // each other - const { next, prev } = current - if (next) next.prev = prev - prev.next = next - current = null - } - }, - notifyAll() { - let current = head.next - - while (current) { - current.listener() - current = current.next - } - } - } -} - - // encapsulates the subscription logic for connecting a component to the redux store, as // well as nesting subscriptions of descendant components, so that we can ensure the // ancestor components re-render before descendants @@ -45,12 +9,38 @@ export default class Subscription { this.onStateChange = onStateChange this.unsubscribe = null - this.nestedSubs = createNestedSubList() + this.nextListeners = this.currentListeners = [] + } + + ensureCanMutateNextListeners() { + if (this.nextListeners === this.currentListeners) { + this.nextListeners = this.currentListeners.slice() + } } addNestedSub(listener) { this.trySubscribe() - return this.nestedSubs.subscribe(listener) + + let isSubscribed = true + this.ensureCanMutateNextListeners() + this.nextListeners.push(listener) + + return function unsubscribe() { + if (!isSubscribed) return + isSubscribed = false + + this.ensureCanMutateNextListeners() + const index = this.nextListeners.indexOf(listener) + this.nextListeners.splice(index, 1) + } + } + + notifyNestedSubs() { + const listeners = this.currentListeners = this.nextListeners + const length = listeners.length + for (let i = 0; i < length; i++) { + listeners[i]() + } } isSubscribed() { @@ -60,9 +50,10 @@ export default class Subscription { trySubscribe() { if (this.unsubscribe) return - this.unsubscribe = this.subscribe(() => { - this.onStateChange(this.nestedSubs.notifyAll) - }) + const notifyNestedSubs = this.notifyNestedSubs.bind(this) + const onStateChange = this.onStateChange + function listener() { onStateChange(notifyNestedSubs) } + this.unsubscribe = this.subscribe(listener) } tryUnsubscribe() { From 44fd92d1fe8cb720a835be5911e5ff155d7c94a7 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sun, 10 Jul 2016 21:51:28 -0400 Subject: [PATCH 64/76] refactoring connect's selectors to move logic to determine what to recompute into getFinalProps.js --- src/components/connect.js | 6 +- src/selectors/createMapOrMapFactoryProxy.js | 15 ++-- src/selectors/getFinalProps.js | 90 ++++++++++++++------- src/selectors/mapDispatchToProps.js | 37 +++------ src/selectors/mapStateToProps.js | 33 ++------ src/utils/memoizeProps.js | 13 ++- src/utils/verifyPlainObject.js | 2 +- 7 files changed, 101 insertions(+), 95 deletions(-) diff --git a/src/components/connect.js b/src/components/connect.js index f08a822f1..1f4d00f4a 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -115,7 +115,11 @@ export function addStateAndDispatchSelectors(options) { } export function wrapWithVerify({ getState, getDispatch, mergeProps, ...options }) { - const verify = (methodName, func) => verifyPlainObject(options.displayName, methodName, func) + const verify = (methodName, func) => { + const withVerify = verifyPlainObject(options.displayName, methodName, func) + withVerify.meta = func.meta + return withVerify + } return { ...options, getState: verify('mapStateToProps', getState), diff --git a/src/selectors/createMapOrMapFactoryProxy.js b/src/selectors/createMapOrMapFactoryProxy.js index 73f691657..129814546 100644 --- a/src/selectors/createMapOrMapFactoryProxy.js +++ b/src/selectors/createMapOrMapFactoryProxy.js @@ -1,23 +1,24 @@ // Detects if the first result of mapStateToProps or mapDispatchToProps is a // function, and uses that as the real function on subsequent calls. export default function createMapOrMapFactoryProxy(mapToProps) { - const proxy = { - mapToProps: firstRun, + const meta = { dependsOnProps: mapToProps.length !== 1 } - function firstRun(storePart, props) { + let actualMapToProps = function firstRun(storePart, props) { const firstResult = mapToProps(storePart, props) if (typeof firstResult === 'function') { - proxy.mapToProps = firstResult - proxy.dependsOnProps = firstResult.length !== 1 + actualMapToProps = firstResult + meta.dependsOnProps = firstResult.length !== 1 return firstResult(storePart, props) } else { - proxy.mapToProps = mapToProps + actualMapToProps = mapToProps return firstResult } } - return proxy + function mapToPropsProxy(storePart, props) { return actualMapToProps(storePart, props) } + mapToPropsProxy.meta = meta + return mapToPropsProxy } diff --git a/src/selectors/getFinalProps.js b/src/selectors/getFinalProps.js index 54e078cfa..f11943235 100644 --- a/src/selectors/getFinalProps.js +++ b/src/selectors/getFinalProps.js @@ -1,42 +1,78 @@ import memoizeProps from '../utils/memoizeProps' +import shallowEqual from '../utils/shallowEqual' -export function createImpureFinalPropsSelector({ getState, getDispatch, mergeProps }) { +export function createImpureFinalPropsSelector({ dispatch, getState, getDispatch, mergeProps }) { return function impureSelector(state, props) { return mergeProps( getState(state, props), - getDispatch(props), + getDispatch(dispatch, props), props ) } } -export function createPureFinalPropsSelector({ getState, getDispatch, mergeProps }) { - const memoizeOwnProps = memoizeProps() - const memoizeStateProps = memoizeProps() - const memoizeDispatchProps = memoizeProps() - const memoizeFinal = memoizeProps() - let lastOwnProps = undefined - let lastStateProps = undefined - let lastDispatchProps = undefined - let lastFinalProps = undefined - - return function pureSelector(state, props) { - const nextOwnProps = memoizeOwnProps(props) - const nextStateProps = memoizeStateProps(getState(state, nextOwnProps)) - const nextDispatchProps = memoizeDispatchProps(getDispatch(nextOwnProps)) - - if ( - lastOwnProps !== nextOwnProps || - lastStateProps !== nextStateProps || - lastDispatchProps !== nextDispatchProps - ) { - lastFinalProps = memoizeFinal(mergeProps(nextStateProps, nextDispatchProps, nextOwnProps)) - lastOwnProps = nextOwnProps - lastStateProps = nextStateProps - lastDispatchProps = nextDispatchProps +export function createPureFinalPropsSelector({ dispatch, getState, getDispatch, mergeProps }) { + const memoizeResult = memoizeProps() + let result = undefined + let prevState = undefined + let prevOwnProps = undefined + let prevStateProps = undefined + let prevDispatchProps = undefined + let getStateDependsOnProps = undefined + let getDispatchDependsOnProps = undefined + + function setResult() { + result = memoizeResult(mergeProps(prevStateProps, prevDispatchProps, prevOwnProps)) + } + + function handleFirstCall(firstState, firstOwnProps) { + prevState = firstState + prevOwnProps = firstOwnProps + prevStateProps = getState(firstState, firstOwnProps) + prevDispatchProps = getDispatch(dispatch, firstOwnProps) + getStateDependsOnProps = getState.meta && getState.meta.dependsOnProps + getDispatchDependsOnProps = getDispatch.meta && getDispatch.meta.dependsOnProps + + setResult() + } + + function handleNewPropsAndMaybeNewState(nextState, nextOwnProps) { + if (getStateDependsOnProps || nextState !== prevState) { + prevStateProps = getState(nextState, nextOwnProps) + } + + if (getDispatchDependsOnProps) { + prevDispatchProps = getDispatch(dispatch, nextOwnProps) + } + + prevState = nextState + prevOwnProps = nextOwnProps + + setResult() + } + + function handleNewStateButNotNewProps(nextState) { + const nextStateProps = getState(nextState, prevOwnProps) + prevState = nextState + + if (!shallowEqual(nextStateProps, prevStateProps)) { + prevStateProps = nextStateProps + setResult() } + } - return lastFinalProps + return function pureSelector(nextState, nextOwnProps) { + if (result === undefined) { + handleFirstCall(nextState, nextOwnProps) + + } else if (!shallowEqual(nextOwnProps, prevOwnProps)) { + handleNewPropsAndMaybeNewState(nextState, nextOwnProps) + + } else if (nextState !== prevState) { + handleNewStateButNotNewProps(nextState) + + } + return result } } diff --git a/src/selectors/mapDispatchToProps.js b/src/selectors/mapDispatchToProps.js index 53458fa19..bdb7265be 100644 --- a/src/selectors/mapDispatchToProps.js +++ b/src/selectors/mapDispatchToProps.js @@ -1,47 +1,28 @@ import { bindActionCreators } from 'redux' import createMapOrMapFactoryProxy from './createMapOrMapFactoryProxy' -export function whenMapDispatchIsMissing({ mapDispatchToProps, dispatch }) { +export function whenMapDispatchToPropsIsMissing({ mapDispatchToProps, dispatch }) { if (!mapDispatchToProps) { const dispatchProp = { dispatch } return function justDispatch() { return dispatchProp } } } -export function whenMapDispatchIsObject({ mapDispatchToProps, dispatch }) { +export function whenMapDispatchToPropsIsObject({ mapDispatchToProps, dispatch }) { if (mapDispatchToProps && typeof mapDispatchToProps === 'object') { const bound = bindActionCreators(mapDispatchToProps, dispatch) - return function boundAcitonCreators() { return bound } + return function boundActionCreators() { return bound } } } -export function whenMapDispatchIsFunctionAndNotPure({ mapDispatchToProps, dispatch, pure }) { - if (!pure && typeof mapDispatchToProps === 'function') { - const proxy = createMapOrMapFactoryProxy(mapDispatchToProps) - return function impureMapDispatchToProps(props) { return proxy.mapToProps(dispatch, props) } - } -} - -export function whenMapDispatchIsFunctionAndPure({ mapDispatchToProps, dispatch, pure }) { - if (pure && typeof mapDispatchToProps === 'function') { - const proxy = createMapOrMapFactoryProxy(mapDispatchToProps) - let lastProps = undefined - let result = undefined - - return function pureMapDispatchToProps(nextProps) { - if (!lastProps || (proxy.dependsOnProps && lastProps !== nextProps)) { - result = proxy.mapToProps(dispatch, nextProps) - lastProps = nextProps - } - - return result - } +export function whenMapDispatchToPropsIsFunction({ mapDispatchToProps }) { + if (typeof mapDispatchToProps === 'function') { + return createMapOrMapFactoryProxy(mapDispatchToProps) } } export default [ - whenMapDispatchIsMissing, - whenMapDispatchIsFunctionAndNotPure, - whenMapDispatchIsFunctionAndPure, - whenMapDispatchIsObject + whenMapDispatchToPropsIsMissing, + whenMapDispatchToPropsIsFunction, + whenMapDispatchToPropsIsObject ] diff --git a/src/selectors/mapStateToProps.js b/src/selectors/mapStateToProps.js index 038980a33..304b0f450 100644 --- a/src/selectors/mapStateToProps.js +++ b/src/selectors/mapStateToProps.js @@ -1,40 +1,19 @@ import createMapOrMapFactoryProxy from './createMapOrMapFactoryProxy' -export function whenMapStateIsMissing({ mapStateToProps }) { +export function whenMapStateToPropsIsMissing({ mapStateToProps }) { if (!mapStateToProps) { const empty = {} return function emptyState() { return empty } } } -export function whenMapStateIsFunctionAndNotPure({ mapStateToProps, pure }) { - if (!pure && typeof mapStateToProps === 'function') { - const proxy = createMapOrMapFactoryProxy(mapStateToProps) - return function impureMapStateToProps(state, props) { return proxy.mapToProps(state, props) } - } -} - -export function whenMapStateIsFunctionAndPure({ mapStateToProps, pure }) { - if (pure && typeof mapStateToProps === 'function') { - const proxy = createMapOrMapFactoryProxy(mapStateToProps) - let lastState = undefined - let lastProps = undefined - let result = undefined - - return function pureMapStateToProps(nextState, nextProps) { - if (lastState !== nextState || (proxy.dependsOnProps && lastProps !== nextProps)) { - result = proxy.mapToProps(nextState, nextProps) - lastState = nextState - lastProps = nextProps - } - - return result - } +export function whenMapStateToPropsIsFunction({ mapStateToProps }) { + if (typeof mapStateToProps === 'function') { + return createMapOrMapFactoryProxy(mapStateToProps) } } export default [ - whenMapStateIsMissing, - whenMapStateIsFunctionAndNotPure, - whenMapStateIsFunctionAndPure + whenMapStateToPropsIsMissing, + whenMapStateToPropsIsFunction ] diff --git a/src/utils/memoizeProps.js b/src/utils/memoizeProps.js index e8946afc6..061e431cd 100644 --- a/src/utils/memoizeProps.js +++ b/src/utils/memoizeProps.js @@ -4,14 +4,19 @@ const equal = shallowEqual // wrap the source props in a shallow equals because props objects with same properties are // semantically equal in the eyes of React... no need to return a new object. export default function memoizeProps() { - let lastProps = undefined + let prevProps = undefined let result = undefined return function memoize(nextProps) { - if (!lastProps || !equal(lastProps, nextProps)) { - result = nextProps + if (nextProps === prevProps) { + return nextProps } - lastProps = nextProps + + if (result === undefined || !equal(prevProps, nextProps)) { + return result = prevProps = nextProps + } + + prevProps = nextProps return result } } diff --git a/src/utils/verifyPlainObject.js b/src/utils/verifyPlainObject.js index a248d454e..5608e6822 100644 --- a/src/utils/verifyPlainObject.js +++ b/src/utils/verifyPlainObject.js @@ -7,7 +7,7 @@ export default function verifyPlainObject(displayName, methodName, func) { if (!func) throw new Error('Missing ' + methodName) let hasVerified = false - return (...args) => { + return function verify(...args) { const result = func(...args) if (hasVerified) return result hasVerified = true From 1cb72c025a1798342fd569098adf0e9f562e9843 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Mon, 11 Jul 2016 21:25:51 -0400 Subject: [PATCH 65/76] refactoring/optimizing connect+selectors --- src/components/connect.js | 73 ++++++------------ src/selectors/getFinalProps.js | 83 --------------------- src/selectors/makeImpurePropsSelector.js | 31 ++++++++ src/selectors/makePurePropsSelector.js | 95 ++++++++++++++++++++++++ src/selectors/mapDispatchToProps.js | 16 ++-- src/selectors/mapStateToProps.js | 10 ++- src/selectors/mergeProps.js | 1 + src/utils/memoizeProps.js | 22 ------ src/utils/verifyPlainObject.js | 18 +---- 9 files changed, 168 insertions(+), 181 deletions(-) delete mode 100644 src/selectors/getFinalProps.js create mode 100644 src/selectors/makeImpurePropsSelector.js create mode 100644 src/selectors/makePurePropsSelector.js delete mode 100644 src/utils/memoizeProps.js diff --git a/src/components/connect.js b/src/components/connect.js index 1f4d00f4a..1945e6ad2 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -1,8 +1,6 @@ -import flow from 'lodash/flow' - import connectAdvanced from './connectAdvanced' -import verifyPlainObject from '../utils/verifyPlainObject' -import { createFinalPropsSelector } from '../selectors/getFinalProps' +import makeImpurePropsSelector from '../selectors/makeImpurePropsSelector' +import makePurePropsSelector from '../selectors/makePurePropsSelector' import defaultMapDispatchFactories from '../selectors/mapDispatchToProps' import defaultMapStateFactories from '../selectors/mapStateToProps' import { defaultMergeProps } from '../selectors/mergeProps' @@ -15,34 +13,27 @@ import { defaultMergeProps } from '../selectors/mergeProps' First, buildOptions combines its args with some meta into an options object that's passed to connectAdvanced, which will pass a modified* version of that options object to selectorFactory. - *values added to options: dispatch, displayName, WrappedComponent + *values added to options: displayName, WrappedComponent Each time selectorFactory is called (whenever an instance of the component in connectAdvanced is constructed or hot reloaded), it uses the modified options object to build a selector function: - 1. Create the getState selector by matching mapStateToProps to the mapStateFactories array + 1. Convert mapStateToProps into a selector by matching it to the mapStateFactories array passed in from buildOptions. The default behaviors (from mapStateToProps.js) check mapStateToProps for a function or missing value. This could be overridden by supplying a custom value to the mapStateFactories property of connect's options argument - 2. Create the getDispatch selector by matching mapDispatchToProps to the mapDispatchFactories + 2. Convert mapDispatchToProps into a selector by matching it to the mapDispatchFactories array passed in from buildOptions. The default behaviors (from mapDispatchToProps.js) check mapDispatchToProps for a - function, object, or missing value. The could be overridden by supplying a custom + function, object, or missing value. This could be overridden by supplying a custom value to the mapDispatchFactories property of connect's options argument. - 3. Wrap the getState, getDispatch, and mergeProps selectors in functions that check that their - return values are plain objects (on their first invocation.) - - This is to inform the developer that they made a mistake coding the function the supplied - for any of mapStateToProps, mapDispatchToProps, or mergeProps. - - 4. Combine getState, getDispatch, and mergeProps selectors into the final props selector, by - passing the getState's results, getDispatch's results, and original props to mergeProps. - (See getFinalProps.js) + 3. Combine mapStateToProps, mapDispatchToProps, and mergeProps selectors into either a pure + (makePurePropsSelector.js) or impure (makeImpurePropsSelector.js) final props selector. The resulting final props selector is called by the component instance whenever it receives new props or is notified by the store subscription. @@ -60,14 +51,14 @@ export function buildOptions( // used to compute the Connect component's displayName from the wrapped component's displayName. getDisplayName: name => `Connect(${name})`, - // passed through to selectorFactory. defaults to the array of funcs returned in mapDispatch.js - // that determine the appropriate sub-selector to use for mapDispatchToProps, depending on - // whether it's a function, object, or missing. + // passed through to selectorFactory. defaults to the array of funcs returned in + // mapDispatchToProps.js that determine the appropriate sub-selector to use for + // mapDispatchToProps, depending on whether it's a function, object, or missing. mapDispatchFactories: defaultMapDispatchFactories, - // passed through to selectorFactory. defaults to the array of funcs returned in mapState.js - // that determine the appropriate sub-selector to use for mapStateToProps, depending on - // whether it's a function or missing. + // passed through to selectorFactory. defaults to the array of funcs returned in + // mapStateToProps.js that determine the appropriate sub-selector to use for + // mapStateToProps, depending on whether it's a function or missing. mapStateFactories: defaultMapStateFactories, // if true, the selector returned by selectorFactory will memoize its results, allowing @@ -98,42 +89,20 @@ export function buildOptions( } } -export function addStateAndDispatchSelectors(options) { - function match(factories) { +export function selectorFactory(dispatch, options) { + function match(mapToProps, factories) { for (let i = factories.length - 1; i >= 0; i--) { - const selector = factories[i](options) + const selector = factories[i](mapToProps, options) if (selector) return selector } return undefined } - return { - ...options, - getState: match(options.mapStateFactories), - getDispatch: match(options.mapDispatchFactories) - } -} + const mapStateToProps = match(options.mapStateToProps, options.mapStateFactories) + const mapDispatchToProps = match(options.mapDispatchToProps, options.mapDispatchFactories) + const factory = options.pure ? makePurePropsSelector : makeImpurePropsSelector -export function wrapWithVerify({ getState, getDispatch, mergeProps, ...options }) { - const verify = (methodName, func) => { - const withVerify = verifyPlainObject(options.displayName, methodName, func) - withVerify.meta = func.meta - return withVerify - } - return { - ...options, - getState: verify('mapStateToProps', getState), - getDispatch: verify('mapDispatchToProps', getDispatch), - mergeProps: verify('mergeProps', mergeProps) - } -} - -export function selectorFactory(dispatch, options) { - return flow( - addStateAndDispatchSelectors, - wrapWithVerify, - createFinalPropsSelector - )({ ...options, dispatch }) + return factory(dispatch, { ...options, mapStateToProps, mapDispatchToProps }) } export default function connect(...args) { diff --git a/src/selectors/getFinalProps.js b/src/selectors/getFinalProps.js deleted file mode 100644 index f11943235..000000000 --- a/src/selectors/getFinalProps.js +++ /dev/null @@ -1,83 +0,0 @@ -import memoizeProps from '../utils/memoizeProps' -import shallowEqual from '../utils/shallowEqual' - -export function createImpureFinalPropsSelector({ dispatch, getState, getDispatch, mergeProps }) { - return function impureSelector(state, props) { - return mergeProps( - getState(state, props), - getDispatch(dispatch, props), - props - ) - } -} - -export function createPureFinalPropsSelector({ dispatch, getState, getDispatch, mergeProps }) { - const memoizeResult = memoizeProps() - let result = undefined - let prevState = undefined - let prevOwnProps = undefined - let prevStateProps = undefined - let prevDispatchProps = undefined - let getStateDependsOnProps = undefined - let getDispatchDependsOnProps = undefined - - function setResult() { - result = memoizeResult(mergeProps(prevStateProps, prevDispatchProps, prevOwnProps)) - } - - function handleFirstCall(firstState, firstOwnProps) { - prevState = firstState - prevOwnProps = firstOwnProps - prevStateProps = getState(firstState, firstOwnProps) - prevDispatchProps = getDispatch(dispatch, firstOwnProps) - getStateDependsOnProps = getState.meta && getState.meta.dependsOnProps - getDispatchDependsOnProps = getDispatch.meta && getDispatch.meta.dependsOnProps - - setResult() - } - - function handleNewPropsAndMaybeNewState(nextState, nextOwnProps) { - if (getStateDependsOnProps || nextState !== prevState) { - prevStateProps = getState(nextState, nextOwnProps) - } - - if (getDispatchDependsOnProps) { - prevDispatchProps = getDispatch(dispatch, nextOwnProps) - } - - prevState = nextState - prevOwnProps = nextOwnProps - - setResult() - } - - function handleNewStateButNotNewProps(nextState) { - const nextStateProps = getState(nextState, prevOwnProps) - prevState = nextState - - if (!shallowEqual(nextStateProps, prevStateProps)) { - prevStateProps = nextStateProps - setResult() - } - } - - return function pureSelector(nextState, nextOwnProps) { - if (result === undefined) { - handleFirstCall(nextState, nextOwnProps) - - } else if (!shallowEqual(nextOwnProps, prevOwnProps)) { - handleNewPropsAndMaybeNewState(nextState, nextOwnProps) - - } else if (nextState !== prevState) { - handleNewStateButNotNewProps(nextState) - - } - return result - } -} - -export function createFinalPropsSelector(options) { - return options.pure - ? createPureFinalPropsSelector(options) - : createImpureFinalPropsSelector(options) -} diff --git a/src/selectors/makeImpurePropsSelector.js b/src/selectors/makeImpurePropsSelector.js new file mode 100644 index 000000000..5edbea267 --- /dev/null +++ b/src/selectors/makeImpurePropsSelector.js @@ -0,0 +1,31 @@ +import verifyPlainObject from '../utils/verifyPlainObject' + +export default function makeImpurePropsSelector( + dispatch, { mapStateToProps, mapDispatchToProps, mergeProps, displayName } +) { + + function impureMain(state, ownProps) { + return mergeProps( + mapStateToProps(state, ownProps), + mapDispatchToProps(dispatch, ownProps), + ownProps + ) + } + + let selector = function impureFirst(state, ownProps) { + selector = impureMain + + const stateProps = mapStateToProps(state, ownProps) + verifyPlainObject(stateProps, displayName, 'mapStateToProps') + + const dispatchProps = mapDispatchToProps(dispatch, ownProps) + verifyPlainObject(dispatchProps, displayName, 'mapDispatchToProps') + + const mergedProps = mergeProps(stateProps, dispatchProps, ownProps) + verifyPlainObject(mergedProps, displayName, 'mergeProps') + + return mergedProps + } + + return function impureProxy(state, props) { return selector(state, props) } +} diff --git a/src/selectors/makePurePropsSelector.js b/src/selectors/makePurePropsSelector.js new file mode 100644 index 000000000..1d0d63d01 --- /dev/null +++ b/src/selectors/makePurePropsSelector.js @@ -0,0 +1,95 @@ +import shallowEqual from '../utils/shallowEqual' +import verifyPlainObject from '../utils/verifyPlainObject' + +export default function makePurePropsSelector( + dispatch, { mapStateToProps, mapDispatchToProps, mergeProps, displayName } +) { + const skipShallowEqualOnMergedProps = mergeProps.meta && mergeProps.meta.skipShallowEqual + let uninitialized = true + let result + let prevState + let prevOwnProps + let prevStateProps + let prevDispatchProps + let prevMergedProps + let statePropsDependOnOwnProps + let dispatchPropsDependOnOwnProps + + function mergeFinalProps() { + const nextMergedProps = mergeProps(prevStateProps, prevDispatchProps, prevOwnProps) + + if (skipShallowEqualOnMergedProps || !shallowEqual(prevMergedProps, nextMergedProps)) { + result = nextMergedProps + } + + prevMergedProps = nextMergedProps + } + + function handleFirstCall(firstState, firstOwnProps) { + uninitialized = false + prevState = firstState + prevOwnProps = firstOwnProps + + prevStateProps = mapStateToProps(firstState, firstOwnProps) + verifyPlainObject(prevStateProps, displayName, 'mapStateToProps') + + prevDispatchProps = mapDispatchToProps(dispatch, firstOwnProps) + verifyPlainObject(prevDispatchProps, displayName, 'mapDispatchToProps') + + result = prevMergedProps = mergeProps(prevStateProps, prevDispatchProps, prevOwnProps) + verifyPlainObject(prevMergedProps, displayName, 'mergeProps') + + statePropsDependOnOwnProps = mapStateToProps.meta + ? mapStateToProps.meta.dependsOnProps + : mapStateToProps.length !== 1 + + dispatchPropsDependOnOwnProps = mapDispatchToProps.meta + ? mapDispatchToProps.meta.dependsOnProps + : mapDispatchToProps.let !== 1 + } + + function handleNewPropsAndMaybeNewState(nextState, nextOwnProps) { + if (statePropsDependOnOwnProps) { + prevStateProps = mapStateToProps(nextState, nextOwnProps) + prevState = nextState + + } else if (nextState !== prevState) { + prevStateProps = mapStateToProps(nextState) + prevState = nextState + } + + if (dispatchPropsDependOnOwnProps) { + prevDispatchProps = mapDispatchToProps(dispatch, nextOwnProps) + } + + prevOwnProps = nextOwnProps + mergeFinalProps() + } + + function handleNewStateButNotNewProps(nextState) { + prevState = nextState + + const nextStateProps = statePropsDependOnOwnProps + ? mapStateToProps(nextState, prevOwnProps) + : mapStateToProps(nextState) + + if (!shallowEqual(nextStateProps, prevStateProps)) { + prevStateProps = nextStateProps + mergeFinalProps() + } + } + + return function pureSelector(nextState, nextOwnProps) { + if (uninitialized) { + handleFirstCall(nextState, nextOwnProps) + + } else if (!shallowEqual(nextOwnProps, prevOwnProps)) { + handleNewPropsAndMaybeNewState(nextState, nextOwnProps) + + } else if (nextState !== prevState) { + handleNewStateButNotNewProps(nextState) + + } + return result + } +} diff --git a/src/selectors/mapDispatchToProps.js b/src/selectors/mapDispatchToProps.js index bdb7265be..299b5b518 100644 --- a/src/selectors/mapDispatchToProps.js +++ b/src/selectors/mapDispatchToProps.js @@ -1,21 +1,23 @@ import { bindActionCreators } from 'redux' import createMapOrMapFactoryProxy from './createMapOrMapFactoryProxy' -export function whenMapDispatchToPropsIsMissing({ mapDispatchToProps, dispatch }) { +export function whenMapDispatchToPropsIsMissing(mapDispatchToProps) { if (!mapDispatchToProps) { - const dispatchProp = { dispatch } - return function justDispatch() { return dispatchProp } + return function justDispatch(dispatch) { + return { dispatch } + } } } -export function whenMapDispatchToPropsIsObject({ mapDispatchToProps, dispatch }) { +export function whenMapDispatchToPropsIsObject(mapDispatchToProps) { if (mapDispatchToProps && typeof mapDispatchToProps === 'object') { - const bound = bindActionCreators(mapDispatchToProps, dispatch) - return function boundActionCreators() { return bound } + return function boundActionCreators(dispatch) { + return bindActionCreators(mapDispatchToProps, dispatch) + } } } -export function whenMapDispatchToPropsIsFunction({ mapDispatchToProps }) { +export function whenMapDispatchToPropsIsFunction(mapDispatchToProps) { if (typeof mapDispatchToProps === 'function') { return createMapOrMapFactoryProxy(mapDispatchToProps) } diff --git a/src/selectors/mapStateToProps.js b/src/selectors/mapStateToProps.js index 304b0f450..757d690f4 100644 --- a/src/selectors/mapStateToProps.js +++ b/src/selectors/mapStateToProps.js @@ -1,13 +1,17 @@ import createMapOrMapFactoryProxy from './createMapOrMapFactoryProxy' -export function whenMapStateToPropsIsMissing({ mapStateToProps }) { +export function whenMapStateToPropsIsMissing(mapStateToProps) { if (!mapStateToProps) { const empty = {} - return function emptyState() { return empty } + // The state arg is to keep the args count equal to 1 so that it's + // detected as not depends on props + return function emptyState(state) { // eslint-disable-line no-unused-vars + return empty + } } } -export function whenMapStateToPropsIsFunction({ mapStateToProps }) { +export function whenMapStateToPropsIsFunction(mapStateToProps) { if (typeof mapStateToProps === 'function') { return createMapOrMapFactoryProxy(mapStateToProps) } diff --git a/src/selectors/mergeProps.js b/src/selectors/mergeProps.js index 4eed9c058..73c80575b 100644 --- a/src/selectors/mergeProps.js +++ b/src/selectors/mergeProps.js @@ -6,3 +6,4 @@ export function defaultMergeProps(stateProps, dispatchProps, ownProps) { ...dispatchProps } } +defaultMergeProps.meta = { skipShallowEqual: true } diff --git a/src/utils/memoizeProps.js b/src/utils/memoizeProps.js deleted file mode 100644 index 061e431cd..000000000 --- a/src/utils/memoizeProps.js +++ /dev/null @@ -1,22 +0,0 @@ -import shallowEqual from './shallowEqual' - -const equal = shallowEqual -// wrap the source props in a shallow equals because props objects with same properties are -// semantically equal in the eyes of React... no need to return a new object. -export default function memoizeProps() { - let prevProps = undefined - let result = undefined - - return function memoize(nextProps) { - if (nextProps === prevProps) { - return nextProps - } - - if (result === undefined || !equal(prevProps, nextProps)) { - return result = prevProps = nextProps - } - - prevProps = nextProps - return result - } -} diff --git a/src/utils/verifyPlainObject.js b/src/utils/verifyPlainObject.js index 5608e6822..6291c1b08 100644 --- a/src/utils/verifyPlainObject.js +++ b/src/utils/verifyPlainObject.js @@ -1,22 +1,12 @@ import isPlainObject from 'lodash/isPlainObject' import warning from './warning' -// verifies that the first execution of func returns a plain object -export default function verifyPlainObject(displayName, methodName, func) { - if (process.env.NODE_ENV === 'production') return func - if (!func) throw new Error('Missing ' + methodName) - - let hasVerified = false - return function verify(...args) { - const result = func(...args) - if (hasVerified) return result - hasVerified = true - if (!isPlainObject(result)) { +export default function verifyPlainObject(value, displayName, methodName) { + if (process.env.NODE_ENV !== 'production') { + if (!isPlainObject(value)) { warning( - `${methodName}() in ${displayName} must return a plain object. ` + - `Instead received ${result}.` + `${methodName}() in ${displayName} must return a plain object. Instead received ${value}.` ) } - return result } } From 5ec814388c228a1220a6dbdcd2494862075da66a Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Wed, 13 Jul 2016 17:45:05 -0400 Subject: [PATCH 66/76] refactoring/optimizing connect+selectors --- src/components/connect.js | 74 +++++-------- src/selectors/createMapOrMapFactoryProxy.js | 24 ----- src/selectors/createMapToPropsProxy.js | 56 ++++++++++ src/selectors/makeImpurePropsSelector.js | 31 ------ src/selectors/makePurePropsSelector.js | 95 ---------------- src/selectors/mapDispatchToProps.js | 36 ++++--- src/selectors/mapStateToProps.js | 26 +++-- src/selectors/mergeProps.js | 34 ++++++ src/selectors/selectorFactory.js | 114 ++++++++++++++++++++ src/utils/verifyPlainObject.js | 10 +- 10 files changed, 263 insertions(+), 237 deletions(-) delete mode 100644 src/selectors/createMapOrMapFactoryProxy.js create mode 100644 src/selectors/createMapToPropsProxy.js delete mode 100644 src/selectors/makeImpurePropsSelector.js delete mode 100644 src/selectors/makePurePropsSelector.js create mode 100644 src/selectors/selectorFactory.js diff --git a/src/components/connect.js b/src/components/connect.js index 1945e6ad2..9f2759dc7 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -1,9 +1,8 @@ import connectAdvanced from './connectAdvanced' -import makeImpurePropsSelector from '../selectors/makeImpurePropsSelector' -import makePurePropsSelector from '../selectors/makePurePropsSelector' -import defaultMapDispatchFactories from '../selectors/mapDispatchToProps' -import defaultMapStateFactories from '../selectors/mapStateToProps' -import { defaultMergeProps } from '../selectors/mergeProps' +import defaultMapDispatchToPropsFactories from '../selectors/mapDispatchToProps' +import defaultMapStateToPropsFactories from '../selectors/mapStateToProps' +import defaultMergePropsFactories from '../selectors/mergeProps' +import selectorFactory from '../selectors/selectorFactory' /* connect combines mapStateToProps, mapDispatchToProps, and mergeProps into a final selector that @@ -43,23 +42,20 @@ export function buildOptions( mapStateToProps, mapDispatchToProps, mergeProps, - { - pure = true, - withRef // ...options - } = {}) { + { pure = true, ...options } = {} +) { return { - // used to compute the Connect component's displayName from the wrapped component's displayName. - getDisplayName: name => `Connect(${name})`, + // passed through to selectorFactory + mapStateToProps, + mapStateToPropsFactories: defaultMapStateToPropsFactories, - // passed through to selectorFactory. defaults to the array of funcs returned in - // mapDispatchToProps.js that determine the appropriate sub-selector to use for - // mapDispatchToProps, depending on whether it's a function, object, or missing. - mapDispatchFactories: defaultMapDispatchFactories, + // passed through to selectorFactory + mapDispatchToProps, + mapDispatchToPropsFactories: defaultMapDispatchToPropsFactories, - // passed through to selectorFactory. defaults to the array of funcs returned in - // mapStateToProps.js that determine the appropriate sub-selector to use for - // mapStateToProps, depending on whether it's a function or missing. - mapStateFactories: defaultMapStateFactories, + // passed through to selectorFactory + mergeProps, + mergePropsFactories: defaultMergePropsFactories, // if true, the selector returned by selectorFactory will memoize its results, allowing // connectAdvanced's shouldComponentUpdate to return false if final props have not changed. @@ -67,42 +63,20 @@ export function buildOptions( // return true. pure, - // in addition to setting withRef, pure, storeKey, and renderCountProp, options can override - // getDisplayName, mapDispatchFactories, or mapStateFactories. - // TODO: REPLACE WITH ...OPTIONS ONCE IT'S OK TO EXPOSE NEW FUNCTIONALITY. - withRef, - - // passed through to selectorFactory - mapStateToProps, + // used to compute the Connect component's displayName from the wrapped component's displayName. + getDisplayName: name => `Connect(${name})`, - // passed through to selectorFactory - mapDispatchToProps, + // if mapStateToProps is not given a value, the Connect component doesn't subscribe to the store + shouldHandleStateChanges: Boolean(mapStateToProps), - // passed through to selectorFactory - mergeProps: mergeProps || defaultMergeProps, + // in addition to setting withRef, pure, storeKey, and renderCountProp, options can override + // getDisplayName, mapDispatchFactories, or mapStateFactories. + // TODO: REPLACE with ...options ONCE IT'S OK TO EXPOSE NEW FUNCTIONALITY. + withRef: options.withRef, // ...options, // used in error messages - methodName: 'connect', - - // if mapStateToProps is not given a value, the Connect component doesn't subscribe to the store - shouldHandleStateChanges: Boolean(mapStateToProps) - } -} - -export function selectorFactory(dispatch, options) { - function match(mapToProps, factories) { - for (let i = factories.length - 1; i >= 0; i--) { - const selector = factories[i](mapToProps, options) - if (selector) return selector - } - return undefined + methodName: 'connect' } - - const mapStateToProps = match(options.mapStateToProps, options.mapStateFactories) - const mapDispatchToProps = match(options.mapDispatchToProps, options.mapDispatchFactories) - const factory = options.pure ? makePurePropsSelector : makeImpurePropsSelector - - return factory(dispatch, { ...options, mapStateToProps, mapDispatchToProps }) } export default function connect(...args) { diff --git a/src/selectors/createMapOrMapFactoryProxy.js b/src/selectors/createMapOrMapFactoryProxy.js deleted file mode 100644 index 129814546..000000000 --- a/src/selectors/createMapOrMapFactoryProxy.js +++ /dev/null @@ -1,24 +0,0 @@ -// Detects if the first result of mapStateToProps or mapDispatchToProps is a -// function, and uses that as the real function on subsequent calls. -export default function createMapOrMapFactoryProxy(mapToProps) { - const meta = { - dependsOnProps: mapToProps.length !== 1 - } - - let actualMapToProps = function firstRun(storePart, props) { - const firstResult = mapToProps(storePart, props) - - if (typeof firstResult === 'function') { - actualMapToProps = firstResult - meta.dependsOnProps = firstResult.length !== 1 - return firstResult(storePart, props) - } else { - actualMapToProps = mapToProps - return firstResult - } - } - - function mapToPropsProxy(storePart, props) { return actualMapToProps(storePart, props) } - mapToPropsProxy.meta = meta - return mapToPropsProxy -} diff --git a/src/selectors/createMapToPropsProxy.js b/src/selectors/createMapToPropsProxy.js new file mode 100644 index 000000000..70328a78b --- /dev/null +++ b/src/selectors/createMapToPropsProxy.js @@ -0,0 +1,56 @@ +import verifyPlainObject from '../utils/verifyPlainObject' + +// Used by whenMapStateToPropsIsFunction and whenMapDispatchToPropsIsFunction, +// this function wraps mapToProps in a proxy function which does several things: +// +// * Detects whether the mapToProps function being called depends on props, which +// is used by selectorFactory to decide if it should reinvoke on props changes. +// +// * On first call, handles mapToProps if returns another function, and treats that +// new function as the true mapToProps for subsequent calls. +// +// * On first call, verifies the first result is a plain object, in order to warn +// the developer that their mapToProps function is not returning a valid result. +// +export default function createMapToPropsProxy(mapToProps, displayName, methodName) { + + // meta.dependsOnProps is used by this function to determine whether to pass + // props as args to the mapToProps function being wrapped. It is also used by + // selectorFactory to determine whether this function needs to be invoked when + // props have changed. + // + // If the original mapToProps function does not have its own meta property, one + // will be created with dependsOnProps based on mapToProps's length (number of + // arguments). A length of one signals that mapToProps does not depend on props + // being passed from the parent component. + // + const detectedMeta = { dependsOnProps: mapToProps.length !== 1 } + const proxyMeta = mapToProps.meta || detectedMeta + + let proxiedMapToProps + function mapToPropsProxy(stateOrDispatch, ownProps) { + return proxyMeta.dependsOnProps + ? proxiedMapToProps(stateOrDispatch, ownProps) + : proxiedMapToProps(stateOrDispatch) + } + mapToPropsProxy.meta = proxyMeta + + function detectFactoryAndVerify(stateOrDispatch, ownProps) { + proxiedMapToProps = mapToProps + let result = mapToPropsProxy(stateOrDispatch, ownProps) + + if (typeof result === 'function') { + proxiedMapToProps = result + detectedMeta.dependsOnProps = result.length !== 1 + result = mapToPropsProxy(stateOrDispatch, ownProps) + } + + if (process.env.NODE_ENV !== 'production') + verifyPlainObject(result, displayName, methodName) + + return result + } + + proxiedMapToProps = detectFactoryAndVerify + return mapToPropsProxy +} diff --git a/src/selectors/makeImpurePropsSelector.js b/src/selectors/makeImpurePropsSelector.js deleted file mode 100644 index 5edbea267..000000000 --- a/src/selectors/makeImpurePropsSelector.js +++ /dev/null @@ -1,31 +0,0 @@ -import verifyPlainObject from '../utils/verifyPlainObject' - -export default function makeImpurePropsSelector( - dispatch, { mapStateToProps, mapDispatchToProps, mergeProps, displayName } -) { - - function impureMain(state, ownProps) { - return mergeProps( - mapStateToProps(state, ownProps), - mapDispatchToProps(dispatch, ownProps), - ownProps - ) - } - - let selector = function impureFirst(state, ownProps) { - selector = impureMain - - const stateProps = mapStateToProps(state, ownProps) - verifyPlainObject(stateProps, displayName, 'mapStateToProps') - - const dispatchProps = mapDispatchToProps(dispatch, ownProps) - verifyPlainObject(dispatchProps, displayName, 'mapDispatchToProps') - - const mergedProps = mergeProps(stateProps, dispatchProps, ownProps) - verifyPlainObject(mergedProps, displayName, 'mergeProps') - - return mergedProps - } - - return function impureProxy(state, props) { return selector(state, props) } -} diff --git a/src/selectors/makePurePropsSelector.js b/src/selectors/makePurePropsSelector.js deleted file mode 100644 index 1d0d63d01..000000000 --- a/src/selectors/makePurePropsSelector.js +++ /dev/null @@ -1,95 +0,0 @@ -import shallowEqual from '../utils/shallowEqual' -import verifyPlainObject from '../utils/verifyPlainObject' - -export default function makePurePropsSelector( - dispatch, { mapStateToProps, mapDispatchToProps, mergeProps, displayName } -) { - const skipShallowEqualOnMergedProps = mergeProps.meta && mergeProps.meta.skipShallowEqual - let uninitialized = true - let result - let prevState - let prevOwnProps - let prevStateProps - let prevDispatchProps - let prevMergedProps - let statePropsDependOnOwnProps - let dispatchPropsDependOnOwnProps - - function mergeFinalProps() { - const nextMergedProps = mergeProps(prevStateProps, prevDispatchProps, prevOwnProps) - - if (skipShallowEqualOnMergedProps || !shallowEqual(prevMergedProps, nextMergedProps)) { - result = nextMergedProps - } - - prevMergedProps = nextMergedProps - } - - function handleFirstCall(firstState, firstOwnProps) { - uninitialized = false - prevState = firstState - prevOwnProps = firstOwnProps - - prevStateProps = mapStateToProps(firstState, firstOwnProps) - verifyPlainObject(prevStateProps, displayName, 'mapStateToProps') - - prevDispatchProps = mapDispatchToProps(dispatch, firstOwnProps) - verifyPlainObject(prevDispatchProps, displayName, 'mapDispatchToProps') - - result = prevMergedProps = mergeProps(prevStateProps, prevDispatchProps, prevOwnProps) - verifyPlainObject(prevMergedProps, displayName, 'mergeProps') - - statePropsDependOnOwnProps = mapStateToProps.meta - ? mapStateToProps.meta.dependsOnProps - : mapStateToProps.length !== 1 - - dispatchPropsDependOnOwnProps = mapDispatchToProps.meta - ? mapDispatchToProps.meta.dependsOnProps - : mapDispatchToProps.let !== 1 - } - - function handleNewPropsAndMaybeNewState(nextState, nextOwnProps) { - if (statePropsDependOnOwnProps) { - prevStateProps = mapStateToProps(nextState, nextOwnProps) - prevState = nextState - - } else if (nextState !== prevState) { - prevStateProps = mapStateToProps(nextState) - prevState = nextState - } - - if (dispatchPropsDependOnOwnProps) { - prevDispatchProps = mapDispatchToProps(dispatch, nextOwnProps) - } - - prevOwnProps = nextOwnProps - mergeFinalProps() - } - - function handleNewStateButNotNewProps(nextState) { - prevState = nextState - - const nextStateProps = statePropsDependOnOwnProps - ? mapStateToProps(nextState, prevOwnProps) - : mapStateToProps(nextState) - - if (!shallowEqual(nextStateProps, prevStateProps)) { - prevStateProps = nextStateProps - mergeFinalProps() - } - } - - return function pureSelector(nextState, nextOwnProps) { - if (uninitialized) { - handleFirstCall(nextState, nextOwnProps) - - } else if (!shallowEqual(nextOwnProps, prevOwnProps)) { - handleNewPropsAndMaybeNewState(nextState, nextOwnProps) - - } else if (nextState !== prevState) { - handleNewStateButNotNewProps(nextState) - - } - return result - } -} diff --git a/src/selectors/mapDispatchToProps.js b/src/selectors/mapDispatchToProps.js index 299b5b518..703f46e12 100644 --- a/src/selectors/mapDispatchToProps.js +++ b/src/selectors/mapDispatchToProps.js @@ -1,26 +1,28 @@ import { bindActionCreators } from 'redux' -import createMapOrMapFactoryProxy from './createMapOrMapFactoryProxy' +import createMapToPropsProxy from './createMapToPropsProxy' -export function whenMapDispatchToPropsIsMissing(mapDispatchToProps) { - if (!mapDispatchToProps) { - return function justDispatch(dispatch) { - return { dispatch } - } - } +export function whenMapDispatchToPropsIsMissing({ mapDispatchToProps, dispatch }) { + if (mapDispatchToProps) return undefined + + const props = { dispatch } + function justDispatch() { return props } + justDispatch.meta = { dependsOnProps: false } + return justDispatch } -export function whenMapDispatchToPropsIsObject(mapDispatchToProps) { - if (mapDispatchToProps && typeof mapDispatchToProps === 'object') { - return function boundActionCreators(dispatch) { - return bindActionCreators(mapDispatchToProps, dispatch) - } - } +export function whenMapDispatchToPropsIsObject({ mapDispatchToProps, dispatch }) { + if (!mapDispatchToProps || typeof mapDispatchToProps !== 'object') return undefined + + const props = bindActionCreators(mapDispatchToProps, dispatch) + function boundActionCreators() { return props } + boundActionCreators.meta = { dependsOnProps: false } + return boundActionCreators } -export function whenMapDispatchToPropsIsFunction(mapDispatchToProps) { - if (typeof mapDispatchToProps === 'function') { - return createMapOrMapFactoryProxy(mapDispatchToProps) - } +export function whenMapDispatchToPropsIsFunction({ mapDispatchToProps, displayName }) { + return typeof mapDispatchToProps === 'function' + ? createMapToPropsProxy(mapDispatchToProps, displayName, 'mapDispatchToProps') + : undefined } export default [ diff --git a/src/selectors/mapStateToProps.js b/src/selectors/mapStateToProps.js index 757d690f4..062256267 100644 --- a/src/selectors/mapStateToProps.js +++ b/src/selectors/mapStateToProps.js @@ -1,20 +1,18 @@ -import createMapOrMapFactoryProxy from './createMapOrMapFactoryProxy' +import createMapToPropsProxy from './createMapToPropsProxy' -export function whenMapStateToPropsIsMissing(mapStateToProps) { - if (!mapStateToProps) { - const empty = {} - // The state arg is to keep the args count equal to 1 so that it's - // detected as not depends on props - return function emptyState(state) { // eslint-disable-line no-unused-vars - return empty - } - } +export function whenMapStateToPropsIsMissing({ mapStateToProps }) { + if (mapStateToProps) return undefined + + const empty = {} + function emptyState() { return empty } + emptyState.meta = { dependsOnProps: false } + return emptyState } -export function whenMapStateToPropsIsFunction(mapStateToProps) { - if (typeof mapStateToProps === 'function') { - return createMapOrMapFactoryProxy(mapStateToProps) - } +export function whenMapStateToPropsIsFunction({ mapStateToProps, displayName }) { + return typeof mapStateToProps === 'function' + ? createMapToPropsProxy(mapStateToProps, displayName, 'mapStateToProps') + : undefined } export default [ diff --git a/src/selectors/mergeProps.js b/src/selectors/mergeProps.js index 73c80575b..b29b23bee 100644 --- a/src/selectors/mergeProps.js +++ b/src/selectors/mergeProps.js @@ -1,3 +1,4 @@ +import verifyPlainObject from '../utils/verifyPlainObject' export function defaultMergeProps(stateProps, dispatchProps, ownProps) { return { @@ -7,3 +8,36 @@ export function defaultMergeProps(stateProps, dispatchProps, ownProps) { } } defaultMergeProps.meta = { skipShallowEqual: true } + +export function whenMergePropsIsMissing({ mergeProps }) { + return mergeProps + ? undefined + : defaultMergeProps +} + +export function whenMergePropsIsFunction({ mergeProps, displayName }) { + if (typeof mergeProps !== 'function') + return undefined + + let hasVerifiedOnce = false + + function mergePropsProxy(stateProps, dispatchProps, ownProps) { + const mergedProps = mergeProps(stateProps, dispatchProps, ownProps) + + if (process.env.NODE_ENV !== 'production') { + if (!hasVerifiedOnce) { + hasVerifiedOnce = true + verifyPlainObject(mergedProps, displayName, 'mergeProps') + } + } + + return mergedProps + } + mergePropsProxy.meta = mergeProps.meta || { skipShallowEqual: false } + return mergePropsProxy +} + +export default [ + whenMergePropsIsMissing, + whenMergePropsIsFunction +] diff --git a/src/selectors/selectorFactory.js b/src/selectors/selectorFactory.js new file mode 100644 index 000000000..ddd45e444 --- /dev/null +++ b/src/selectors/selectorFactory.js @@ -0,0 +1,114 @@ +import shallowEqual from '../utils/shallowEqual' + +export function makeImpurePropsSelector( + dispatch, { mapStateToProps, mapDispatchToProps, mergeProps } +) { + return function impureSelector(state, ownProps) { + return mergeProps( + mapStateToProps(state, ownProps), + mapDispatchToProps(dispatch, ownProps), + ownProps + ) + } +} + +export function makePurePropsSelector( + dispatch, { mapStateToProps, mapDispatchToProps, mergeProps } +) { + let hasRunAtLeastOnce = false + let state + let ownProps + let stateProps + let dispatchProps + let mergedProps + + function handleFirstCall(firstState, firstOwnProps) { + state = firstState + ownProps = firstOwnProps + stateProps = mapStateToProps(state, ownProps) + dispatchProps = mapDispatchToProps(dispatch, ownProps) + mergedProps = mergeProps(stateProps, dispatchProps, ownProps) + hasRunAtLeastOnce = true + return mergedProps + } + + function mergeFinalProps() { + const nextMergedProps = mergeProps(stateProps, dispatchProps, ownProps) + + if (mergeProps.meta.skipShallowEqual || !shallowEqual(mergedProps, nextMergedProps)) + mergedProps = nextMergedProps + + return mergedProps + } + + function handleNewPropsAndNewState() { + stateProps = mapStateToProps(state, ownProps) + + if (mapDispatchToProps.meta.dependsOnProps) + dispatchProps = mapDispatchToProps(dispatch, ownProps) + + return mergeFinalProps() + } + + function handleNewProps() { + if (mapStateToProps.meta.dependsOnProps) + stateProps = mapStateToProps(state, ownProps) + + if (mapDispatchToProps.meta.dependsOnProps) + dispatchProps = mapDispatchToProps(dispatch, ownProps) + + return mergeFinalProps() + } + + function handleNewState() { + const nextStateProps = mapStateToProps(state, ownProps) + const statePropsChanged = !shallowEqual(nextStateProps, stateProps) + stateProps = nextStateProps + + return statePropsChanged + ? mergeFinalProps() + : mergedProps + } + + function handleSubsequentCalls(nextState, nextOwnProps) { + const propsChanged = !shallowEqual(nextOwnProps, ownProps) + const stateChanged = nextState !== state + state = nextState + ownProps = nextOwnProps + + if (propsChanged && stateChanged) return handleNewPropsAndNewState() + if (propsChanged) return handleNewProps() + if (stateChanged) return handleNewState() + return mergedProps + } + + return function pureSelector(nextState, nextOwnProps) { + return hasRunAtLeastOnce + ? handleSubsequentCalls(nextState, nextOwnProps) + : handleFirstCall(nextState, nextOwnProps) + } +} + +export function findMatchingSelector(name, options) { + const factories = options[name + 'Factories'] + + for (let i = factories.length - 1; i >= 0; i--) { + const selector = factories[i](options) + if (selector) return selector + } + + throw new Error(`Unexpected value for ${name} in ${options.displayName}.`) +} + +export default function selectorFactory(dispatch, options) { + const finalOptions = { + ...options, + mapStateToProps: findMatchingSelector('mapStateToProps', options), + mapDispatchToProps: findMatchingSelector('mapDispatchToProps', { ...options, dispatch }), + mergeProps: findMatchingSelector('mergeProps', options) + } + + return options.pure + ? makePurePropsSelector(dispatch, finalOptions) + : makeImpurePropsSelector(dispatch, finalOptions) +} diff --git a/src/utils/verifyPlainObject.js b/src/utils/verifyPlainObject.js index 6291c1b08..a56e1c6de 100644 --- a/src/utils/verifyPlainObject.js +++ b/src/utils/verifyPlainObject.js @@ -2,11 +2,9 @@ import isPlainObject from 'lodash/isPlainObject' import warning from './warning' export default function verifyPlainObject(value, displayName, methodName) { - if (process.env.NODE_ENV !== 'production') { - if (!isPlainObject(value)) { - warning( - `${methodName}() in ${displayName} must return a plain object. Instead received ${value}.` - ) - } + if (!isPlainObject(value)) { + warning( + `${methodName}() in ${displayName} must return a plain object. Instead received ${value}.` + ) } } From 1120555b76cae2876307b949f5e614804052b56c Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 14 Jul 2016 01:25:56 -0400 Subject: [PATCH 67/76] Merge selectors and connect.js into a connect folder --- src/{components => connect}/connect.js | 0 src/{selectors => connect}/createMapToPropsProxy.js | 0 src/{selectors => connect}/mapDispatchToProps.js | 0 src/{selectors => connect}/mapStateToProps.js | 0 src/{selectors => connect}/mergeProps.js | 0 src/{selectors => connect}/selectorFactory.js | 0 src/index.js | 2 +- 7 files changed, 1 insertion(+), 1 deletion(-) rename src/{components => connect}/connect.js (100%) rename src/{selectors => connect}/createMapToPropsProxy.js (100%) rename src/{selectors => connect}/mapDispatchToProps.js (100%) rename src/{selectors => connect}/mapStateToProps.js (100%) rename src/{selectors => connect}/mergeProps.js (100%) rename src/{selectors => connect}/selectorFactory.js (100%) diff --git a/src/components/connect.js b/src/connect/connect.js similarity index 100% rename from src/components/connect.js rename to src/connect/connect.js diff --git a/src/selectors/createMapToPropsProxy.js b/src/connect/createMapToPropsProxy.js similarity index 100% rename from src/selectors/createMapToPropsProxy.js rename to src/connect/createMapToPropsProxy.js diff --git a/src/selectors/mapDispatchToProps.js b/src/connect/mapDispatchToProps.js similarity index 100% rename from src/selectors/mapDispatchToProps.js rename to src/connect/mapDispatchToProps.js diff --git a/src/selectors/mapStateToProps.js b/src/connect/mapStateToProps.js similarity index 100% rename from src/selectors/mapStateToProps.js rename to src/connect/mapStateToProps.js diff --git a/src/selectors/mergeProps.js b/src/connect/mergeProps.js similarity index 100% rename from src/selectors/mergeProps.js rename to src/connect/mergeProps.js diff --git a/src/selectors/selectorFactory.js b/src/connect/selectorFactory.js similarity index 100% rename from src/selectors/selectorFactory.js rename to src/connect/selectorFactory.js diff --git a/src/index.js b/src/index.js index ad89eec2d..2384a4428 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ import Provider from './components/Provider' -import connect from './components/connect' +import connect from './connect/connect' export { Provider, connect } From 55b49b2503121c234d783eeeaf1f896b0453332b Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 14 Jul 2016 02:13:38 -0400 Subject: [PATCH 68/76] comments --- src/connect/connect.js | 130 ++++++++++++++------------------- src/connect/selectorFactory.js | 12 ++- 2 files changed, 66 insertions(+), 76 deletions(-) diff --git a/src/connect/connect.js b/src/connect/connect.js index 9f2759dc7..29463b066 100644 --- a/src/connect/connect.js +++ b/src/connect/connect.js @@ -1,85 +1,67 @@ -import connectAdvanced from './connectAdvanced' -import defaultMapDispatchToPropsFactories from '../selectors/mapDispatchToProps' -import defaultMapStateToPropsFactories from '../selectors/mapStateToProps' -import defaultMergePropsFactories from '../selectors/mergeProps' -import selectorFactory from '../selectors/selectorFactory' +import connectAdvanced from '../components/connectAdvanced' +import defaultMapDispatchToPropsFactories from './mapDispatchToProps' +import defaultMapStateToPropsFactories from './mapStateToProps' +import defaultMergePropsFactories from './mergeProps' +import defaultSelectorFactory from './selectorFactory' /* - connect combines mapStateToProps, mapDispatchToProps, and mergeProps into a final selector that - is compatible with connectAdvanced. The functions in the selectors folder are the individual - pieces of that final selector. + connect is a facade over connectAdvanced. It turns its args into a compatible + selectorFactory, which has the signature: - First, buildOptions combines its args with some meta into an options object that's passed to - connectAdvanced, which will pass a modified* version of that options object to selectorFactory. + (dispatch, options) => (nextState, nextOwnProps) => nextFinalProps + + connect passes its args to connectAdvanced as options, which will in turn pass them to + selectorFactory each time a Connect component instance is instantiated or hot reloaded. - *values added to options: displayName, WrappedComponent + selectorFactory returns a final props selector from its mapStateToProps, + mapStateToPropsFactories, mapDispatchToProps, mapDispatchToPropsFactories, mergeProps, + mergePropsFactories, and pure args. - Each time selectorFactory is called (whenever an instance of the component in connectAdvanced is - constructed or hot reloaded), it uses the modified options object to build a selector function: - - 1. Convert mapStateToProps into a selector by matching it to the mapStateFactories array - passed in from buildOptions. - - The default behaviors (from mapStateToProps.js) check mapStateToProps for a function - or missing value. This could be overridden by supplying a custom value to the - mapStateFactories property of connect's options argument - - 2. Convert mapDispatchToProps into a selector by matching it to the mapDispatchFactories - array passed in from buildOptions. - - The default behaviors (from mapDispatchToProps.js) check mapDispatchToProps for a - function, object, or missing value. This could be overridden by supplying a custom - value to the mapDispatchFactories property of connect's options argument. - - 3. Combine mapStateToProps, mapDispatchToProps, and mergeProps selectors into either a pure - (makePurePropsSelector.js) or impure (makeImpurePropsSelector.js) final props selector. - - The resulting final props selector is called by the component instance whenever it receives new - props or is notified by the store subscription. + The resulting final props selector is called by the Connect component instance whenever + it receives new props or store state. */ - -export function buildOptions( +export default function connect( mapStateToProps, mapDispatchToProps, mergeProps, - { pure = true, ...options } = {} + { + mapStateToPropsFactories = defaultMapStateToPropsFactories, + mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, + mergePropsFactories = defaultMergePropsFactories, + selectorFactory = defaultSelectorFactory, + ...options + } = {} ) { - return { - // passed through to selectorFactory - mapStateToProps, - mapStateToPropsFactories: defaultMapStateToPropsFactories, - - // passed through to selectorFactory - mapDispatchToProps, - mapDispatchToPropsFactories: defaultMapDispatchToPropsFactories, - - // passed through to selectorFactory - mergeProps, - mergePropsFactories: defaultMergePropsFactories, - - // if true, the selector returned by selectorFactory will memoize its results, allowing - // connectAdvanced's shouldComponentUpdate to return false if final props have not changed. - // if false, the selector will always return a new object and shouldComponentUpdate will always - // return true. - pure, - - // used to compute the Connect component's displayName from the wrapped component's displayName. - getDisplayName: name => `Connect(${name})`, - - // if mapStateToProps is not given a value, the Connect component doesn't subscribe to the store - shouldHandleStateChanges: Boolean(mapStateToProps), - - // in addition to setting withRef, pure, storeKey, and renderCountProp, options can override - // getDisplayName, mapDispatchFactories, or mapStateFactories. - // TODO: REPLACE with ...options ONCE IT'S OK TO EXPOSE NEW FUNCTIONALITY. - withRef: options.withRef, // ...options, - - // used in error messages - methodName: 'connect' - } -} - -export default function connect(...args) { - const options = buildOptions(...args) - return connectAdvanced(selectorFactory, options) + // !!!!!!!!!! TEMPORARY DISABLING OF NEW CUSTOMIZATION FEATURES !!!!!!!!!! + mapStateToPropsFactories = defaultMapStateToPropsFactories + mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories + mergePropsFactories = defaultMergePropsFactories + selectorFactory = defaultSelectorFactory + options = { pure: options.pure, withRef: options.withRef } + // !!!!!!!!!! REMOVE THESE STATEMENTS ONCE APPROVED !!!!!!!!!! + + return connectAdvanced( + selectorFactory, + { + // used in error messages + methodName: 'connect', + + // used to compute Connect's displayName from the wrapped component's displayName. + getDisplayName: name => `Connect(${name})`, + + // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes + shouldHandleStateChanges: Boolean(mapStateToProps), + + // any addional options args can override defaults of connect or connectAdvanced + ...options, + + // passed through to selectorFactory + mapStateToProps, + mapStateToPropsFactories, + mapDispatchToProps, + mapDispatchToPropsFactories, + mergeProps, + mergePropsFactories + } + ) } diff --git a/src/connect/selectorFactory.js b/src/connect/selectorFactory.js index ddd45e444..56e17d100 100644 --- a/src/connect/selectorFactory.js +++ b/src/connect/selectorFactory.js @@ -3,6 +3,7 @@ import shallowEqual from '../utils/shallowEqual' export function makeImpurePropsSelector( dispatch, { mapStateToProps, mapDispatchToProps, mergeProps } ) { + // TODO: cache mapDispatchToProps result if not dependent on ownProps return function impureSelector(state, ownProps) { return mergeProps( mapStateToProps(state, ownProps), @@ -100,7 +101,14 @@ export function findMatchingSelector(name, options) { throw new Error(`Unexpected value for ${name} in ${options.displayName}.`) } -export default function selectorFactory(dispatch, options) { +// TODO: Add more comments + +// If pure is true, the selector returned by selectorFactory will memoize its results, +// allowing connectAdvanced's shouldComponentUpdate to return false if final +// props have not changed. If false, the selector will always return a new +// object and shouldComponentUpdate will always return true. + +export default function selectorFactory(dispatch, { pure = true, ...options }) { const finalOptions = { ...options, mapStateToProps: findMatchingSelector('mapStateToProps', options), @@ -108,7 +116,7 @@ export default function selectorFactory(dispatch, options) { mergeProps: findMatchingSelector('mergeProps', options) } - return options.pure + return pure ? makePurePropsSelector(dispatch, finalOptions) : makeImpurePropsSelector(dispatch, finalOptions) } From 86a4bbe6a488df2c36f28800fdab6e88d3f6e328 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Fri, 15 Jul 2016 17:56:37 -0400 Subject: [PATCH 69/76] refactoring connect --- src/components/connectAdvanced.js | 2 +- src/connect/connect.js | 63 ++++++++++------- src/connect/createMapToPropsProxy.js | 56 --------------- src/connect/mapDispatchToProps.js | 32 ++++----- src/connect/mapStateToProps.js | 23 +++--- src/connect/mergeProps.js | 63 +++++++++-------- src/connect/selectorFactory.js | 102 +++++++++++++++------------ src/connect/verifySubselectors.js | 20 ++++++ src/connect/wrapMapToProps.js | 70 ++++++++++++++++++ 9 files changed, 241 insertions(+), 190 deletions(-) delete mode 100644 src/connect/createMapToPropsProxy.js create mode 100644 src/connect/verifySubselectors.js create mode 100644 src/connect/wrapMapToProps.js diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 60bb47a78..2f6041fc2 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -20,7 +20,7 @@ export default function connectAdvanced( outside of their selector as an optimization. Options passed to connectAdvanced are passed to the selectorFactory, along with displayName and WrappedComponent, as the second argument. - Note that selectorFactory is responisible for all caching/memoization of inbound and outbound + Note that selectorFactory is responsible for all caching/memoization of inbound and outbound props. Do not use connectAdvanced directly without memoizing results between calls to your selector, otherwise the Connect component will re-render on every state or props change. */ diff --git a/src/connect/connect.js b/src/connect/connect.js index 29463b066..acdb933b6 100644 --- a/src/connect/connect.js +++ b/src/connect/connect.js @@ -29,39 +29,48 @@ export default function connect( mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory, + pure = true, + __ENABLE_SECRET_EXPERIMENTAL_FEATURES_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = false, ...options } = {} ) { - // !!!!!!!!!! TEMPORARY DISABLING OF NEW CUSTOMIZATION FEATURES !!!!!!!!!! - mapStateToPropsFactories = defaultMapStateToPropsFactories - mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories - mergePropsFactories = defaultMergePropsFactories - selectorFactory = defaultSelectorFactory - options = { pure: options.pure, withRef: options.withRef } - // !!!!!!!!!! REMOVE THESE STATEMENTS ONCE APPROVED !!!!!!!!!! + if (!__ENABLE_SECRET_EXPERIMENTAL_FEATURES_DO_NOT_USE_OR_YOU_WILL_BE_FIRED) { + mapStateToPropsFactories = defaultMapStateToPropsFactories + mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories + mergePropsFactories = defaultMergePropsFactories + selectorFactory = defaultSelectorFactory + options = { withRef: options.withRef } + } - return connectAdvanced( - selectorFactory, - { - // used in error messages - methodName: 'connect', + const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories) + const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories) + const initMergeProps = match(mergeProps, mergePropsFactories) - // used to compute Connect's displayName from the wrapped component's displayName. - getDisplayName: name => `Connect(${name})`, + return connectAdvanced(selectorFactory, { + // used in error messages + methodName: 'connect', - // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes - shouldHandleStateChanges: Boolean(mapStateToProps), + // used to compute Connect's displayName from the wrapped component's displayName. + getDisplayName: name => `Connect(${name})`, - // any addional options args can override defaults of connect or connectAdvanced - ...options, + // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes + shouldHandleStateChanges: Boolean(mapStateToProps), - // passed through to selectorFactory - mapStateToProps, - mapStateToPropsFactories, - mapDispatchToProps, - mapDispatchToPropsFactories, - mergeProps, - mergePropsFactories - } - ) + // passed through to selectorFactory + initMapStateToProps, + initMapDispatchToProps, + initMergeProps, + pure, + + // any addional options args can override defaults of connect or connectAdvanced + ...options + }) +} + +function match(arg, factories) { + for (let i = factories.length - 1; i >= 0; i--) { + const result = factories[i](arg) + if (result) return result + } + return undefined } diff --git a/src/connect/createMapToPropsProxy.js b/src/connect/createMapToPropsProxy.js deleted file mode 100644 index 70328a78b..000000000 --- a/src/connect/createMapToPropsProxy.js +++ /dev/null @@ -1,56 +0,0 @@ -import verifyPlainObject from '../utils/verifyPlainObject' - -// Used by whenMapStateToPropsIsFunction and whenMapDispatchToPropsIsFunction, -// this function wraps mapToProps in a proxy function which does several things: -// -// * Detects whether the mapToProps function being called depends on props, which -// is used by selectorFactory to decide if it should reinvoke on props changes. -// -// * On first call, handles mapToProps if returns another function, and treats that -// new function as the true mapToProps for subsequent calls. -// -// * On first call, verifies the first result is a plain object, in order to warn -// the developer that their mapToProps function is not returning a valid result. -// -export default function createMapToPropsProxy(mapToProps, displayName, methodName) { - - // meta.dependsOnProps is used by this function to determine whether to pass - // props as args to the mapToProps function being wrapped. It is also used by - // selectorFactory to determine whether this function needs to be invoked when - // props have changed. - // - // If the original mapToProps function does not have its own meta property, one - // will be created with dependsOnProps based on mapToProps's length (number of - // arguments). A length of one signals that mapToProps does not depend on props - // being passed from the parent component. - // - const detectedMeta = { dependsOnProps: mapToProps.length !== 1 } - const proxyMeta = mapToProps.meta || detectedMeta - - let proxiedMapToProps - function mapToPropsProxy(stateOrDispatch, ownProps) { - return proxyMeta.dependsOnProps - ? proxiedMapToProps(stateOrDispatch, ownProps) - : proxiedMapToProps(stateOrDispatch) - } - mapToPropsProxy.meta = proxyMeta - - function detectFactoryAndVerify(stateOrDispatch, ownProps) { - proxiedMapToProps = mapToProps - let result = mapToPropsProxy(stateOrDispatch, ownProps) - - if (typeof result === 'function') { - proxiedMapToProps = result - detectedMeta.dependsOnProps = result.length !== 1 - result = mapToPropsProxy(stateOrDispatch, ownProps) - } - - if (process.env.NODE_ENV !== 'production') - verifyPlainObject(result, displayName, methodName) - - return result - } - - proxiedMapToProps = detectFactoryAndVerify - return mapToPropsProxy -} diff --git a/src/connect/mapDispatchToProps.js b/src/connect/mapDispatchToProps.js index 703f46e12..405004538 100644 --- a/src/connect/mapDispatchToProps.js +++ b/src/connect/mapDispatchToProps.js @@ -1,32 +1,26 @@ import { bindActionCreators } from 'redux' -import createMapToPropsProxy from './createMapToPropsProxy' +import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps' -export function whenMapDispatchToPropsIsMissing({ mapDispatchToProps, dispatch }) { - if (mapDispatchToProps) return undefined - - const props = { dispatch } - function justDispatch() { return props } - justDispatch.meta = { dependsOnProps: false } - return justDispatch +export function whenMapDispatchToPropsIsFunction(mapDispatchToProps) { + return (typeof mapDispatchToProps === 'function') + ? wrapMapToPropsFunc(mapDispatchToProps, 'mapDispatchToProps') + : undefined } -export function whenMapDispatchToPropsIsObject({ mapDispatchToProps, dispatch }) { - if (!mapDispatchToProps || typeof mapDispatchToProps !== 'object') return undefined - - const props = bindActionCreators(mapDispatchToProps, dispatch) - function boundActionCreators() { return props } - boundActionCreators.meta = { dependsOnProps: false } - return boundActionCreators +export function whenMapDispatchToPropsIsMissing(mapDispatchToProps) { + return (!mapDispatchToProps) + ? wrapMapToPropsConstant(dispatch => ({ dispatch })) + : undefined } -export function whenMapDispatchToPropsIsFunction({ mapDispatchToProps, displayName }) { - return typeof mapDispatchToProps === 'function' - ? createMapToPropsProxy(mapDispatchToProps, displayName, 'mapDispatchToProps') +export function whenMapDispatchToPropsIsObject(mapDispatchToProps) { + return (mapDispatchToProps && typeof mapDispatchToProps === 'object') + ? wrapMapToPropsConstant(dispatch => bindActionCreators(mapDispatchToProps, dispatch)) : undefined } export default [ - whenMapDispatchToPropsIsMissing, whenMapDispatchToPropsIsFunction, + whenMapDispatchToPropsIsMissing, whenMapDispatchToPropsIsObject ] diff --git a/src/connect/mapStateToProps.js b/src/connect/mapStateToProps.js index 062256267..039291b0a 100644 --- a/src/connect/mapStateToProps.js +++ b/src/connect/mapStateToProps.js @@ -1,21 +1,18 @@ -import createMapToPropsProxy from './createMapToPropsProxy' +import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps' -export function whenMapStateToPropsIsMissing({ mapStateToProps }) { - if (mapStateToProps) return undefined - - const empty = {} - function emptyState() { return empty } - emptyState.meta = { dependsOnProps: false } - return emptyState +export function whenMapStateToPropsIsFunction(mapStateToProps) { + return (typeof mapStateToProps === 'function') + ? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps') + : undefined } -export function whenMapStateToPropsIsFunction({ mapStateToProps, displayName }) { - return typeof mapStateToProps === 'function' - ? createMapToPropsProxy(mapStateToProps, displayName, 'mapStateToProps') +export function whenMapStateToPropsIsMissing(mapStateToProps) { + return (!mapStateToProps) + ? wrapMapToPropsConstant(() => ({})) : undefined } export default [ - whenMapStateToPropsIsMissing, - whenMapStateToPropsIsFunction + whenMapStateToPropsIsFunction, + whenMapStateToPropsIsMissing ] diff --git a/src/connect/mergeProps.js b/src/connect/mergeProps.js index b29b23bee..f2a42c42b 100644 --- a/src/connect/mergeProps.js +++ b/src/connect/mergeProps.js @@ -1,43 +1,50 @@ +import shallowEqual from '../utils/shallowEqual' import verifyPlainObject from '../utils/verifyPlainObject' export function defaultMergeProps(stateProps, dispatchProps, ownProps) { - return { - ...ownProps, - ...stateProps, - ...dispatchProps - } + return { ...ownProps, ...stateProps, ...dispatchProps } } -defaultMergeProps.meta = { skipShallowEqual: true } -export function whenMergePropsIsMissing({ mergeProps }) { - return mergeProps - ? undefined - : defaultMergeProps -} +export function wrapMergePropsFunc(mergeProps) { + return function initMergePropsProxy( + dispatch, { displayName, pure, areMergedPropsEqual = shallowEqual } + ) { + let hasRunOnce = false + let mergedProps + + return function mergePropsProxy(stateProps, dispatchProps, ownProps) { + const nextMergedProps = mergeProps(stateProps, dispatchProps, ownProps) -export function whenMergePropsIsFunction({ mergeProps, displayName }) { - if (typeof mergeProps !== 'function') - return undefined + if (hasRunOnce) { + if (!pure || !areMergedPropsEqual(nextMergedProps, mergedProps)) + mergedProps = nextMergedProps - let hasVerifiedOnce = false + } else { + hasRunOnce = true + mergedProps = nextMergedProps - function mergePropsProxy(stateProps, dispatchProps, ownProps) { - const mergedProps = mergeProps(stateProps, dispatchProps, ownProps) - - if (process.env.NODE_ENV !== 'production') { - if (!hasVerifiedOnce) { - hasVerifiedOnce = true - verifyPlainObject(mergedProps, displayName, 'mergeProps') + if (process.env.NODE_ENV !== 'production') + verifyPlainObject(mergedProps, displayName, 'mergeProps') } - } - return mergedProps + return mergedProps + } } - mergePropsProxy.meta = mergeProps.meta || { skipShallowEqual: false } - return mergePropsProxy +} + +export function whenMergePropsIsFunction(mergeProps) { + return (typeof mergeProps === 'function') + ? wrapMergePropsFunc(mergeProps) + : undefined +} + +export function whenMergePropsIsOmitted(mergeProps) { + return (!mergeProps) + ? () => defaultMergeProps + : undefined } export default [ - whenMergePropsIsMissing, - whenMergePropsIsFunction + whenMergePropsIsFunction, + whenMergePropsIsOmitted ] diff --git a/src/connect/selectorFactory.js b/src/connect/selectorFactory.js index 56e17d100..9e12382ab 100644 --- a/src/connect/selectorFactory.js +++ b/src/connect/selectorFactory.js @@ -1,10 +1,13 @@ +import verifySubselectors from './verifySubselectors' import shallowEqual from '../utils/shallowEqual' - -export function makeImpurePropsSelector( - dispatch, { mapStateToProps, mapDispatchToProps, mergeProps } + +export function impureFinalPropsSelectorFactory( + mapStateToProps, + mapDispatchToProps, + mergeProps, + dispatch ) { - // TODO: cache mapDispatchToProps result if not dependent on ownProps - return function impureSelector(state, ownProps) { + return function impureFinalPropsSelector(state, ownProps) { return mergeProps( mapStateToProps(state, ownProps), mapDispatchToProps(dispatch, ownProps), @@ -13,8 +16,18 @@ export function makeImpurePropsSelector( } } -export function makePurePropsSelector( - dispatch, { mapStateToProps, mapDispatchToProps, mergeProps } +function strictEqual(a, b) { return a === b } + +export function pureFinalPropsSelectorFactory( + mapStateToProps, + mapDispatchToProps, + mergeProps, + dispatch, + { + areStatesEqual = strictEqual, + areOwnPropsEqual = shallowEqual, + areStatePropsEqual = shallowEqual + } ) { let hasRunAtLeastOnce = false let state @@ -33,22 +46,14 @@ export function makePurePropsSelector( return mergedProps } - function mergeFinalProps() { - const nextMergedProps = mergeProps(stateProps, dispatchProps, ownProps) - - if (mergeProps.meta.skipShallowEqual || !shallowEqual(mergedProps, nextMergedProps)) - mergedProps = nextMergedProps - - return mergedProps - } - function handleNewPropsAndNewState() { stateProps = mapStateToProps(state, ownProps) if (mapDispatchToProps.meta.dependsOnProps) dispatchProps = mapDispatchToProps(dispatch, ownProps) - return mergeFinalProps() + mergedProps = mergeProps(stateProps, dispatchProps, ownProps) + return mergedProps } function handleNewProps() { @@ -58,22 +63,24 @@ export function makePurePropsSelector( if (mapDispatchToProps.meta.dependsOnProps) dispatchProps = mapDispatchToProps(dispatch, ownProps) - return mergeFinalProps() + mergedProps = mergeProps(stateProps, dispatchProps, ownProps) + return mergedProps } function handleNewState() { const nextStateProps = mapStateToProps(state, ownProps) - const statePropsChanged = !shallowEqual(nextStateProps, stateProps) + const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps) stateProps = nextStateProps - return statePropsChanged - ? mergeFinalProps() - : mergedProps + if (statePropsChanged) + mergedProps = mergeProps(stateProps, dispatchProps, ownProps) + + return mergedProps } function handleSubsequentCalls(nextState, nextOwnProps) { - const propsChanged = !shallowEqual(nextOwnProps, ownProps) - const stateChanged = nextState !== state + const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps) + const stateChanged = !areStatesEqual(nextState, state) state = nextState ownProps = nextOwnProps @@ -83,24 +90,13 @@ export function makePurePropsSelector( return mergedProps } - return function pureSelector(nextState, nextOwnProps) { + return function pureFinalPropsSelector(nextState, nextOwnProps) { return hasRunAtLeastOnce ? handleSubsequentCalls(nextState, nextOwnProps) : handleFirstCall(nextState, nextOwnProps) } } -export function findMatchingSelector(name, options) { - const factories = options[name + 'Factories'] - - for (let i = factories.length - 1; i >= 0; i--) { - const selector = factories[i](options) - if (selector) return selector - } - - throw new Error(`Unexpected value for ${name} in ${options.displayName}.`) -} - // TODO: Add more comments // If pure is true, the selector returned by selectorFactory will memoize its results, @@ -108,15 +104,29 @@ export function findMatchingSelector(name, options) { // props have not changed. If false, the selector will always return a new // object and shouldComponentUpdate will always return true. -export default function selectorFactory(dispatch, { pure = true, ...options }) { - const finalOptions = { - ...options, - mapStateToProps: findMatchingSelector('mapStateToProps', options), - mapDispatchToProps: findMatchingSelector('mapDispatchToProps', { ...options, dispatch }), - mergeProps: findMatchingSelector('mergeProps', options) +export default function finalPropsSelectorFactory(dispatch, { + initMapStateToProps, + initMapDispatchToProps, + initMergeProps, + ...options +}) { + const mapStateToProps = initMapStateToProps(dispatch, options) + const mapDispatchToProps = initMapDispatchToProps(dispatch, options) + const mergeProps = initMergeProps(dispatch, options) + + if (process.env.NODE_ENV !== 'production') { + verifySubselectors(mapStateToProps, mapDispatchToProps, mergeProps, options.displayName) } - - return pure - ? makePurePropsSelector(dispatch, finalOptions) - : makeImpurePropsSelector(dispatch, finalOptions) + + const selectorFactory = options.pure + ? pureFinalPropsSelectorFactory + : impureFinalPropsSelectorFactory + + return selectorFactory( + mapStateToProps, + mapDispatchToProps, + mergeProps, + dispatch, + options + ) } diff --git a/src/connect/verifySubselectors.js b/src/connect/verifySubselectors.js new file mode 100644 index 000000000..8beb9125c --- /dev/null +++ b/src/connect/verifySubselectors.js @@ -0,0 +1,20 @@ +import warning from '../utils/warning' + +function verify(selector, methodName, displayName) { + if (!selector) { + throw new Error(`Unexpected value for ${methodName} in ${displayName}.`) + + } else if (methodName === 'mapStateToProps' || methodName === 'mapDispatchToProps') { + if (!selector.meta || !selector.meta.hasOwnProperty('dependsOnProps')) { + warning( + `The selector for ${methodName} of ${displayName} did not specify a value for dependsOnProps.` + ) + } + } +} + +export default function verifySubselectors(mapStateToProps, mapDispatchToProps, mergeProps, displayName) { + verify(mapStateToProps, 'mapStateToProps', displayName) + verify(mapDispatchToProps, 'mapDispatchToProps', displayName) + verify(mergeProps, 'mergeProps', displayName) +} diff --git a/src/connect/wrapMapToProps.js b/src/connect/wrapMapToProps.js new file mode 100644 index 000000000..1ca5003f7 --- /dev/null +++ b/src/connect/wrapMapToProps.js @@ -0,0 +1,70 @@ +import verifyPlainObject from '../utils/verifyPlainObject' + +export function wrapMapToPropsConstant(getConstant) { + return function initConstantSelector(dispatch, options) { + const constant = getConstant(dispatch, options) + + function constantSelector() { return constant } + constantSelector.meta = { dependsOnProps: false } + return constantSelector + } +} + +// meta.dependsOnProps is used by createMapToPropsProxy to determine whether to pass props as args +// to the mapToProps function being wrapped. It is also used by makePurePropsSelector to determine +// whether mapToProps needs to be invoked when props have changed. +// +// A length of one signals that mapToProps does not depend on props from the parent component. +// A length of zero is assumed to mean mapToProps is getting args via arguments or ...args and +// therefore not reporting its length accurately.. +export function getMeta(mapToProps) { + if (mapToProps.meta) return mapToProps.meta + return { + dependsOnProps: mapToProps.hasOwnProperty('dependsOnProps') + ? mapToProps.dependsOnProps + : mapToProps.length !== 1 + } +} + +// Used by whenMapStateToPropsIsFunction and whenMapDispatchToPropsIsFunction, +// this function wraps mapToProps in a proxy function which does several things: +// +// * Detects whether the mapToProps function being called depends on props, which +// is used by selectorFactory to decide if it should reinvoke on props changes. +// +// * On first call, handles mapToProps if returns another function, and treats that +// new function as the true mapToProps for subsequent calls. +// +// * On first call, verifies the first result is a plain object, in order to warn +// the developer that their mapToProps function is not returning a valid result. +// +export function wrapMapToPropsFunc(mapToProps, methodName) { + return function initProxySelector(dispatch, { displayName }) { + let proxiedMapToProps + function mapToPropsProxy(stateOrDispatch, ownProps) { + return mapToPropsProxy.meta.dependsOnProps + ? proxiedMapToProps(stateOrDispatch, ownProps) + : proxiedMapToProps(stateOrDispatch) + } + mapToPropsProxy.meta = getMeta(mapToProps) + + function detectFactoryAndVerify(stateOrDispatch, ownProps) { + proxiedMapToProps = mapToProps + let result = mapToPropsProxy(stateOrDispatch, ownProps) + + if (typeof result === 'function') { + proxiedMapToProps = result + mapToPropsProxy.meta = getMeta(result) + result = mapToPropsProxy(stateOrDispatch, ownProps) + } + + if (process.env.NODE_ENV !== 'production') + verifyPlainObject(result, displayName, methodName) + + return result + } + + proxiedMapToProps = detectFactoryAndVerify + return mapToPropsProxy + } +} From 3cc504f05d1e25369d0d81f615f1d1a5a1a55cfc Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 16 Jul 2016 12:31:46 -0400 Subject: [PATCH 70/76] Optimize connectAdvanced to not create subscription if it shouldn't respond to state changes. --- src/components/connectAdvanced.js | 79 ++++++++++++++++++------------- src/utils/Subscription.js | 12 ++--- 2 files changed, 50 insertions(+), 41 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 2f6041fc2..c34702d05 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -60,7 +60,7 @@ export default function connectAdvanced( [subscriptionKey]: PropTypes.instanceOf(Subscription) } const childContextTypes = { - [subscriptionKey]: PropTypes.instanceOf(Subscription).isRequired + [subscriptionKey]: PropTypes.instanceOf(Subscription) } return function wrapWithConnect(WrappedComponent) { @@ -119,7 +119,7 @@ export default function connectAdvanced( // re-render. this.subscription.trySubscribe() this.selector.run(this.props) - if (this.shouldComponentUpdate()) this.forceUpdate() + if (this.selector.shouldComponentUpdate) this.forceUpdate() } componentWillReceiveProps(nextProps) { @@ -127,14 +127,14 @@ export default function connectAdvanced( } shouldComponentUpdate() { - return this.selector.lastProps !== this.selector.nextProps + return this.selector.shouldComponentUpdate } componentWillUnmount() { - this.subscription.tryUnsubscribe() + if (this.subscription) this.subscription.tryUnsubscribe() // these are just to guard against extra memory leakage if a parent element doesn't // dereference this instance properly, such as an async callback that never finishes - this.subscription = { isSubscribed: () => false } + this.subscription = null this.store = null this.parentSub = null this.selector.run = () => {} @@ -153,40 +153,49 @@ export default function connectAdvanced( } initSelector() { - const store = this.store - const selector = selectorFactory(store.dispatch, selectorFactoryOptions) + const { dispatch, getState } = this.store + const sourceSelector = selectorFactory(dispatch, selectorFactoryOptions) // wrap the selector in an object that tracks its results between runs - const wrapper = { - lastProps: null, - nextProps: selector(store.getState(), this.props), - run(props) { - wrapper.nextProps = selector(store.getState(), props) + const selector = this.selector = { + shouldComponentUpdate: true, + props: sourceSelector(getState(), this.props), + run: function runComponentSelector(props) { + try { + const nextProps = sourceSelector(getState(), props) + if (selector.error || nextProps !== selector.props) { + selector.shouldComponentUpdate = true + selector.props = nextProps + selector.error = null + } + } catch (error) { + selector.shouldComponentUpdate = true + selector.error = error + } } } - this.selector = wrapper } initSubscription() { - const dummyState = {} - function onStoreStateChange(notifyNestedSubs) { - this.selector.run(this.props) - if (this.shouldComponentUpdate()) { - this.setState(dummyState, notifyNestedSubs) - } else { - notifyNestedSubs() - } + if (shouldHandleStateChanges) { + const subscription = this.subscription = new Subscription(this.store, this.parentSub) + const notifyNestedSubs = subscription.notifyNestedSubs.bind(subscription) + const dummyState = {} + + subscription.onStateChange = function onStateChange() { + this.selector.run(this.props) + + if (!this.selector.shouldComponentUpdate) { + subscription.notifyNestedSubs() + } else { + this.setState(dummyState, notifyNestedSubs) + } + }.bind(this) } - - const onChange = shouldHandleStateChanges - ? onStoreStateChange.bind(this) - : (notifyNestedSubs => notifyNestedSubs()) - - this.subscription = new Subscription(this.store, this.parentSub, onChange) } isSubscribed() { - return this.subscription.isSubscribed() + return Boolean(this.subscription) && this.subscription.isSubscribed() } addExtraProps(props) { @@ -202,10 +211,14 @@ export default function connectAdvanced( } render() { - return createElement( - WrappedComponent, - this.addExtraProps(this.selector.lastProps = this.selector.nextProps) - ) + const selector = this.selector + selector.shouldComponentUpdate = false + + if (selector.error) { + throw selector.error + } else { + return createElement(WrappedComponent, this.addExtraProps(selector.props)) + } } } @@ -222,7 +235,7 @@ export default function connectAdvanced( this.version = version this.initSelector() - this.subscription.tryUnsubscribe() + if (this.subscription) this.subscription.tryUnsubscribe() this.initSubscription() if (shouldHandleStateChanges) this.subscription.trySubscribe() } diff --git a/src/utils/Subscription.js b/src/utils/Subscription.js index a06c103ee..b26615ed2 100644 --- a/src/utils/Subscription.js +++ b/src/utils/Subscription.js @@ -2,12 +2,11 @@ // well as nesting subscriptions of descendant components, so that we can ensure the // ancestor components re-render before descendants export default class Subscription { - constructor(store, parentSub, onStateChange) { + constructor(store, parentSub) { this.subscribe = parentSub ? parentSub.addNestedSub.bind(parentSub) : store.subscribe - this.onStateChange = onStateChange this.unsubscribe = null this.nextListeners = this.currentListeners = [] } @@ -48,12 +47,9 @@ export default class Subscription { } trySubscribe() { - if (this.unsubscribe) return - - const notifyNestedSubs = this.notifyNestedSubs.bind(this) - const onStateChange = this.onStateChange - function listener() { onStateChange(notifyNestedSubs) } - this.unsubscribe = this.subscribe(listener) + if (!this.unsubscribe) { + this.unsubscribe = this.subscribe(this.onStateChange) + } } tryUnsubscribe() { From 3c5c80e757c4f6e4127fb10d5b6ccd8c88aead6c Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 16 Jul 2016 13:45:31 -0400 Subject: [PATCH 71/76] Refactor - put dependsOnOwnProps right on the selector func instead of extra "meta" property --- src/connect/selectorFactory.js | 6 ++-- src/connect/verifySubselectors.js | 4 +-- src/connect/wrapMapToProps.js | 48 ++++++++++++++----------------- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/connect/selectorFactory.js b/src/connect/selectorFactory.js index 9e12382ab..47593aa9b 100644 --- a/src/connect/selectorFactory.js +++ b/src/connect/selectorFactory.js @@ -49,7 +49,7 @@ export function pureFinalPropsSelectorFactory( function handleNewPropsAndNewState() { stateProps = mapStateToProps(state, ownProps) - if (mapDispatchToProps.meta.dependsOnProps) + if (mapDispatchToProps.dependsOnOwnProps) dispatchProps = mapDispatchToProps(dispatch, ownProps) mergedProps = mergeProps(stateProps, dispatchProps, ownProps) @@ -57,10 +57,10 @@ export function pureFinalPropsSelectorFactory( } function handleNewProps() { - if (mapStateToProps.meta.dependsOnProps) + if (mapStateToProps.dependsOnOwnProps) stateProps = mapStateToProps(state, ownProps) - if (mapDispatchToProps.meta.dependsOnProps) + if (mapDispatchToProps.dependsOnOwnProps) dispatchProps = mapDispatchToProps(dispatch, ownProps) mergedProps = mergeProps(stateProps, dispatchProps, ownProps) diff --git a/src/connect/verifySubselectors.js b/src/connect/verifySubselectors.js index 8beb9125c..7c6b248b8 100644 --- a/src/connect/verifySubselectors.js +++ b/src/connect/verifySubselectors.js @@ -5,9 +5,9 @@ function verify(selector, methodName, displayName) { throw new Error(`Unexpected value for ${methodName} in ${displayName}.`) } else if (methodName === 'mapStateToProps' || methodName === 'mapDispatchToProps') { - if (!selector.meta || !selector.meta.hasOwnProperty('dependsOnProps')) { + if (!selector.hasOwnProperty('dependsOnOwnProps')) { warning( - `The selector for ${methodName} of ${displayName} did not specify a value for dependsOnProps.` + `The selector for ${methodName} of ${displayName} did not specify a value for dependsOnOwnProps.` ) } } diff --git a/src/connect/wrapMapToProps.js b/src/connect/wrapMapToProps.js index 1ca5003f7..93eaa80b3 100644 --- a/src/connect/wrapMapToProps.js +++ b/src/connect/wrapMapToProps.js @@ -5,25 +5,22 @@ export function wrapMapToPropsConstant(getConstant) { const constant = getConstant(dispatch, options) function constantSelector() { return constant } - constantSelector.meta = { dependsOnProps: false } + constantSelector.dependsOnOwnProps = false return constantSelector } } -// meta.dependsOnProps is used by createMapToPropsProxy to determine whether to pass props as args +// dependsOnOwnProps is used by createMapToPropsProxy to determine whether to pass props as args // to the mapToProps function being wrapped. It is also used by makePurePropsSelector to determine // whether mapToProps needs to be invoked when props have changed. // // A length of one signals that mapToProps does not depend on props from the parent component. // A length of zero is assumed to mean mapToProps is getting args via arguments or ...args and // therefore not reporting its length accurately.. -export function getMeta(mapToProps) { - if (mapToProps.meta) return mapToProps.meta - return { - dependsOnProps: mapToProps.hasOwnProperty('dependsOnProps') - ? mapToProps.dependsOnProps - : mapToProps.length !== 1 - } +export function getDependsOnOwnProps(mapToProps) { + return (mapToProps.dependsOnOwnProps !== null && mapToProps.dependsOnOwnProps !== undefined) + ? Boolean(mapToProps.dependsOnOwnProps) + : mapToProps.length !== 1 } // Used by whenMapStateToPropsIsFunction and whenMapDispatchToPropsIsFunction, @@ -40,31 +37,30 @@ export function getMeta(mapToProps) { // export function wrapMapToPropsFunc(mapToProps, methodName) { return function initProxySelector(dispatch, { displayName }) { - let proxiedMapToProps - function mapToPropsProxy(stateOrDispatch, ownProps) { - return mapToPropsProxy.meta.dependsOnProps - ? proxiedMapToProps(stateOrDispatch, ownProps) - : proxiedMapToProps(stateOrDispatch) + const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) { + return proxy.dependsOnOwnProps + ? proxy.mapToProps(stateOrDispatch, ownProps) + : proxy.mapToProps(stateOrDispatch) } - mapToPropsProxy.meta = getMeta(mapToProps) - function detectFactoryAndVerify(stateOrDispatch, ownProps) { - proxiedMapToProps = mapToProps - let result = mapToPropsProxy(stateOrDispatch, ownProps) + proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps) + + proxy.mapToProps = function detectFactoryAndVerify(stateOrDispatch, ownProps) { + proxy.mapToProps = mapToProps + let props = proxy(stateOrDispatch, ownProps) - if (typeof result === 'function') { - proxiedMapToProps = result - mapToPropsProxy.meta = getMeta(result) - result = mapToPropsProxy(stateOrDispatch, ownProps) + if (typeof props === 'function') { + proxy.mapToProps = props + proxy.dependsOnOwnProps = getDependsOnOwnProps(props) + props = proxy(stateOrDispatch, ownProps) } if (process.env.NODE_ENV !== 'production') - verifyPlainObject(result, displayName, methodName) + verifyPlainObject(props, displayName, methodName) - return result + return props } - proxiedMapToProps = detectFactoryAndVerify - return mapToPropsProxy + return proxy } } From de81801ae90ffb9f7dff95b3a278e7e5bb400361 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 16 Jul 2016 14:05:46 -0400 Subject: [PATCH 72/76] Add test from #293 and uncomment now-passing expect in 'should pass state consistently to mapState' --- test/components/Provider.spec.js | 65 +++++++++++++++++++++++++++++++- test/components/connect.spec.js | 10 +---- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/test/components/Provider.spec.js b/test/components/Provider.spec.js index affbf81e7..4d4a17df1 100644 --- a/test/components/Provider.spec.js +++ b/test/components/Provider.spec.js @@ -2,7 +2,7 @@ import expect from 'expect' import React, { PropTypes, Component } from 'react' import TestUtils from 'react-addons-test-utils' import { createStore } from 'redux' -import { Provider } from '../../src/index' +import { Provider, connect } from '../../src/index' describe('React', () => { describe('Provider', () => { @@ -108,4 +108,67 @@ describe('React', () => { expect(spy.calls.length).toBe(0) }) }) + + it('should pass state consistently to mapState', () => { + function stringBuilder(prev = '', action) { + return action.type === 'APPEND' + ? prev + action.body + : prev + } + + const store = createStore(stringBuilder) + + store.dispatch({ type: 'APPEND', body: 'a' }) + let childMapStateInvokes = 0 + + @connect(state => ({ state }), null, null, { withRef: true }) + class Container extends Component { + emitChange() { + store.dispatch({ type: 'APPEND', body: 'b' }) + } + + render() { + return ( +
+ + +
+ ) + } + } + + @connect((state, parentProps) => { + childMapStateInvokes++ + // The state from parent props should always be consistent with the current state + expect(state).toEqual(parentProps.parentState) + return {} + }) + class ChildContainer extends Component { + render() { + return
+ } + } + + const tree = TestUtils.renderIntoDocument( + + + + ) + + expect(childMapStateInvokes).toBe(1) + + // The store state stays consistent when setState calls are batched + store.dispatch({ type: 'APPEND', body: 'c' }) + expect(childMapStateInvokes).toBe(2) + + // setState calls DOM handlers are batched + const container = TestUtils.findRenderedComponentWithType(tree, Container) + const node = container.getWrappedInstance().refs.button + TestUtils.Simulate.click(node) + expect(childMapStateInvokes).toBe(3) + + // Provider uses unstable_batchedUpdates() under the hood + store.dispatch({ type: 'APPEND', body: 'd' }) + expect(childMapStateInvokes).toBe(4) + }) }) diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 5e494a951..38e4f95ae 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1538,14 +1538,8 @@ describe('React', () => { TestUtils.Simulate.click(node) expect(childMapStateInvokes).toBe(3) - // In future all setState calls will be batched[1]. Uncomment when it - // happens. For now redux-batched-updates middleware can be used as - // workaround this. - // - // [1]: https://twitter.com/sebmarkbage/status/642366976824864768 - // - // store.dispatch({ type: 'APPEND', body: 'd' }) - // expect(childMapStateInvokes).toBe(4) + store.dispatch({ type: 'APPEND', body: 'd' }) + expect(childMapStateInvokes).toBe(4) }) it('should not render the wrapped component when mapState does not produce change', () => { From e2df05103a47b54220a5dbfcba7ec4e93dc1754b Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 16 Jul 2016 14:23:04 -0400 Subject: [PATCH 73/76] Added passing test from #395 --- test/components/connect.spec.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 38e4f95ae..0254657bb 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1862,5 +1862,18 @@ describe('React', () => { ReactDOM.unmountComponentAtNode(div) }) + + it('should allow custom displayName', () => { + // TODO remove __ENABLE_SECRET_EXPERIMENTAL_FEATURES_DO_NOT_USE_OR_YOU_WILL_BE_FIRED once approved + @connect(null, null, null, { getDisplayName: name => `Custom(${name})`, __ENABLE_SECRET_EXPERIMENTAL_FEATURES_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: true }) + class MyComponent extends React.Component { + render() { + return
+ } + } + + expect(MyComponent.displayName).toEqual('Custom(MyComponent)') + }) + }) }) From e907f858ae78e6ee686f85d033f9d108dcda3c5c Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 16 Jul 2016 14:24:24 -0400 Subject: [PATCH 74/76] Add passing test from #429 --- test/components/connect.spec.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 0254657bb..3a63fa023 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1875,5 +1875,28 @@ describe('React', () => { expect(MyComponent.displayName).toEqual('Custom(MyComponent)') }) + it('should update impure components whenever the state of the store changes', () => { + const store = createStore(() => ({})) + let renderCount = 0 + + @connect(() => ({}), null, null, { pure: false }) + class ImpureComponent extends React.Component { + render() { + ++renderCount + return
+ } + } + + TestUtils.renderIntoDocument( + + + + ) + + const rendersBeforeStateChange = renderCount + store.dispatch({ type: 'ACTION' }) + expect(renderCount).toBe(rendersBeforeStateChange + 1) + }) }) + }) From a6d82f0fcb2e661f9d6f3a799dd055d1dfca982f Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 16 Jul 2016 14:28:29 -0400 Subject: [PATCH 75/76] Add code + test for #436 --- src/components/connectAdvanced.js | 6 ++++++ test/components/connect.spec.js | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index c34702d05..6736e7a9d 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -64,6 +64,12 @@ export default function connectAdvanced( } return function wrapWithConnect(WrappedComponent) { + invariant( + typeof WrappedComponent == 'function', + `You must pass a component to the function returned by ` + + `connect. Instead received ${WrappedComponent}` + ) + const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component' diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 3a63fa023..3742afd79 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1017,6 +1017,12 @@ describe('React', () => { expect(stub.props.passVal).toBe('otherval') }) + it('should throw an error if a component is not passed to the function returned by connect', () => { + expect(connect()).toThrow( + /You must pass a component to the function/ + ) + }) + it('should throw an error if mapState, mapDispatch, or mergeProps returns anything but a plain object', () => { const store = createStore(() => ({})) From a4d321119fce95baa63127e0cf7701b900f32644 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 16 Jul 2016 14:53:28 -0400 Subject: [PATCH 76/76] Extract buildConnectOptions out of connect()... will make testing easier --- src/connect/connect.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/connect/connect.js b/src/connect/connect.js index acdb933b6..9c4611983 100644 --- a/src/connect/connect.js +++ b/src/connect/connect.js @@ -20,7 +20,16 @@ import defaultSelectorFactory from './selectorFactory' The resulting final props selector is called by the Connect component instance whenever it receives new props or store state. */ -export default function connect( + +function match(arg, factories) { + for (let i = factories.length - 1; i >= 0; i--) { + const result = factories[i](arg) + if (result) return result + } + return undefined +} + +export function buildConnectOptions( mapStateToProps, mapDispatchToProps, mergeProps, @@ -46,7 +55,7 @@ export default function connect( const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories) const initMergeProps = match(mergeProps, mergePropsFactories) - return connectAdvanced(selectorFactory, { + return { // used in error messages methodName: 'connect', @@ -57,6 +66,7 @@ export default function connect( shouldHandleStateChanges: Boolean(mapStateToProps), // passed through to selectorFactory + selectorFactory, initMapStateToProps, initMapDispatchToProps, initMergeProps, @@ -64,13 +74,10 @@ export default function connect( // any addional options args can override defaults of connect or connectAdvanced ...options - }) + } } -function match(arg, factories) { - for (let i = factories.length - 1; i >= 0; i--) { - const result = factories[i](arg) - if (result) return result - } - return undefined +export default function connect(...args) { + const options = buildConnectOptions(...args) + return connectAdvanced(options.selectorFactory, options) }