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.
		
		
		
		
		
			
		
			
				
					
					
						
							410 lines
						
					
					
						
							19 KiB
						
					
					
				
			
		
		
	
	
							410 lines
						
					
					
						
							19 KiB
						
					
					
				import _extends from "@babel/runtime/helpers/esm/extends";
 | 
						|
import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
 | 
						|
const _excluded = ["reactReduxForwardedRef"];
 | 
						|
 | 
						|
/* eslint-disable valid-jsdoc, @typescript-eslint/no-unused-vars */
 | 
						|
import hoistStatics from 'hoist-non-react-statics';
 | 
						|
import React, { useContext, useMemo, useRef } from 'react';
 | 
						|
import { isValidElementType, isContextConsumer } from 'react-is';
 | 
						|
import defaultSelectorFactory from '../connect/selectorFactory';
 | 
						|
import { mapDispatchToPropsFactory } from '../connect/mapDispatchToProps';
 | 
						|
import { mapStateToPropsFactory } from '../connect/mapStateToProps';
 | 
						|
import { mergePropsFactory } from '../connect/mergeProps';
 | 
						|
import { createSubscription } from '../utils/Subscription';
 | 
						|
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect';
 | 
						|
import shallowEqual from '../utils/shallowEqual';
 | 
						|
import warning from '../utils/warning';
 | 
						|
import { ReactReduxContext } from './Context';
 | 
						|
import { notInitialized } from '../utils/useSyncExternalStore';
 | 
						|
let useSyncExternalStore = notInitialized;
 | 
						|
export const initializeConnect = fn => {
 | 
						|
  useSyncExternalStore = fn;
 | 
						|
}; // Define some constant arrays just to avoid re-creating these
 | 
						|
 | 
						|
const EMPTY_ARRAY = [null, 0];
 | 
						|
const NO_SUBSCRIPTION_ARRAY = [null, null]; // Attempts to stringify whatever not-really-a-component value we were given
 | 
						|
// for logging in an error message
 | 
						|
 | 
						|
const stringifyComponent = Comp => {
 | 
						|
  try {
 | 
						|
    return JSON.stringify(Comp);
 | 
						|
  } catch (err) {
 | 
						|
    return String(Comp);
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
// This is "just" a `useLayoutEffect`, but with two modifications:
 | 
						|
// - we need to fall back to `useEffect` in SSR to avoid annoying warnings
 | 
						|
// - we extract this to a separate function to avoid closing over values
 | 
						|
//   and causing memory leaks
 | 
						|
function useIsomorphicLayoutEffectWithArgs(effectFunc, effectArgs, dependencies) {
 | 
						|
  useIsomorphicLayoutEffect(() => effectFunc(...effectArgs), dependencies);
 | 
						|
} // Effect callback, extracted: assign the latest props values to refs for later usage
 | 
						|
 | 
						|
 | 
						|
function captureWrapperProps(lastWrapperProps, lastChildProps, renderIsScheduled, wrapperProps, // actualChildProps: unknown,
 | 
						|
childPropsFromStoreUpdate, notifyNestedSubs) {
 | 
						|
  // We want to capture the wrapper props and child props we used for later comparisons
 | 
						|
  lastWrapperProps.current = wrapperProps;
 | 
						|
  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();
 | 
						|
  }
 | 
						|
} // Effect callback, extracted: subscribe to the Redux store or nearest connected ancestor,
 | 
						|
// check for updates after dispatched actions, and trigger re-renders.
 | 
						|
 | 
						|
 | 
						|
function subscribeUpdates(shouldHandleStateChanges, store, subscription, childPropsSelector, lastWrapperProps, lastChildProps, renderIsScheduled, isMounted, childPropsFromStoreUpdate, notifyNestedSubs, // forceComponentUpdateDispatch: React.Dispatch<any>,
 | 
						|
additionalSubscribeListener) {
 | 
						|
  // 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
 | 
						|
 | 
						|
  let didUnsubscribe = false;
 | 
						|
  let lastThrownError = null; // We'll run this callback every time a store subscription update propagates to this component
 | 
						|
 | 
						|
  const checkForUpdates = () => {
 | 
						|
    if (didUnsubscribe || !isMounted.current) {
 | 
						|
      // Don't run stale listeners.
 | 
						|
      // Redux doesn't guarantee unsubscriptions happen until next dispatch.
 | 
						|
      return;
 | 
						|
    } // TODO We're currently calling getState ourselves here, rather than letting `uSES` do it
 | 
						|
 | 
						|
 | 
						|
    const latestStoreState = store.getState();
 | 
						|
    let 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; // TODO This is hacky and not how `uSES` is meant to be used
 | 
						|
      // Trigger the React `useSyncExternalStore` subscriber
 | 
						|
 | 
						|
      additionalSubscribeListener();
 | 
						|
    }
 | 
						|
  }; // 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();
 | 
						|
 | 
						|
  const 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;
 | 
						|
} // Reducer initial state creation for our update reducer
 | 
						|
 | 
						|
 | 
						|
const initStateUpdates = () => EMPTY_ARRAY;
 | 
						|
 | 
						|
function strictEqual(a, b) {
 | 
						|
  return a === b;
 | 
						|
}
 | 
						|
/**
 | 
						|
 * Infers the type of props that a connector will inject into a component.
 | 
						|
 */
 | 
						|
 | 
						|
 | 
						|
let hasWarnedAboutDeprecatedPureOption = false;
 | 
						|
/**
 | 
						|
 * Connects a React component to a Redux store.
 | 
						|
 *
 | 
						|
 * - Without arguments, just wraps the component, without changing the behavior / props
 | 
						|
 *
 | 
						|
 * - If 2 params are passed (3rd param, mergeProps, is skipped), default behavior
 | 
						|
 * is to override ownProps (as stated in the docs), so what remains is everything that's
 | 
						|
 * not a state or dispatch prop
 | 
						|
 *
 | 
						|
 * - When 3rd param is passed, we don't know if ownProps propagate and whether they
 | 
						|
 * should be valid component props, because it depends on mergeProps implementation.
 | 
						|
 * As such, it is the user's responsibility to extend ownProps interface from state or
 | 
						|
 * dispatch props or both when applicable
 | 
						|
 *
 | 
						|
 * @param mapStateToProps A function that extracts values from state
 | 
						|
 * @param mapDispatchToProps Setup for dispatching actions
 | 
						|
 * @param mergeProps Optional callback to merge state and dispatch props together
 | 
						|
 * @param options Options for configuring the connection
 | 
						|
 *
 | 
						|
 */
 | 
						|
 | 
						|
function connect(mapStateToProps, mapDispatchToProps, mergeProps, {
 | 
						|
  // The `pure` option has been removed, so TS doesn't like us destructuring this to check its existence.
 | 
						|
  // @ts-ignore
 | 
						|
  pure,
 | 
						|
  areStatesEqual = strictEqual,
 | 
						|
  areOwnPropsEqual = shallowEqual,
 | 
						|
  areStatePropsEqual = shallowEqual,
 | 
						|
  areMergedPropsEqual = shallowEqual,
 | 
						|
  // use React's forwardRef to expose a ref of the wrapped component
 | 
						|
  forwardRef = false,
 | 
						|
  // the context consumer to use
 | 
						|
  context = ReactReduxContext
 | 
						|
} = {}) {
 | 
						|
  if (process.env.NODE_ENV !== 'production') {
 | 
						|
    if (pure !== undefined && !hasWarnedAboutDeprecatedPureOption) {
 | 
						|
      hasWarnedAboutDeprecatedPureOption = true;
 | 
						|
      warning('The `pure` option has been removed. `connect` is now always a "pure/memoized" component');
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  const Context = context;
 | 
						|
  const initMapStateToProps = mapStateToPropsFactory(mapStateToProps);
 | 
						|
  const initMapDispatchToProps = mapDispatchToPropsFactory(mapDispatchToProps);
 | 
						|
  const initMergeProps = mergePropsFactory(mergeProps);
 | 
						|
  const shouldHandleStateChanges = Boolean(mapStateToProps);
 | 
						|
 | 
						|
  const wrapWithConnect = WrappedComponent => {
 | 
						|
    if (process.env.NODE_ENV !== 'production' && !isValidElementType(WrappedComponent)) {
 | 
						|
      throw new Error(`You must pass a component to the function returned by connect. Instead received ${stringifyComponent(WrappedComponent)}`);
 | 
						|
    }
 | 
						|
 | 
						|
    const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
 | 
						|
    const displayName = `Connect(${wrappedComponentName})`;
 | 
						|
    const selectorFactoryOptions = {
 | 
						|
      shouldHandleStateChanges,
 | 
						|
      displayName,
 | 
						|
      wrappedComponentName,
 | 
						|
      WrappedComponent,
 | 
						|
      // @ts-ignore
 | 
						|
      initMapStateToProps,
 | 
						|
      // @ts-ignore
 | 
						|
      initMapDispatchToProps,
 | 
						|
      initMergeProps,
 | 
						|
      areStatesEqual,
 | 
						|
      areStatePropsEqual,
 | 
						|
      areOwnPropsEqual,
 | 
						|
      areMergedPropsEqual
 | 
						|
    };
 | 
						|
 | 
						|
    function ConnectFunction(props) {
 | 
						|
      const [propsContext, reactReduxForwardedRef, wrapperProps] = useMemo(() => {
 | 
						|
        // 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.
 | 
						|
        const {
 | 
						|
          reactReduxForwardedRef
 | 
						|
        } = props,
 | 
						|
              wrapperProps = _objectWithoutPropertiesLoose(props, _excluded);
 | 
						|
 | 
						|
        return [props.context, reactReduxForwardedRef, wrapperProps];
 | 
						|
      }, [props]);
 | 
						|
      const ContextToUse = useMemo(() => {
 | 
						|
        // 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 && // @ts-ignore
 | 
						|
        isContextConsumer( /*#__PURE__*/React.createElement(propsContext.Consumer, null)) ? propsContext : Context;
 | 
						|
      }, [propsContext, Context]); // Retrieve the store and ancestor subscription via context, if available
 | 
						|
 | 
						|
      const contextValue = 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.
 | 
						|
 | 
						|
      const didStoreComeFromProps = Boolean(props.store) && Boolean(props.store.getState) && Boolean(props.store.dispatch);
 | 
						|
      const 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
 | 
						|
 | 
						|
 | 
						|
      const store = didStoreComeFromProps ? props.store : contextValue.store;
 | 
						|
      const getServerState = didStoreComeFromContext ? contextValue.getServerState : store.getState;
 | 
						|
      const childPropsSelector = useMemo(() => {
 | 
						|
        // The child props selector needs the store reference as an input.
 | 
						|
        // Re-create this selector whenever the store changes.
 | 
						|
        return defaultSelectorFactory(store.dispatch, selectorFactoryOptions);
 | 
						|
      }, [store]);
 | 
						|
      const [subscription, notifyNestedSubs] = useMemo(() => {
 | 
						|
        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.
 | 
						|
 | 
						|
        const subscription = createSubscription(store, didStoreComeFromProps ? undefined : 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.
 | 
						|
 | 
						|
        const notifyNestedSubs = subscription.notifyNestedSubs.bind(subscription);
 | 
						|
        return [subscription, notifyNestedSubs];
 | 
						|
      }, [store, didStoreComeFromProps, contextValue]); // Determine what {store, subscription} value should be put into nested context, if necessary,
 | 
						|
      // and memoize that value to avoid unnecessary context updates.
 | 
						|
 | 
						|
      const overriddenContextValue = useMemo(() => {
 | 
						|
        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 _extends({}, contextValue, {
 | 
						|
          subscription
 | 
						|
        });
 | 
						|
      }, [didStoreComeFromProps, contextValue, subscription]); // Set up refs to coordinate values between the subscription effect and the render logic
 | 
						|
 | 
						|
      const lastChildProps = useRef();
 | 
						|
      const lastWrapperProps = useRef(wrapperProps);
 | 
						|
      const childPropsFromStoreUpdate = useRef();
 | 
						|
      const renderIsScheduled = useRef(false);
 | 
						|
      const isProcessingDispatch = useRef(false);
 | 
						|
      const isMounted = useRef(false);
 | 
						|
      const latestSubscriptionCallbackError = useRef();
 | 
						|
      useIsomorphicLayoutEffect(() => {
 | 
						|
        isMounted.current = true;
 | 
						|
        return () => {
 | 
						|
          isMounted.current = false;
 | 
						|
        };
 | 
						|
      }, []);
 | 
						|
      const actualChildPropsSelector = useMemo(() => {
 | 
						|
        const selector = () => {
 | 
						|
          // 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);
 | 
						|
        };
 | 
						|
 | 
						|
        return selector;
 | 
						|
      }, [store, 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.
 | 
						|
 | 
						|
      const subscribeForReact = useMemo(() => {
 | 
						|
        const subscribe = reactListener => {
 | 
						|
          if (!subscription) {
 | 
						|
            return () => {};
 | 
						|
          }
 | 
						|
 | 
						|
          return subscribeUpdates(shouldHandleStateChanges, store, subscription, // @ts-ignore
 | 
						|
          childPropsSelector, lastWrapperProps, lastChildProps, renderIsScheduled, isMounted, childPropsFromStoreUpdate, notifyNestedSubs, reactListener);
 | 
						|
        };
 | 
						|
 | 
						|
        return subscribe;
 | 
						|
      }, [subscription]);
 | 
						|
      useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [lastWrapperProps, lastChildProps, renderIsScheduled, wrapperProps, childPropsFromStoreUpdate, notifyNestedSubs]);
 | 
						|
      let actualChildProps;
 | 
						|
 | 
						|
      try {
 | 
						|
        actualChildProps = useSyncExternalStore( // TODO We're passing through a big wrapper that does a bunch of extra side effects besides subscribing
 | 
						|
        subscribeForReact, // TODO This is incredibly hacky. We've already processed the store update and calculated new child props,
 | 
						|
        // TODO and we're just passing that through so it triggers a re-render for us rather than relying on `uSES`.
 | 
						|
        actualChildPropsSelector, getServerState ? () => childPropsSelector(getServerState(), wrapperProps) : actualChildPropsSelector);
 | 
						|
      } catch (err) {
 | 
						|
        if (latestSubscriptionCallbackError.current) {
 | 
						|
          ;
 | 
						|
          err.message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`;
 | 
						|
        }
 | 
						|
 | 
						|
        throw err;
 | 
						|
      }
 | 
						|
 | 
						|
      useIsomorphicLayoutEffect(() => {
 | 
						|
        latestSubscriptionCallbackError.current = undefined;
 | 
						|
        childPropsFromStoreUpdate.current = undefined;
 | 
						|
        lastChildProps.current = actualChildProps;
 | 
						|
      }); // 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.
 | 
						|
 | 
						|
      const renderedWrappedComponent = useMemo(() => {
 | 
						|
        return (
 | 
						|
          /*#__PURE__*/
 | 
						|
          // @ts-ignore
 | 
						|
          React.createElement(WrappedComponent, _extends({}, 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.
 | 
						|
 | 
						|
      const renderedChild = useMemo(() => {
 | 
						|
        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.createElement(ContextToUse.Provider, {
 | 
						|
            value: overriddenContextValue
 | 
						|
          }, renderedWrappedComponent);
 | 
						|
        }
 | 
						|
 | 
						|
        return renderedWrappedComponent;
 | 
						|
      }, [ContextToUse, renderedWrappedComponent, overriddenContextValue]);
 | 
						|
      return renderedChild;
 | 
						|
    }
 | 
						|
 | 
						|
    const _Connect = React.memo(ConnectFunction);
 | 
						|
 | 
						|
    // Add a hacky cast to get the right output type
 | 
						|
    const Connect = _Connect;
 | 
						|
    Connect.WrappedComponent = WrappedComponent;
 | 
						|
    Connect.displayName = ConnectFunction.displayName = displayName;
 | 
						|
 | 
						|
    if (forwardRef) {
 | 
						|
      const _forwarded = React.forwardRef(function forwardConnectRef(props, ref) {
 | 
						|
        // @ts-ignore
 | 
						|
        return /*#__PURE__*/React.createElement(Connect, _extends({}, props, {
 | 
						|
          reactReduxForwardedRef: ref
 | 
						|
        }));
 | 
						|
      });
 | 
						|
 | 
						|
      const forwarded = _forwarded;
 | 
						|
      forwarded.displayName = displayName;
 | 
						|
      forwarded.WrappedComponent = WrappedComponent;
 | 
						|
      return hoistStatics(forwarded, WrappedComponent);
 | 
						|
    }
 | 
						|
 | 
						|
    return hoistStatics(Connect, WrappedComponent);
 | 
						|
  };
 | 
						|
 | 
						|
  return wrapWithConnect;
 | 
						|
}
 | 
						|
 | 
						|
export default connect; |