connect.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. "use strict";
  2. var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
  3. exports.__esModule = true;
  4. exports.default = exports.initializeConnect = void 0;
  5. var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
  6. var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose"));
  7. var _hoistNonReactStatics = _interopRequireDefault(require("hoist-non-react-statics"));
  8. var React = _interopRequireWildcard(require("react"));
  9. var _reactIs = require("react-is");
  10. var _selectorFactory = _interopRequireDefault(require("../connect/selectorFactory"));
  11. var _mapDispatchToProps = require("../connect/mapDispatchToProps");
  12. var _mapStateToProps = require("../connect/mapStateToProps");
  13. var _mergeProps = require("../connect/mergeProps");
  14. var _Subscription = require("../utils/Subscription");
  15. var _useIsomorphicLayoutEffect = require("../utils/useIsomorphicLayoutEffect");
  16. var _shallowEqual = _interopRequireDefault(require("../utils/shallowEqual"));
  17. var _warning = _interopRequireDefault(require("../utils/warning"));
  18. var _Context = require("./Context");
  19. var _useSyncExternalStore = require("../utils/useSyncExternalStore");
  20. const _excluded = ["reactReduxForwardedRef"];
  21. function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
  22. function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
  23. let useSyncExternalStore = _useSyncExternalStore.notInitialized;
  24. const initializeConnect = fn => {
  25. useSyncExternalStore = fn;
  26. }; // Define some constant arrays just to avoid re-creating these
  27. exports.initializeConnect = initializeConnect;
  28. const EMPTY_ARRAY = [null, 0];
  29. const NO_SUBSCRIPTION_ARRAY = [null, null]; // Attempts to stringify whatever not-really-a-component value we were given
  30. // for logging in an error message
  31. const stringifyComponent = Comp => {
  32. try {
  33. return JSON.stringify(Comp);
  34. } catch (err) {
  35. return String(Comp);
  36. }
  37. };
  38. // This is "just" a `useLayoutEffect`, but with two modifications:
  39. // - we need to fall back to `useEffect` in SSR to avoid annoying warnings
  40. // - we extract this to a separate function to avoid closing over values
  41. // and causing memory leaks
  42. function useIsomorphicLayoutEffectWithArgs(effectFunc, effectArgs, dependencies) {
  43. (0, _useIsomorphicLayoutEffect.useIsomorphicLayoutEffect)(() => effectFunc(...effectArgs), dependencies);
  44. } // Effect callback, extracted: assign the latest props values to refs for later usage
  45. function captureWrapperProps(lastWrapperProps, lastChildProps, renderIsScheduled, wrapperProps, // actualChildProps: unknown,
  46. childPropsFromStoreUpdate, notifyNestedSubs) {
  47. // We want to capture the wrapper props and child props we used for later comparisons
  48. lastWrapperProps.current = wrapperProps;
  49. renderIsScheduled.current = false; // If the render was from a store update, clear out that reference and cascade the subscriber update
  50. if (childPropsFromStoreUpdate.current) {
  51. childPropsFromStoreUpdate.current = null;
  52. notifyNestedSubs();
  53. }
  54. } // Effect callback, extracted: subscribe to the Redux store or nearest connected ancestor,
  55. // check for updates after dispatched actions, and trigger re-renders.
  56. function subscribeUpdates(shouldHandleStateChanges, store, subscription, childPropsSelector, lastWrapperProps, lastChildProps, renderIsScheduled, isMounted, childPropsFromStoreUpdate, notifyNestedSubs, // forceComponentUpdateDispatch: React.Dispatch<any>,
  57. additionalSubscribeListener) {
  58. // If we're not subscribed to the store, nothing to do here
  59. if (!shouldHandleStateChanges) return () => {}; // Capture values for checking if and when this component unmounts
  60. let didUnsubscribe = false;
  61. let lastThrownError = null; // We'll run this callback every time a store subscription update propagates to this component
  62. const checkForUpdates = () => {
  63. if (didUnsubscribe || !isMounted.current) {
  64. // Don't run stale listeners.
  65. // Redux doesn't guarantee unsubscriptions happen until next dispatch.
  66. return;
  67. } // TODO We're currently calling getState ourselves here, rather than letting `uSES` do it
  68. const latestStoreState = store.getState();
  69. let newChildProps, error;
  70. try {
  71. // Actually run the selector with the most recent store state and wrapper props
  72. // to determine what the child props should be
  73. newChildProps = childPropsSelector(latestStoreState, lastWrapperProps.current);
  74. } catch (e) {
  75. error = e;
  76. lastThrownError = e;
  77. }
  78. if (!error) {
  79. lastThrownError = null;
  80. } // If the child props haven't changed, nothing to do here - cascade the subscription update
  81. if (newChildProps === lastChildProps.current) {
  82. if (!renderIsScheduled.current) {
  83. notifyNestedSubs();
  84. }
  85. } else {
  86. // Save references to the new child props. Note that we track the "child props from store update"
  87. // as a ref instead of a useState/useReducer because we need a way to determine if that value has
  88. // been processed. If this went into useState/useReducer, we couldn't clear out the value without
  89. // forcing another re-render, which we don't want.
  90. lastChildProps.current = newChildProps;
  91. childPropsFromStoreUpdate.current = newChildProps;
  92. renderIsScheduled.current = true; // TODO This is hacky and not how `uSES` is meant to be used
  93. // Trigger the React `useSyncExternalStore` subscriber
  94. additionalSubscribeListener();
  95. }
  96. }; // Actually subscribe to the nearest connected ancestor (or store)
  97. subscription.onStateChange = checkForUpdates;
  98. subscription.trySubscribe(); // Pull data from the store after first render in case the store has
  99. // changed since we began.
  100. checkForUpdates();
  101. const unsubscribeWrapper = () => {
  102. didUnsubscribe = true;
  103. subscription.tryUnsubscribe();
  104. subscription.onStateChange = null;
  105. if (lastThrownError) {
  106. // It's possible that we caught an error due to a bad mapState function, but the
  107. // parent re-rendered without this component and we're about to unmount.
  108. // This shouldn't happen as long as we do top-down subscriptions correctly, but
  109. // if we ever do those wrong, this throw will surface the error in our tests.
  110. // In that case, throw the error from here so it doesn't get lost.
  111. throw lastThrownError;
  112. }
  113. };
  114. return unsubscribeWrapper;
  115. } // Reducer initial state creation for our update reducer
  116. const initStateUpdates = () => EMPTY_ARRAY;
  117. function strictEqual(a, b) {
  118. return a === b;
  119. }
  120. /**
  121. * Infers the type of props that a connector will inject into a component.
  122. */
  123. let hasWarnedAboutDeprecatedPureOption = false;
  124. /**
  125. * Connects a React component to a Redux store.
  126. *
  127. * - Without arguments, just wraps the component, without changing the behavior / props
  128. *
  129. * - If 2 params are passed (3rd param, mergeProps, is skipped), default behavior
  130. * is to override ownProps (as stated in the docs), so what remains is everything that's
  131. * not a state or dispatch prop
  132. *
  133. * - When 3rd param is passed, we don't know if ownProps propagate and whether they
  134. * should be valid component props, because it depends on mergeProps implementation.
  135. * As such, it is the user's responsibility to extend ownProps interface from state or
  136. * dispatch props or both when applicable
  137. *
  138. * @param mapStateToProps A function that extracts values from state
  139. * @param mapDispatchToProps Setup for dispatching actions
  140. * @param mergeProps Optional callback to merge state and dispatch props together
  141. * @param options Options for configuring the connection
  142. *
  143. */
  144. function connect(mapStateToProps, mapDispatchToProps, mergeProps, {
  145. // The `pure` option has been removed, so TS doesn't like us destructuring this to check its existence.
  146. // @ts-ignore
  147. pure,
  148. areStatesEqual = strictEqual,
  149. areOwnPropsEqual = _shallowEqual.default,
  150. areStatePropsEqual = _shallowEqual.default,
  151. areMergedPropsEqual = _shallowEqual.default,
  152. // use React's forwardRef to expose a ref of the wrapped component
  153. forwardRef = false,
  154. // the context consumer to use
  155. context = _Context.ReactReduxContext
  156. } = {}) {
  157. if (process.env.NODE_ENV !== 'production') {
  158. if (pure !== undefined && !hasWarnedAboutDeprecatedPureOption) {
  159. hasWarnedAboutDeprecatedPureOption = true;
  160. (0, _warning.default)('The `pure` option has been removed. `connect` is now always a "pure/memoized" component');
  161. }
  162. }
  163. const Context = context;
  164. const initMapStateToProps = (0, _mapStateToProps.mapStateToPropsFactory)(mapStateToProps);
  165. const initMapDispatchToProps = (0, _mapDispatchToProps.mapDispatchToPropsFactory)(mapDispatchToProps);
  166. const initMergeProps = (0, _mergeProps.mergePropsFactory)(mergeProps);
  167. const shouldHandleStateChanges = Boolean(mapStateToProps);
  168. const wrapWithConnect = WrappedComponent => {
  169. if (process.env.NODE_ENV !== 'production' && !(0, _reactIs.isValidElementType)(WrappedComponent)) {
  170. throw new Error(`You must pass a component to the function returned by connect. Instead received ${stringifyComponent(WrappedComponent)}`);
  171. }
  172. const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
  173. const displayName = `Connect(${wrappedComponentName})`;
  174. const selectorFactoryOptions = {
  175. shouldHandleStateChanges,
  176. displayName,
  177. wrappedComponentName,
  178. WrappedComponent,
  179. // @ts-ignore
  180. initMapStateToProps,
  181. // @ts-ignore
  182. initMapDispatchToProps,
  183. initMergeProps,
  184. areStatesEqual,
  185. areStatePropsEqual,
  186. areOwnPropsEqual,
  187. areMergedPropsEqual
  188. };
  189. function ConnectFunction(props) {
  190. const [propsContext, reactReduxForwardedRef, wrapperProps] = React.useMemo(() => {
  191. // Distinguish between actual "data" props that were passed to the wrapper component,
  192. // and values needed to control behavior (forwarded refs, alternate context instances).
  193. // To maintain the wrapperProps object reference, memoize this destructuring.
  194. const {
  195. reactReduxForwardedRef
  196. } = props,
  197. wrapperProps = (0, _objectWithoutPropertiesLoose2.default)(props, _excluded);
  198. return [props.context, reactReduxForwardedRef, wrapperProps];
  199. }, [props]);
  200. const ContextToUse = React.useMemo(() => {
  201. // Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
  202. // Memoize the check that determines which context instance we should use.
  203. return propsContext && propsContext.Consumer && // @ts-ignore
  204. (0, _reactIs.isContextConsumer)( /*#__PURE__*/React.createElement(propsContext.Consumer, null)) ? propsContext : Context;
  205. }, [propsContext, Context]); // Retrieve the store and ancestor subscription via context, if available
  206. const contextValue = React.useContext(ContextToUse); // The store _must_ exist as either a prop or in context.
  207. // We'll check to see if it _looks_ like a Redux store first.
  208. // This allows us to pass through a `store` prop that is just a plain value.
  209. const didStoreComeFromProps = Boolean(props.store) && Boolean(props.store.getState) && Boolean(props.store.dispatch);
  210. const didStoreComeFromContext = Boolean(contextValue) && Boolean(contextValue.store);
  211. if (process.env.NODE_ENV !== 'production' && !didStoreComeFromProps && !didStoreComeFromContext) {
  212. 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.`);
  213. } // Based on the previous check, one of these must be true
  214. const store = didStoreComeFromProps ? props.store : contextValue.store;
  215. const getServerState = didStoreComeFromContext ? contextValue.getServerState : store.getState;
  216. const childPropsSelector = React.useMemo(() => {
  217. // The child props selector needs the store reference as an input.
  218. // Re-create this selector whenever the store changes.
  219. return (0, _selectorFactory.default)(store.dispatch, selectorFactoryOptions);
  220. }, [store]);
  221. const [subscription, notifyNestedSubs] = React.useMemo(() => {
  222. if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY; // This Subscription's source should match where store came from: props vs. context. A component
  223. // connected to the store via props shouldn't use subscription from context, or vice versa.
  224. const subscription = (0, _Subscription.createSubscription)(store, didStoreComeFromProps ? undefined : contextValue.subscription); // `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in
  225. // the middle of the notification loop, where `subscription` will then be null. This can
  226. // probably be avoided if Subscription's listeners logic is changed to not call listeners
  227. // that have been unsubscribed in the middle of the notification loop.
  228. const notifyNestedSubs = subscription.notifyNestedSubs.bind(subscription);
  229. return [subscription, notifyNestedSubs];
  230. }, [store, didStoreComeFromProps, contextValue]); // Determine what {store, subscription} value should be put into nested context, if necessary,
  231. // and memoize that value to avoid unnecessary context updates.
  232. const overriddenContextValue = React.useMemo(() => {
  233. if (didStoreComeFromProps) {
  234. // This component is directly subscribed to a store from props.
  235. // We don't want descendants reading from this store - pass down whatever
  236. // the existing context value is from the nearest connected ancestor.
  237. return contextValue;
  238. } // Otherwise, put this component's subscription instance into context, so that
  239. // connected descendants won't update until after this component is done
  240. return (0, _extends2.default)({}, contextValue, {
  241. subscription
  242. });
  243. }, [didStoreComeFromProps, contextValue, subscription]); // Set up refs to coordinate values between the subscription effect and the render logic
  244. const lastChildProps = React.useRef();
  245. const lastWrapperProps = React.useRef(wrapperProps);
  246. const childPropsFromStoreUpdate = React.useRef();
  247. const renderIsScheduled = React.useRef(false);
  248. const isProcessingDispatch = React.useRef(false);
  249. const isMounted = React.useRef(false);
  250. const latestSubscriptionCallbackError = React.useRef();
  251. (0, _useIsomorphicLayoutEffect.useIsomorphicLayoutEffect)(() => {
  252. isMounted.current = true;
  253. return () => {
  254. isMounted.current = false;
  255. };
  256. }, []);
  257. const actualChildPropsSelector = React.useMemo(() => {
  258. const selector = () => {
  259. // Tricky logic here:
  260. // - This render may have been triggered by a Redux store update that produced new child props
  261. // - However, we may have gotten new wrapper props after that
  262. // If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
  263. // But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
  264. // So, we'll use the child props from store update only if the wrapper props are the same as last time.
  265. if (childPropsFromStoreUpdate.current && wrapperProps === lastWrapperProps.current) {
  266. return childPropsFromStoreUpdate.current;
  267. } // TODO We're reading the store directly in render() here. Bad idea?
  268. // This will likely cause Bad Things (TM) to happen in Concurrent Mode.
  269. // Note that we do this because on renders _not_ caused by store updates, we need the latest store state
  270. // to determine what the child props should be.
  271. return childPropsSelector(store.getState(), wrapperProps);
  272. };
  273. return selector;
  274. }, [store, wrapperProps]); // We need this to execute synchronously every time we re-render. However, React warns
  275. // about useLayoutEffect in SSR, so we try to detect environment and fall back to
  276. // just useEffect instead to avoid the warning, since neither will run anyway.
  277. const subscribeForReact = React.useMemo(() => {
  278. const subscribe = reactListener => {
  279. if (!subscription) {
  280. return () => {};
  281. }
  282. return subscribeUpdates(shouldHandleStateChanges, store, subscription, // @ts-ignore
  283. childPropsSelector, lastWrapperProps, lastChildProps, renderIsScheduled, isMounted, childPropsFromStoreUpdate, notifyNestedSubs, reactListener);
  284. };
  285. return subscribe;
  286. }, [subscription]);
  287. useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [lastWrapperProps, lastChildProps, renderIsScheduled, wrapperProps, childPropsFromStoreUpdate, notifyNestedSubs]);
  288. let actualChildProps;
  289. try {
  290. actualChildProps = useSyncExternalStore( // TODO We're passing through a big wrapper that does a bunch of extra side effects besides subscribing
  291. subscribeForReact, // TODO This is incredibly hacky. We've already processed the store update and calculated new child props,
  292. // TODO and we're just passing that through so it triggers a re-render for us rather than relying on `uSES`.
  293. actualChildPropsSelector, getServerState ? () => childPropsSelector(getServerState(), wrapperProps) : actualChildPropsSelector);
  294. } catch (err) {
  295. if (latestSubscriptionCallbackError.current) {
  296. ;
  297. err.message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`;
  298. }
  299. throw err;
  300. }
  301. (0, _useIsomorphicLayoutEffect.useIsomorphicLayoutEffect)(() => {
  302. latestSubscriptionCallbackError.current = undefined;
  303. childPropsFromStoreUpdate.current = undefined;
  304. lastChildProps.current = actualChildProps;
  305. }); // Now that all that's done, we can finally try to actually render the child component.
  306. // We memoize the elements for the rendered child component as an optimization.
  307. const renderedWrappedComponent = React.useMemo(() => {
  308. return (
  309. /*#__PURE__*/
  310. // @ts-ignore
  311. React.createElement(WrappedComponent, (0, _extends2.default)({}, actualChildProps, {
  312. ref: reactReduxForwardedRef
  313. }))
  314. );
  315. }, [reactReduxForwardedRef, WrappedComponent, actualChildProps]); // If React sees the exact same element reference as last time, it bails out of re-rendering
  316. // that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.
  317. const renderedChild = React.useMemo(() => {
  318. if (shouldHandleStateChanges) {
  319. // If this component is subscribed to store updates, we need to pass its own
  320. // subscription instance down to our descendants. That means rendering the same
  321. // Context instance, and putting a different value into the context.
  322. return /*#__PURE__*/React.createElement(ContextToUse.Provider, {
  323. value: overriddenContextValue
  324. }, renderedWrappedComponent);
  325. }
  326. return renderedWrappedComponent;
  327. }, [ContextToUse, renderedWrappedComponent, overriddenContextValue]);
  328. return renderedChild;
  329. }
  330. const _Connect = React.memo(ConnectFunction);
  331. // Add a hacky cast to get the right output type
  332. const Connect = _Connect;
  333. Connect.WrappedComponent = WrappedComponent;
  334. Connect.displayName = ConnectFunction.displayName = displayName;
  335. if (forwardRef) {
  336. const _forwarded = React.forwardRef(function forwardConnectRef(props, ref) {
  337. // @ts-ignore
  338. return /*#__PURE__*/React.createElement(Connect, (0, _extends2.default)({}, props, {
  339. reactReduxForwardedRef: ref
  340. }));
  341. });
  342. const forwarded = _forwarded;
  343. forwarded.displayName = displayName;
  344. forwarded.WrappedComponent = WrappedComponent;
  345. return (0, _hoistNonReactStatics.default)(forwarded, WrappedComponent);
  346. }
  347. return (0, _hoistNonReactStatics.default)(Connect, WrappedComponent);
  348. };
  349. return wrapWithConnect;
  350. }
  351. var _default = connect;
  352. exports.default = _default;