123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 |
- import _extends from "@babel/runtime/helpers/esm/extends";
- import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
- var _excluded = ["getDisplayName", "methodName", "renderCountProp", "shouldHandleStateChanges", "storeKey", "withRef", "forwardRef", "context"],
- _excluded2 = ["reactReduxForwardedRef"];
- import hoistStatics from 'hoist-non-react-statics';
- import React, { useContext, useMemo, useRef, useReducer } from 'react';
- import { isValidElementType, isContextConsumer } from 'react-is';
- import { createSubscription } from '../utils/Subscription';
- import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect';
- import { ReactReduxContext } from './Context'; // 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) {
- 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];
- };
- export default 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 ? ReactReduxContext : _ref2$context,
- connectOptions = _objectWithoutPropertiesLoose(_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' && !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 = _extends({}, 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 ? useMemo : function (callback) {
- return callback();
- };
- function ConnectFunction(props) {
- var _useMemo = 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 = _objectWithoutPropertiesLoose(props, _excluded2);
- return [props.context, reactReduxForwardedRef, wrapperProps];
- }, [props]),
- propsContext = _useMemo[0],
- reactReduxForwardedRef = _useMemo[1],
- wrapperProps = _useMemo[2];
- var ContextToUse = 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 && isContextConsumer( /*#__PURE__*/React.createElement(propsContext.Consumer, null)) ? propsContext : Context;
- }, [propsContext, Context]); // Retrieve the store and ancestor subscription via context, if available
- var 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.
- 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 = 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 = 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 = 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 = 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 _extends({}, 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 = 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 = useRef();
- var lastWrapperProps = useRef(wrapperProps);
- var childPropsFromStoreUpdate = useRef();
- var renderIsScheduled = 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 = useMemo(function () {
- return /*#__PURE__*/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.
- var renderedChild = 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.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.memo(ConnectFunction) : ConnectFunction;
- Connect.WrappedComponent = WrappedComponent;
- Connect.displayName = ConnectFunction.displayName = displayName;
- if (forwardRef) {
- var forwarded = React.forwardRef(function forwardConnectRef(props, ref) {
- return /*#__PURE__*/React.createElement(Connect, _extends({}, props, {
- reactReduxForwardedRef: ref
- }));
- });
- forwarded.displayName = displayName;
- forwarded.WrappedComponent = WrappedComponent;
- return hoistStatics(forwarded, WrappedComponent);
- }
- return hoistStatics(Connect, WrappedComponent);
- };
- }
|