306 lines
14 KiB
JavaScript
306 lines
14 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
exports.__esModule = true;
|
||
|
|
||
|
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
||
|
|
||
|
exports.default = connectAdvanced;
|
||
|
|
||
|
var _hoistNonReactStatics = require('hoist-non-react-statics');
|
||
|
|
||
|
var _hoistNonReactStatics2 = _interopRequireDefault(_hoistNonReactStatics);
|
||
|
|
||
|
var _invariant = require('invariant');
|
||
|
|
||
|
var _invariant2 = _interopRequireDefault(_invariant);
|
||
|
|
||
|
var _react = require('react');
|
||
|
|
||
|
var _Subscription = require('../utils/Subscription');
|
||
|
|
||
|
var _Subscription2 = _interopRequireDefault(_Subscription);
|
||
|
|
||
|
var _PropTypes = require('../utils/PropTypes');
|
||
|
|
||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||
|
|
||
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
||
|
|
||
|
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
|
||
|
|
||
|
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
|
||
|
|
||
|
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
|
||
|
|
||
|
var hotReloadingVersion = 0;
|
||
|
var dummyState = {};
|
||
|
function noop() {}
|
||
|
function makeSelectorStateful(sourceSelector, store) {
|
||
|
// wrap the selector in an object that tracks its results between runs.
|
||
|
var selector = {
|
||
|
run: function runComponentSelector(props) {
|
||
|
try {
|
||
|
var nextProps = sourceSelector(store.getState(), props);
|
||
|
if (nextProps !== selector.props || selector.error) {
|
||
|
selector.shouldComponentUpdate = true;
|
||
|
selector.props = nextProps;
|
||
|
selector.error = null;
|
||
|
}
|
||
|
} catch (error) {
|
||
|
selector.shouldComponentUpdate = true;
|
||
|
selector.error = error;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return selector;
|
||
|
}
|
||
|
|
||
|
function connectAdvanced(
|
||
|
/*
|
||
|
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 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 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.
|
||
|
*/
|
||
|
selectorFactory) {
|
||
|
var _contextTypes, _childContextTypes;
|
||
|
|
||
|
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
|
||
|
_ref$getDisplayName = _ref.getDisplayName,
|
||
|
getDisplayName = _ref$getDisplayName === undefined ? function (name) {
|
||
|
return 'ConnectAdvanced(' + name + ')';
|
||
|
} : _ref$getDisplayName,
|
||
|
_ref$methodName = _ref.methodName,
|
||
|
methodName = _ref$methodName === undefined ? 'connectAdvanced' : _ref$methodName,
|
||
|
_ref$renderCountProp = _ref.renderCountProp,
|
||
|
renderCountProp = _ref$renderCountProp === undefined ? undefined : _ref$renderCountProp,
|
||
|
_ref$shouldHandleStat = _ref.shouldHandleStateChanges,
|
||
|
shouldHandleStateChanges = _ref$shouldHandleStat === undefined ? true : _ref$shouldHandleStat,
|
||
|
_ref$storeKey = _ref.storeKey,
|
||
|
storeKey = _ref$storeKey === undefined ? 'store' : _ref$storeKey,
|
||
|
_ref$withRef = _ref.withRef,
|
||
|
withRef = _ref$withRef === undefined ? false : _ref$withRef,
|
||
|
connectOptions = _objectWithoutProperties(_ref, ['getDisplayName', 'methodName', 'renderCountProp', 'shouldHandleStateChanges', 'storeKey', 'withRef']);
|
||
|
|
||
|
var subscriptionKey = storeKey + 'Subscription';
|
||
|
var version = hotReloadingVersion++;
|
||
|
|
||
|
var contextTypes = (_contextTypes = {}, _contextTypes[storeKey] = _PropTypes.storeShape, _contextTypes[subscriptionKey] = _PropTypes.subscriptionShape, _contextTypes);
|
||
|
var childContextTypes = (_childContextTypes = {}, _childContextTypes[subscriptionKey] = _PropTypes.subscriptionShape, _childContextTypes);
|
||
|
|
||
|
return function wrapWithConnect(WrappedComponent) {
|
||
|
(0, _invariant2.default)(typeof WrappedComponent == 'function', 'You must pass a component to the function returned by ' + ('connect. Instead received ' + JSON.stringify(WrappedComponent)));
|
||
|
|
||
|
var wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
||
|
|
||
|
var displayName = getDisplayName(wrappedComponentName);
|
||
|
|
||
|
var selectorFactoryOptions = _extends({}, connectOptions, {
|
||
|
getDisplayName: getDisplayName,
|
||
|
methodName: methodName,
|
||
|
renderCountProp: renderCountProp,
|
||
|
shouldHandleStateChanges: shouldHandleStateChanges,
|
||
|
storeKey: storeKey,
|
||
|
withRef: withRef,
|
||
|
displayName: displayName,
|
||
|
wrappedComponentName: wrappedComponentName,
|
||
|
WrappedComponent: WrappedComponent
|
||
|
});
|
||
|
|
||
|
var Connect = function (_Component) {
|
||
|
_inherits(Connect, _Component);
|
||
|
|
||
|
function Connect(props, context) {
|
||
|
_classCallCheck(this, Connect);
|
||
|
|
||
|
var _this = _possibleConstructorReturn(this, _Component.call(this, props, context));
|
||
|
|
||
|
_this.version = version;
|
||
|
_this.state = {};
|
||
|
_this.renderCount = 0;
|
||
|
_this.store = props[storeKey] || context[storeKey];
|
||
|
_this.propsMode = Boolean(props[storeKey]);
|
||
|
_this.setWrappedInstance = _this.setWrappedInstance.bind(_this);
|
||
|
|
||
|
(0, _invariant2.default)(_this.store, 'Could not find "' + storeKey + '" in either the context or props of ' + ('"' + displayName + '". Either wrap the root component in a <Provider>, ') + ('or explicitly pass "' + storeKey + '" as a prop to "' + displayName + '".'));
|
||
|
|
||
|
_this.initSelector();
|
||
|
_this.initSubscription();
|
||
|
return _this;
|
||
|
}
|
||
|
|
||
|
Connect.prototype.getChildContext = function getChildContext() {
|
||
|
var _ref2;
|
||
|
|
||
|
// If this component received store from props, its subscription should be transparent
|
||
|
// to any descendants receiving store+subscription from context; it passes along
|
||
|
// subscription passed to it. Otherwise, it shadows the parent subscription, which allows
|
||
|
// Connect to control ordering of notifications to flow top-down.
|
||
|
var subscription = this.propsMode ? null : this.subscription;
|
||
|
return _ref2 = {}, _ref2[subscriptionKey] = subscription || this.context[subscriptionKey], _ref2;
|
||
|
};
|
||
|
|
||
|
Connect.prototype.componentDidMount = function componentDidMount() {
|
||
|
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();
|
||
|
this.selector.run(this.props);
|
||
|
if (this.selector.shouldComponentUpdate) this.forceUpdate();
|
||
|
};
|
||
|
|
||
|
Connect.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps) {
|
||
|
this.selector.run(nextProps);
|
||
|
};
|
||
|
|
||
|
Connect.prototype.shouldComponentUpdate = function shouldComponentUpdate() {
|
||
|
return this.selector.shouldComponentUpdate;
|
||
|
};
|
||
|
|
||
|
Connect.prototype.componentWillUnmount = function componentWillUnmount() {
|
||
|
if (this.subscription) this.subscription.tryUnsubscribe();
|
||
|
this.subscription = null;
|
||
|
this.notifyNestedSubs = noop;
|
||
|
this.store = null;
|
||
|
this.selector.run = noop;
|
||
|
this.selector.shouldComponentUpdate = false;
|
||
|
};
|
||
|
|
||
|
Connect.prototype.getWrappedInstance = function getWrappedInstance() {
|
||
|
(0, _invariant2.default)(withRef, 'To access the wrapped instance, you need to specify ' + ('{ withRef: true } in the options argument of the ' + methodName + '() call.'));
|
||
|
return this.wrappedInstance;
|
||
|
};
|
||
|
|
||
|
Connect.prototype.setWrappedInstance = function setWrappedInstance(ref) {
|
||
|
this.wrappedInstance = ref;
|
||
|
};
|
||
|
|
||
|
Connect.prototype.initSelector = function initSelector() {
|
||
|
var sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions);
|
||
|
this.selector = makeSelectorStateful(sourceSelector, this.store);
|
||
|
this.selector.run(this.props);
|
||
|
};
|
||
|
|
||
|
Connect.prototype.initSubscription = function initSubscription() {
|
||
|
if (!shouldHandleStateChanges) return;
|
||
|
|
||
|
// parentSub's source should match where store came from: props vs. context. A component
|
||
|
// connected to the store via props shouldn't use subscription from context, or vice versa.
|
||
|
var parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey];
|
||
|
this.subscription = new _Subscription2.default(this.store, parentSub, this.onStateChange.bind(this));
|
||
|
|
||
|
// `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in
|
||
|
// the middle of the notification loop, where `this.subscription` will then be null. An
|
||
|
// extra null check every change can be avoided by copying the method onto `this` and then
|
||
|
// replacing it with a no-op on unmount. This can probably be avoided if Subscription's
|
||
|
// listeners logic is changed to not call listeners that have been unsubscribed in the
|
||
|
// middle of the notification loop.
|
||
|
this.notifyNestedSubs = this.subscription.notifyNestedSubs.bind(this.subscription);
|
||
|
};
|
||
|
|
||
|
Connect.prototype.onStateChange = function onStateChange() {
|
||
|
this.selector.run(this.props);
|
||
|
|
||
|
if (!this.selector.shouldComponentUpdate) {
|
||
|
this.notifyNestedSubs();
|
||
|
} else {
|
||
|
this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate;
|
||
|
this.setState(dummyState);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Connect.prototype.notifyNestedSubsOnComponentDidUpdate = function notifyNestedSubsOnComponentDidUpdate() {
|
||
|
// `componentDidUpdate` is conditionally implemented when `onStateChange` determines it
|
||
|
// needs to notify nested subs. Once called, it unimplements itself until further state
|
||
|
// changes occur. Doing it this way vs having a permanent `componentDidUpdate` that does
|
||
|
// a boolean check every time avoids an extra method call most of the time, resulting
|
||
|
// in some perf boost.
|
||
|
this.componentDidUpdate = undefined;
|
||
|
this.notifyNestedSubs();
|
||
|
};
|
||
|
|
||
|
Connect.prototype.isSubscribed = function isSubscribed() {
|
||
|
return Boolean(this.subscription) && this.subscription.isSubscribed();
|
||
|
};
|
||
|
|
||
|
Connect.prototype.addExtraProps = function addExtraProps(props) {
|
||
|
if (!withRef && !renderCountProp && !(this.propsMode && this.subscription)) 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
|
||
|
var withExtras = _extends({}, props);
|
||
|
if (withRef) withExtras.ref = this.setWrappedInstance;
|
||
|
if (renderCountProp) withExtras[renderCountProp] = this.renderCount++;
|
||
|
if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription;
|
||
|
return withExtras;
|
||
|
};
|
||
|
|
||
|
Connect.prototype.render = function render() {
|
||
|
var selector = this.selector;
|
||
|
selector.shouldComponentUpdate = false;
|
||
|
|
||
|
if (selector.error) {
|
||
|
throw selector.error;
|
||
|
} else {
|
||
|
return (0, _react.createElement)(WrappedComponent, this.addExtraProps(selector.props));
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return Connect;
|
||
|
}(_react.Component);
|
||
|
|
||
|
Connect.WrappedComponent = WrappedComponent;
|
||
|
Connect.displayName = displayName;
|
||
|
Connect.childContextTypes = childContextTypes;
|
||
|
Connect.contextTypes = contextTypes;
|
||
|
Connect.propTypes = contextTypes;
|
||
|
|
||
|
if (process.env.NODE_ENV !== 'production') {
|
||
|
Connect.prototype.componentWillUpdate = function componentWillUpdate() {
|
||
|
var _this2 = this;
|
||
|
|
||
|
// We are hot reloading!
|
||
|
if (this.version !== version) {
|
||
|
this.version = version;
|
||
|
this.initSelector();
|
||
|
|
||
|
// If any connected descendants don't hot reload (and resubscribe in the process), their
|
||
|
// listeners will be lost when we unsubscribe. Unfortunately, by copying over all
|
||
|
// listeners, this does mean that the old versions of connected descendants will still be
|
||
|
// notified of state changes; however, their onStateChange function is a no-op so this
|
||
|
// isn't a huge deal.
|
||
|
var oldListeners = [];
|
||
|
|
||
|
if (this.subscription) {
|
||
|
oldListeners = this.subscription.listeners.get();
|
||
|
this.subscription.tryUnsubscribe();
|
||
|
}
|
||
|
this.initSubscription();
|
||
|
if (shouldHandleStateChanges) {
|
||
|
this.subscription.trySubscribe();
|
||
|
oldListeners.forEach(function (listener) {
|
||
|
return _this2.subscription.listeners.subscribe(listener);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
return (0, _hoistNonReactStatics2.default)(Connect, WrappedComponent);
|
||
|
};
|
||
|
}
|