You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
392 lines
20 KiB
392 lines
20 KiB
"use strict"; |
|
|
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault")["default"]; |
|
|
|
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard")["default"]; |
|
|
|
exports.__esModule = true; |
|
exports["default"] = connectAdvanced; |
|
|
|
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); |
|
|
|
var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose")); |
|
|
|
var _hoistNonReactStatics = _interopRequireDefault(require("hoist-non-react-statics")); |
|
|
|
var _react = _interopRequireWildcard(require("react")); |
|
|
|
var _reactIs = require("react-is"); |
|
|
|
var _Subscription = require("../utils/Subscription"); |
|
|
|
var _useIsomorphicLayoutEffect = require("../utils/useIsomorphicLayoutEffect"); |
|
|
|
var _Context = require("./Context"); |
|
|
|
var _excluded = ["getDisplayName", "methodName", "renderCountProp", "shouldHandleStateChanges", "storeKey", "withRef", "forwardRef", "context"], |
|
_excluded2 = ["reactReduxForwardedRef"]; |
|
// Define some constant arrays just to avoid re-creating these |
|
var EMPTY_ARRAY = []; |
|
var NO_SUBSCRIPTION_ARRAY = [null, null]; |
|
|
|
var stringifyComponent = function stringifyComponent(Comp) { |
|
try { |
|
return JSON.stringify(Comp); |
|
} catch (err) { |
|
return String(Comp); |
|
} |
|
}; |
|
|
|
function storeStateUpdatesReducer(state, action) { |
|
var updateCount = state[1]; |
|
return [action.payload, updateCount + 1]; |
|
} |
|
|
|
function useIsomorphicLayoutEffectWithArgs(effectFunc, effectArgs, dependencies) { |
|
(0, _useIsomorphicLayoutEffect.useIsomorphicLayoutEffect)(function () { |
|
return effectFunc.apply(void 0, effectArgs); |
|
}, dependencies); |
|
} |
|
|
|
function captureWrapperProps(lastWrapperProps, lastChildProps, renderIsScheduled, wrapperProps, actualChildProps, childPropsFromStoreUpdate, notifyNestedSubs) { |
|
// We want to capture the wrapper props and child props we used for later comparisons |
|
lastWrapperProps.current = wrapperProps; |
|
lastChildProps.current = actualChildProps; |
|
renderIsScheduled.current = false; // If the render was from a store update, clear out that reference and cascade the subscriber update |
|
|
|
if (childPropsFromStoreUpdate.current) { |
|
childPropsFromStoreUpdate.current = null; |
|
notifyNestedSubs(); |
|
} |
|
} |
|
|
|
function subscribeUpdates(shouldHandleStateChanges, store, subscription, childPropsSelector, lastWrapperProps, lastChildProps, renderIsScheduled, childPropsFromStoreUpdate, notifyNestedSubs, forceComponentUpdateDispatch) { |
|
// If we're not subscribed to the store, nothing to do here |
|
if (!shouldHandleStateChanges) return; // Capture values for checking if and when this component unmounts |
|
|
|
var didUnsubscribe = false; |
|
var lastThrownError = null; // We'll run this callback every time a store subscription update propagates to this component |
|
|
|
var checkForUpdates = function checkForUpdates() { |
|
if (didUnsubscribe) { |
|
// Don't run stale listeners. |
|
// Redux doesn't guarantee unsubscriptions happen until next dispatch. |
|
return; |
|
} |
|
|
|
var latestStoreState = store.getState(); |
|
var newChildProps, error; |
|
|
|
try { |
|
// Actually run the selector with the most recent store state and wrapper props |
|
// to determine what the child props should be |
|
newChildProps = childPropsSelector(latestStoreState, lastWrapperProps.current); |
|
} catch (e) { |
|
error = e; |
|
lastThrownError = e; |
|
} |
|
|
|
if (!error) { |
|
lastThrownError = null; |
|
} // If the child props haven't changed, nothing to do here - cascade the subscription update |
|
|
|
|
|
if (newChildProps === lastChildProps.current) { |
|
if (!renderIsScheduled.current) { |
|
notifyNestedSubs(); |
|
} |
|
} else { |
|
// Save references to the new child props. Note that we track the "child props from store update" |
|
// as a ref instead of a useState/useReducer because we need a way to determine if that value has |
|
// been processed. If this went into useState/useReducer, we couldn't clear out the value without |
|
// forcing another re-render, which we don't want. |
|
lastChildProps.current = newChildProps; |
|
childPropsFromStoreUpdate.current = newChildProps; |
|
renderIsScheduled.current = true; // If the child props _did_ change (or we caught an error), this wrapper component needs to re-render |
|
|
|
forceComponentUpdateDispatch({ |
|
type: 'STORE_UPDATED', |
|
payload: { |
|
error: error |
|
} |
|
}); |
|
} |
|
}; // Actually subscribe to the nearest connected ancestor (or store) |
|
|
|
|
|
subscription.onStateChange = checkForUpdates; |
|
subscription.trySubscribe(); // Pull data from the store after first render in case the store has |
|
// changed since we began. |
|
|
|
checkForUpdates(); |
|
|
|
var unsubscribeWrapper = function unsubscribeWrapper() { |
|
didUnsubscribe = true; |
|
subscription.tryUnsubscribe(); |
|
subscription.onStateChange = null; |
|
|
|
if (lastThrownError) { |
|
// It's possible that we caught an error due to a bad mapState function, but the |
|
// parent re-rendered without this component and we're about to unmount. |
|
// This shouldn't happen as long as we do top-down subscriptions correctly, but |
|
// if we ever do those wrong, this throw will surface the error in our tests. |
|
// In that case, throw the error from here so it doesn't get lost. |
|
throw lastThrownError; |
|
} |
|
}; |
|
|
|
return unsubscribeWrapper; |
|
} |
|
|
|
var initStateUpdates = function initStateUpdates() { |
|
return [null, 0]; |
|
}; |
|
|
|
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, // options object: |
|
_ref) { |
|
if (_ref === void 0) { |
|
_ref = {}; |
|
} |
|
|
|
var _ref2 = _ref, |
|
_ref2$getDisplayName = _ref2.getDisplayName, |
|
getDisplayName = _ref2$getDisplayName === void 0 ? function (name) { |
|
return "ConnectAdvanced(" + name + ")"; |
|
} : _ref2$getDisplayName, |
|
_ref2$methodName = _ref2.methodName, |
|
methodName = _ref2$methodName === void 0 ? 'connectAdvanced' : _ref2$methodName, |
|
_ref2$renderCountProp = _ref2.renderCountProp, |
|
renderCountProp = _ref2$renderCountProp === void 0 ? undefined : _ref2$renderCountProp, |
|
_ref2$shouldHandleSta = _ref2.shouldHandleStateChanges, |
|
shouldHandleStateChanges = _ref2$shouldHandleSta === void 0 ? true : _ref2$shouldHandleSta, |
|
_ref2$storeKey = _ref2.storeKey, |
|
storeKey = _ref2$storeKey === void 0 ? 'store' : _ref2$storeKey, |
|
_ref2$withRef = _ref2.withRef, |
|
withRef = _ref2$withRef === void 0 ? false : _ref2$withRef, |
|
_ref2$forwardRef = _ref2.forwardRef, |
|
forwardRef = _ref2$forwardRef === void 0 ? false : _ref2$forwardRef, |
|
_ref2$context = _ref2.context, |
|
context = _ref2$context === void 0 ? _Context.ReactReduxContext : _ref2$context, |
|
connectOptions = (0, _objectWithoutPropertiesLoose2["default"])(_ref2, _excluded); |
|
|
|
if (process.env.NODE_ENV !== 'production') { |
|
if (renderCountProp !== undefined) { |
|
throw new Error("renderCountProp is removed. render counting is built into the latest React Dev Tools profiling extension"); |
|
} |
|
|
|
if (withRef) { |
|
throw new Error('withRef is removed. To access the wrapped instance, use a ref on the connected component'); |
|
} |
|
|
|
var customStoreWarningMessage = 'To use a custom Redux store for specific components, create a custom React context with ' + "React.createContext(), and pass the context object to React Redux's Provider and specific components" + ' like: <Provider context={MyContext}><ConnectedComponent context={MyContext} /></Provider>. ' + 'You may also pass a {context : MyContext} option to connect'; |
|
|
|
if (storeKey !== 'store') { |
|
throw new Error('storeKey has been removed and does not do anything. ' + customStoreWarningMessage); |
|
} |
|
} |
|
|
|
var Context = context; |
|
return function wrapWithConnect(WrappedComponent) { |
|
if (process.env.NODE_ENV !== 'production' && !(0, _reactIs.isValidElementType)(WrappedComponent)) { |
|
throw new Error("You must pass a component to the function returned by " + (methodName + ". Instead received " + stringifyComponent(WrappedComponent))); |
|
} |
|
|
|
var wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; |
|
var displayName = getDisplayName(wrappedComponentName); |
|
var selectorFactoryOptions = (0, _extends2["default"])({}, connectOptions, { |
|
getDisplayName: getDisplayName, |
|
methodName: methodName, |
|
renderCountProp: renderCountProp, |
|
shouldHandleStateChanges: shouldHandleStateChanges, |
|
storeKey: storeKey, |
|
displayName: displayName, |
|
wrappedComponentName: wrappedComponentName, |
|
WrappedComponent: WrappedComponent |
|
}); |
|
var pure = connectOptions.pure; |
|
|
|
function createChildSelector(store) { |
|
return selectorFactory(store.dispatch, selectorFactoryOptions); |
|
} // If we aren't running in "pure" mode, we don't want to memoize values. |
|
// To avoid conditionally calling hooks, we fall back to a tiny wrapper |
|
// that just executes the given callback immediately. |
|
|
|
|
|
var usePureOnlyMemo = pure ? _react.useMemo : function (callback) { |
|
return callback(); |
|
}; |
|
|
|
function ConnectFunction(props) { |
|
var _useMemo = (0, _react.useMemo)(function () { |
|
// Distinguish between actual "data" props that were passed to the wrapper component, |
|
// and values needed to control behavior (forwarded refs, alternate context instances). |
|
// To maintain the wrapperProps object reference, memoize this destructuring. |
|
var reactReduxForwardedRef = props.reactReduxForwardedRef, |
|
wrapperProps = (0, _objectWithoutPropertiesLoose2["default"])(props, _excluded2); |
|
return [props.context, reactReduxForwardedRef, wrapperProps]; |
|
}, [props]), |
|
propsContext = _useMemo[0], |
|
reactReduxForwardedRef = _useMemo[1], |
|
wrapperProps = _useMemo[2]; |
|
|
|
var ContextToUse = (0, _react.useMemo)(function () { |
|
// Users may optionally pass in a custom context instance to use instead of our ReactReduxContext. |
|
// Memoize the check that determines which context instance we should use. |
|
return propsContext && propsContext.Consumer && (0, _reactIs.isContextConsumer)( /*#__PURE__*/_react["default"].createElement(propsContext.Consumer, null)) ? propsContext : Context; |
|
}, [propsContext, Context]); // Retrieve the store and ancestor subscription via context, if available |
|
|
|
var contextValue = (0, _react.useContext)(ContextToUse); // The store _must_ exist as either a prop or in context. |
|
// We'll check to see if it _looks_ like a Redux store first. |
|
// This allows us to pass through a `store` prop that is just a plain value. |
|
|
|
var didStoreComeFromProps = Boolean(props.store) && Boolean(props.store.getState) && Boolean(props.store.dispatch); |
|
var didStoreComeFromContext = Boolean(contextValue) && Boolean(contextValue.store); |
|
|
|
if (process.env.NODE_ENV !== 'production' && !didStoreComeFromProps && !didStoreComeFromContext) { |
|
throw new Error("Could not find \"store\" in the context of " + ("\"" + displayName + "\". Either wrap the root component in a <Provider>, ") + "or pass a custom React context provider to <Provider> and the corresponding " + ("React context consumer to " + displayName + " in connect options.")); |
|
} // Based on the previous check, one of these must be true |
|
|
|
|
|
var store = didStoreComeFromProps ? props.store : contextValue.store; |
|
var childPropsSelector = (0, _react.useMemo)(function () { |
|
// The child props selector needs the store reference as an input. |
|
// Re-create this selector whenever the store changes. |
|
return createChildSelector(store); |
|
}, [store]); |
|
|
|
var _useMemo2 = (0, _react.useMemo)(function () { |
|
if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY; // This Subscription'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. |
|
|
|
// This Subscription'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 subscription = (0, _Subscription.createSubscription)(store, didStoreComeFromProps ? null : contextValue.subscription); // `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in |
|
// the middle of the notification loop, where `subscription` will then be null. 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. |
|
|
|
// `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in |
|
// the middle of the notification loop, where `subscription` will then be null. 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. |
|
var notifyNestedSubs = subscription.notifyNestedSubs.bind(subscription); |
|
return [subscription, notifyNestedSubs]; |
|
}, [store, didStoreComeFromProps, contextValue]), |
|
subscription = _useMemo2[0], |
|
notifyNestedSubs = _useMemo2[1]; // Determine what {store, subscription} value should be put into nested context, if necessary, |
|
// and memoize that value to avoid unnecessary context updates. |
|
|
|
|
|
var overriddenContextValue = (0, _react.useMemo)(function () { |
|
if (didStoreComeFromProps) { |
|
// This component is directly subscribed to a store from props. |
|
// We don't want descendants reading from this store - pass down whatever |
|
// the existing context value is from the nearest connected ancestor. |
|
return contextValue; |
|
} // Otherwise, put this component's subscription instance into context, so that |
|
// connected descendants won't update until after this component is done |
|
|
|
|
|
return (0, _extends2["default"])({}, contextValue, { |
|
subscription: subscription |
|
}); |
|
}, [didStoreComeFromProps, contextValue, subscription]); // We need to force this wrapper component to re-render whenever a Redux store update |
|
// causes a change to the calculated child component props (or we caught an error in mapState) |
|
|
|
var _useReducer = (0, _react.useReducer)(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates), |
|
_useReducer$ = _useReducer[0], |
|
previousStateUpdateResult = _useReducer$[0], |
|
forceComponentUpdateDispatch = _useReducer[1]; // Propagate any mapState/mapDispatch errors upwards |
|
|
|
|
|
if (previousStateUpdateResult && previousStateUpdateResult.error) { |
|
throw previousStateUpdateResult.error; |
|
} // Set up refs to coordinate values between the subscription effect and the render logic |
|
|
|
|
|
var lastChildProps = (0, _react.useRef)(); |
|
var lastWrapperProps = (0, _react.useRef)(wrapperProps); |
|
var childPropsFromStoreUpdate = (0, _react.useRef)(); |
|
var renderIsScheduled = (0, _react.useRef)(false); |
|
var actualChildProps = usePureOnlyMemo(function () { |
|
// Tricky logic here: |
|
// - This render may have been triggered by a Redux store update that produced new child props |
|
// - However, we may have gotten new wrapper props after that |
|
// If we have new child props, and the same wrapper props, we know we should use the new child props as-is. |
|
// But, if we have new wrapper props, those might change the child props, so we have to recalculate things. |
|
// So, we'll use the child props from store update only if the wrapper props are the same as last time. |
|
if (childPropsFromStoreUpdate.current && wrapperProps === lastWrapperProps.current) { |
|
return childPropsFromStoreUpdate.current; |
|
} // TODO We're reading the store directly in render() here. Bad idea? |
|
// This will likely cause Bad Things (TM) to happen in Concurrent Mode. |
|
// Note that we do this because on renders _not_ caused by store updates, we need the latest store state |
|
// to determine what the child props should be. |
|
|
|
|
|
return childPropsSelector(store.getState(), wrapperProps); |
|
}, [store, previousStateUpdateResult, wrapperProps]); // We need this to execute synchronously every time we re-render. However, React warns |
|
// about useLayoutEffect in SSR, so we try to detect environment and fall back to |
|
// just useEffect instead to avoid the warning, since neither will run anyway. |
|
|
|
useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [lastWrapperProps, lastChildProps, renderIsScheduled, wrapperProps, actualChildProps, childPropsFromStoreUpdate, notifyNestedSubs]); // Our re-subscribe logic only runs when the store/subscription setup changes |
|
|
|
useIsomorphicLayoutEffectWithArgs(subscribeUpdates, [shouldHandleStateChanges, store, subscription, childPropsSelector, lastWrapperProps, lastChildProps, renderIsScheduled, childPropsFromStoreUpdate, notifyNestedSubs, forceComponentUpdateDispatch], [store, subscription, childPropsSelector]); // Now that all that's done, we can finally try to actually render the child component. |
|
// We memoize the elements for the rendered child component as an optimization. |
|
|
|
var renderedWrappedComponent = (0, _react.useMemo)(function () { |
|
return /*#__PURE__*/_react["default"].createElement(WrappedComponent, (0, _extends2["default"])({}, actualChildProps, { |
|
ref: reactReduxForwardedRef |
|
})); |
|
}, [reactReduxForwardedRef, WrappedComponent, actualChildProps]); // If React sees the exact same element reference as last time, it bails out of re-rendering |
|
// that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate. |
|
|
|
var renderedChild = (0, _react.useMemo)(function () { |
|
if (shouldHandleStateChanges) { |
|
// If this component is subscribed to store updates, we need to pass its own |
|
// subscription instance down to our descendants. That means rendering the same |
|
// Context instance, and putting a different value into the context. |
|
return /*#__PURE__*/_react["default"].createElement(ContextToUse.Provider, { |
|
value: overriddenContextValue |
|
}, renderedWrappedComponent); |
|
} |
|
|
|
return renderedWrappedComponent; |
|
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue]); |
|
return renderedChild; |
|
} // If we're in "pure" mode, ensure our wrapper component only re-renders when incoming props have changed. |
|
|
|
|
|
var Connect = pure ? _react["default"].memo(ConnectFunction) : ConnectFunction; |
|
Connect.WrappedComponent = WrappedComponent; |
|
Connect.displayName = ConnectFunction.displayName = displayName; |
|
|
|
if (forwardRef) { |
|
var forwarded = _react["default"].forwardRef(function forwardConnectRef(props, ref) { |
|
return /*#__PURE__*/_react["default"].createElement(Connect, (0, _extends2["default"])({}, props, { |
|
reactReduxForwardedRef: ref |
|
})); |
|
}); |
|
|
|
forwarded.displayName = displayName; |
|
forwarded.WrappedComponent = WrappedComponent; |
|
return (0, _hoistNonReactStatics["default"])(forwarded, WrappedComponent); |
|
} |
|
|
|
return (0, _hoistNonReactStatics["default"])(Connect, WrappedComponent); |
|
}; |
|
} |