438 lines
21 KiB
JavaScript
438 lines
21 KiB
JavaScript
"use strict";
|
|
|
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
|
|
exports.__esModule = true;
|
|
exports.default = exports.initializeConnect = void 0;
|
|
|
|
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 _selectorFactory = _interopRequireDefault(require("../connect/selectorFactory"));
|
|
|
|
var _mapDispatchToProps = require("../connect/mapDispatchToProps");
|
|
|
|
var _mapStateToProps = require("../connect/mapStateToProps");
|
|
|
|
var _mergeProps = require("../connect/mergeProps");
|
|
|
|
var _Subscription = require("../utils/Subscription");
|
|
|
|
var _useIsomorphicLayoutEffect = require("../utils/useIsomorphicLayoutEffect");
|
|
|
|
var _shallowEqual = _interopRequireDefault(require("../utils/shallowEqual"));
|
|
|
|
var _warning = _interopRequireDefault(require("../utils/warning"));
|
|
|
|
var _Context = require("./Context");
|
|
|
|
var _useSyncExternalStore = require("../utils/useSyncExternalStore");
|
|
|
|
const _excluded = ["reactReduxForwardedRef"];
|
|
|
|
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); }
|
|
|
|
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; }
|
|
|
|
let useSyncExternalStore = _useSyncExternalStore.notInitialized;
|
|
|
|
const initializeConnect = fn => {
|
|
useSyncExternalStore = fn;
|
|
}; // Define some constant arrays just to avoid re-creating these
|
|
|
|
|
|
exports.initializeConnect = initializeConnect;
|
|
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) {
|
|
(0, _useIsomorphicLayoutEffect.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.default,
|
|
areStatePropsEqual = _shallowEqual.default,
|
|
areMergedPropsEqual = _shallowEqual.default,
|
|
// use React's forwardRef to expose a ref of the wrapped component
|
|
forwardRef = false,
|
|
// the context consumer to use
|
|
context = _Context.ReactReduxContext
|
|
} = {}) {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
if (pure !== undefined && !hasWarnedAboutDeprecatedPureOption) {
|
|
hasWarnedAboutDeprecatedPureOption = true;
|
|
(0, _warning.default)('The `pure` option has been removed. `connect` is now always a "pure/memoized" component');
|
|
}
|
|
}
|
|
|
|
const Context = context;
|
|
const initMapStateToProps = (0, _mapStateToProps.mapStateToPropsFactory)(mapStateToProps);
|
|
const initMapDispatchToProps = (0, _mapDispatchToProps.mapDispatchToPropsFactory)(mapDispatchToProps);
|
|
const initMergeProps = (0, _mergeProps.mergePropsFactory)(mergeProps);
|
|
const shouldHandleStateChanges = Boolean(mapStateToProps);
|
|
|
|
const 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 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] = React.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 = (0, _objectWithoutPropertiesLoose2.default)(props, _excluded);
|
|
return [props.context, reactReduxForwardedRef, wrapperProps];
|
|
}, [props]);
|
|
const ContextToUse = React.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
|
|
(0, _reactIs.isContextConsumer)( /*#__PURE__*/React.createElement(propsContext.Consumer, null)) ? propsContext : Context;
|
|
}, [propsContext, Context]); // Retrieve the store and ancestor subscription via context, if available
|
|
|
|
const contextValue = 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.
|
|
|
|
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 = React.useMemo(() => {
|
|
// The child props selector needs the store reference as an input.
|
|
// Re-create this selector whenever the store changes.
|
|
return (0, _selectorFactory.default)(store.dispatch, selectorFactoryOptions);
|
|
}, [store]);
|
|
const [subscription, notifyNestedSubs] = React.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 = (0, _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 = React.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 (0, _extends2.default)({}, contextValue, {
|
|
subscription
|
|
});
|
|
}, [didStoreComeFromProps, contextValue, subscription]); // Set up refs to coordinate values between the subscription effect and the render logic
|
|
|
|
const lastChildProps = React.useRef();
|
|
const lastWrapperProps = React.useRef(wrapperProps);
|
|
const childPropsFromStoreUpdate = React.useRef();
|
|
const renderIsScheduled = React.useRef(false);
|
|
const isProcessingDispatch = React.useRef(false);
|
|
const isMounted = React.useRef(false);
|
|
const latestSubscriptionCallbackError = React.useRef();
|
|
(0, _useIsomorphicLayoutEffect.useIsomorphicLayoutEffect)(() => {
|
|
isMounted.current = true;
|
|
return () => {
|
|
isMounted.current = false;
|
|
};
|
|
}, []);
|
|
const actualChildPropsSelector = React.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 = React.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;
|
|
}
|
|
|
|
(0, _useIsomorphicLayoutEffect.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 = React.useMemo(() => {
|
|
return (
|
|
/*#__PURE__*/
|
|
// @ts-ignore
|
|
React.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.
|
|
|
|
const renderedChild = React.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, (0, _extends2.default)({}, props, {
|
|
reactReduxForwardedRef: ref
|
|
}));
|
|
});
|
|
|
|
const forwarded = _forwarded;
|
|
forwarded.displayName = displayName;
|
|
forwarded.WrappedComponent = WrappedComponent;
|
|
return (0, _hoistNonReactStatics.default)(forwarded, WrappedComponent);
|
|
}
|
|
|
|
return (0, _hoistNonReactStatics.default)(Connect, WrappedComponent);
|
|
};
|
|
|
|
return wrapWithConnect;
|
|
}
|
|
|
|
var _default = connect;
|
|
exports.default = _default; |