From 69d9df48dc0c8d4fca6899cf4535f01ee0f63cbe Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 18 Jul 2021 11:41:10 -0400 Subject: [PATCH 01/12] Add additional comments to connectAdvanced --- src/components/connectAdvanced.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/components/connectAdvanced.tsx b/src/components/connectAdvanced.tsx index 77e433814..2394d99f3 100644 --- a/src/components/connectAdvanced.tsx +++ b/src/components/connectAdvanced.tsx @@ -17,6 +17,8 @@ import { const EMPTY_ARRAY: [unknown, number] = [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: unknown) => { try { return JSON.stringify(Comp) @@ -25,6 +27,11 @@ const stringifyComponent = (Comp: unknown) => { } } +// Reducer for our "forceUpdate" equivalent. +// This primarily stores the current error, if any, +// but also an update counter. +// Since we're returning a new array anyway, in theory the counter isn't needed. +// Or for that matter, since the dispatch gets a new object, we don't even need an array. function storeStateUpdatesReducer( state: [unknown, number], action: { payload: unknown } @@ -35,6 +42,10 @@ function storeStateUpdatesReducer( type EffectFunc = (...args: any[]) => void | ReturnType +// 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: EffectFunc, effectArgs: any[], @@ -43,6 +54,7 @@ function useIsomorphicLayoutEffectWithArgs( useIsomorphicLayoutEffect(() => effectFunc(...effectArgs), dependencies) } +// Effect callback, extracted: assign the latest props values to refs for later usage function captureWrapperProps( lastWrapperProps: React.MutableRefObject, lastChildProps: React.MutableRefObject, @@ -64,6 +76,8 @@ function captureWrapperProps( } } +// 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: boolean, store: Store, @@ -160,6 +174,7 @@ function subscribeUpdates( return unsubscribeWrapper } +// Reducer initial state creation for our update reducer const initStateUpdates = () => EMPTY_ARRAY export interface ConnectProps { From c932ea3b95d8fb225d6a9b0df8e92219d64adfc5 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 18 Jul 2021 11:41:26 -0400 Subject: [PATCH 02/12] Fix types for wrapper props args --- src/components/connectAdvanced.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/connectAdvanced.tsx b/src/components/connectAdvanced.tsx index 2394d99f3..4f990789e 100644 --- a/src/components/connectAdvanced.tsx +++ b/src/components/connectAdvanced.tsx @@ -59,8 +59,8 @@ function captureWrapperProps( lastWrapperProps: React.MutableRefObject, lastChildProps: React.MutableRefObject, renderIsScheduled: React.MutableRefObject, - wrapperProps: React.MutableRefObject, - actualChildProps: React.MutableRefObject, + wrapperProps: unknown, + actualChildProps: unknown, childPropsFromStoreUpdate: React.MutableRefObject, notifyNestedSubs: () => void ) { From 28bd86e431827e77944f06453c7b1e9ac1c01943 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 18 Jul 2021 11:42:22 -0400 Subject: [PATCH 03/12] Remove dead code --- src/components/connectAdvanced.tsx | 7 -- src/connect/connect.ts | 127 ----------------------------- 2 files changed, 134 deletions(-) diff --git a/src/components/connectAdvanced.tsx b/src/components/connectAdvanced.tsx index 4f990789e..6d4852ca8 100644 --- a/src/components/connectAdvanced.tsx +++ b/src/components/connectAdvanced.tsx @@ -238,13 +238,6 @@ function connectAdvanced( type WrappedComponentProps = TOwnProps & ConnectProps - /* - return function wrapWithConnect< - WC extends React.ComponentType< - Matching, GetProps> - > - >(WrappedComponent: WC) { - */ const wrapWithConnect: AdvancedComponentDecorator< TProps, WrappedComponentProps diff --git a/src/connect/connect.ts b/src/connect/connect.ts index ae95fe28b..463df0b24 100644 --- a/src/connect/connect.ts +++ b/src/connect/connect.ts @@ -101,133 +101,6 @@ export interface ConnectOptions< forwardRef?: boolean | undefined } -/* -export interface Connect { - // tslint:disable:no-unnecessary-generics - (): InferableComponentEnhancer - - ( - mapStateToProps: MapStateToPropsParam - ): InferableComponentEnhancerWithProps - - ( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsNonObject - ): InferableComponentEnhancerWithProps - - ( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsParam - ): InferableComponentEnhancerWithProps< - ResolveThunks, - TOwnProps - > - - ( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsNonObject - ): InferableComponentEnhancerWithProps< - TStateProps & TDispatchProps, - TOwnProps - > - - ( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsParam - ): InferableComponentEnhancerWithProps< - TStateProps & ResolveThunks, - TOwnProps - > - - ( - mapStateToProps: null | undefined, - mapDispatchToProps: null | undefined, - mergeProps: MergeProps - ): InferableComponentEnhancerWithProps - - < - TStateProps = {}, - no_dispatch = {}, - TOwnProps = {}, - TMergedProps = {}, - State = DefaultState - >( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: null | undefined, - mergeProps: MergeProps - ): InferableComponentEnhancerWithProps - - ( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: MergeProps - ): InferableComponentEnhancerWithProps - - ( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: null | undefined, - mergeProps: null | undefined, - options: ConnectOptions - ): InferableComponentEnhancerWithProps - - ( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsNonObject, - mergeProps: null | undefined, - options: ConnectOptions<{}, TStateProps, TOwnProps> - ): InferableComponentEnhancerWithProps - - ( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: null | undefined, - options: ConnectOptions<{}, TStateProps, TOwnProps> - ): InferableComponentEnhancerWithProps< - ResolveThunks, - TOwnProps - > - - ( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsNonObject, - mergeProps: null | undefined, - options: ConnectOptions - ): InferableComponentEnhancerWithProps< - TStateProps & TDispatchProps, - TOwnProps - > - - ( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: null | undefined, - options: ConnectOptions - ): InferableComponentEnhancerWithProps< - TStateProps & ResolveThunks, - TOwnProps - > - - < - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - TMergedProps = {}, - State = DefaultState - >( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: MergeProps< - TStateProps, - TDispatchProps, - TOwnProps, - TMergedProps - >, - options?: ConnectOptions - ): InferableComponentEnhancerWithProps - // tslint:enable:no-unnecessary-generics -} -*/ - // createConnect with default args builds the 'official' connect behavior. Calling it with // different options opens up some testing and extensibility scenarios export function createConnect({ From 2a6b92705b8414fd72ab40610b6fb49d1062b72b Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 18 Jul 2021 11:43:45 -0400 Subject: [PATCH 04/12] Eliminate useless `createConnect` wrapper --- src/connect/connect.ts | 449 +++++++++++++++++++---------------------- 1 file changed, 210 insertions(+), 239 deletions(-) diff --git a/src/connect/connect.ts b/src/connect/connect.ts index 463df0b24..1eb5fae3d 100644 --- a/src/connect/connect.ts +++ b/src/connect/connect.ts @@ -101,245 +101,221 @@ export interface ConnectOptions< forwardRef?: boolean | undefined } -// createConnect with default args builds the 'official' connect behavior. Calling it with -// different options opens up some testing and extensibility scenarios -export function createConnect({ - connectHOC = connectAdvanced, - mapStateToPropsFactories = defaultMapStateToPropsFactories, - mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, - mergePropsFactories = defaultMergePropsFactories, - selectorFactory = defaultSelectorFactory, -} = {}) { - /* @public */ - function connect(): InferableComponentEnhancer - - /* @public */ - function connect< - TStateProps = {}, - no_dispatch = {}, - TOwnProps = {}, - State = DefaultRootState - >( - mapStateToProps: MapStateToPropsParam - ): InferableComponentEnhancerWithProps - - /* @public */ - function connect( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsNonObject - ): InferableComponentEnhancerWithProps - - /* @public */ - function connect( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsParam - ): InferableComponentEnhancerWithProps< - ResolveThunks, - TOwnProps - > +/* @public */ +function connect(): InferableComponentEnhancer - /* @public */ - function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState - >( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsNonObject - ): InferableComponentEnhancerWithProps< - TStateProps & TDispatchProps, - TOwnProps - > +/* @public */ +function connect< + TStateProps = {}, + no_dispatch = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam +): InferableComponentEnhancerWithProps - /* @public */ - function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState - >( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsParam - ): InferableComponentEnhancerWithProps< - TStateProps & ResolveThunks, - TOwnProps - > +/* @public */ +function connect( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsNonObject +): InferableComponentEnhancerWithProps - /* @public */ - function connect< - no_state = {}, - no_dispatch = {}, - TOwnProps = {}, - TMergedProps = {} - >( - mapStateToProps: null | undefined, - mapDispatchToProps: null | undefined, - mergeProps: MergeProps - ): InferableComponentEnhancerWithProps - - /* @public */ - function connect< - TStateProps = {}, - no_dispatch = {}, - TOwnProps = {}, - TMergedProps = {}, - State = DefaultRootState - >( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: null | undefined, - mergeProps: MergeProps - ): InferableComponentEnhancerWithProps - - /* @public */ - function connect< - no_state = {}, - TDispatchProps = {}, - TOwnProps = {}, - TMergedProps = {} - >( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: MergeProps - ): InferableComponentEnhancerWithProps - - /* @public */ - // @ts-ignore - function connect< - TStateProps = {}, - no_dispatch = {}, - TOwnProps = {}, - State = DefaultRootState - >( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: null | undefined, - mergeProps: null | undefined, - options: ConnectOptions - ): InferableComponentEnhancerWithProps - - /* @public */ - function connect( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsNonObject, - mergeProps: null | undefined, - options: ConnectOptions<{}, TStateProps, TOwnProps> - ): InferableComponentEnhancerWithProps - - /* @public */ - function connect( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: null | undefined, - options: ConnectOptions<{}, TStateProps, TOwnProps> - ): InferableComponentEnhancerWithProps< - ResolveThunks, - TOwnProps - > +/* @public */ +function connect( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam +): InferableComponentEnhancerWithProps, TOwnProps> - /* @public */ - function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState - >( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsNonObject, - mergeProps: null | undefined, - options: ConnectOptions - ): InferableComponentEnhancerWithProps< - TStateProps & TDispatchProps, - TOwnProps - > +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsNonObject +): InferableComponentEnhancerWithProps - /* @public */ - function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState - >( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: null | undefined, - options: ConnectOptions - ): InferableComponentEnhancerWithProps< - TStateProps & ResolveThunks, - TOwnProps - > +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam +): InferableComponentEnhancerWithProps< + TStateProps & ResolveThunks, + TOwnProps +> - /* @public */ - function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - TMergedProps = {}, - State = DefaultRootState - >( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: MergeProps< - TStateProps, - TDispatchProps, - TOwnProps, - TMergedProps - >, - options?: ConnectOptions - ): InferableComponentEnhancerWithProps - - /** - * 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?: unknown, - mapDispatchToProps?: unknown, - mergeProps?: unknown, - { - pure = true, - areStatesEqual = strictEqual, - areOwnPropsEqual = shallowEqual, - areStatePropsEqual = shallowEqual, - areMergedPropsEqual = shallowEqual, - ...extraOptions - }: ConnectOptions = {} - ): unknown { - const initMapStateToProps = match( - mapStateToProps, - // @ts-ignore - mapStateToPropsFactories, - 'mapStateToProps' - ) - const initMapDispatchToProps = match( - mapDispatchToProps, - // @ts-ignore - mapDispatchToPropsFactories, - 'mapDispatchToProps' - ) - const initMergeProps = match( - mergeProps, - // @ts-ignore - mergePropsFactories, - 'mergeProps' - ) +/* @public */ +function connect< + no_state = {}, + no_dispatch = {}, + TOwnProps = {}, + TMergedProps = {} +>( + mapStateToProps: null | undefined, + mapDispatchToProps: null | undefined, + mergeProps: MergeProps +): InferableComponentEnhancerWithProps + +/* @public */ +function connect< + TStateProps = {}, + no_dispatch = {}, + TOwnProps = {}, + TMergedProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: null | undefined, + mergeProps: MergeProps +): InferableComponentEnhancerWithProps + +/* @public */ +function connect< + no_state = {}, + TDispatchProps = {}, + TOwnProps = {}, + TMergedProps = {} +>( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: MergeProps +): InferableComponentEnhancerWithProps + +/* @public */ +// @ts-ignore +function connect< + TStateProps = {}, + no_dispatch = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: null | undefined, + mergeProps: null | undefined, + options: ConnectOptions +): InferableComponentEnhancerWithProps + +/* @public */ +function connect( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsNonObject, + mergeProps: null | undefined, + options: ConnectOptions<{}, TStateProps, TOwnProps> +): InferableComponentEnhancerWithProps + +/* @public */ +function connect( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: null | undefined, + options: ConnectOptions<{}, TStateProps, TOwnProps> +): InferableComponentEnhancerWithProps, TOwnProps> + +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsNonObject, + mergeProps: null | undefined, + options: ConnectOptions +): InferableComponentEnhancerWithProps - return connectHOC(selectorFactory as SelectorFactory, { +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: null | undefined, + options: ConnectOptions +): InferableComponentEnhancerWithProps< + TStateProps & ResolveThunks, + TOwnProps +> + +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + TMergedProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: MergeProps, + options?: ConnectOptions +): InferableComponentEnhancerWithProps + +/** + * 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?: unknown, + mapDispatchToProps?: unknown, + mergeProps?: unknown, + { + pure = true, + areStatesEqual = strictEqual, + areOwnPropsEqual = shallowEqual, + areStatePropsEqual = shallowEqual, + areMergedPropsEqual = shallowEqual, + ...extraOptions + }: ConnectOptions = {} +): unknown { + const initMapStateToProps = match( + mapStateToProps, + // @ts-ignore + defaultMapStateToPropsFactories, + 'mapStateToProps' + ) + const initMapDispatchToProps = match( + mapDispatchToProps, + // @ts-ignore + defaultMapDispatchToPropsFactories, + 'mapDispatchToProps' + ) + const initMergeProps = match( + mergeProps, + // @ts-ignore + defaultMergePropsFactories, + 'mergeProps' + ) + + return connectAdvanced( + defaultSelectorFactory as SelectorFactory, + { // used in error messages methodName: 'connect', @@ -361,13 +337,8 @@ export function createConnect({ // any extra options args can override defaults of connect or connectAdvanced ...extraOptions, - }) - } - - return connect + } + ) } -/* @public */ -const connect = /*#__PURE__*/ createConnect() - export default connect From 2dd0af27e57abe25b0d2afeeb7ba97d84f1bd079 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 18 Jul 2021 11:44:12 -0400 Subject: [PATCH 05/12] Remove connectAdvanced exports --- src/exports.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/exports.ts b/src/exports.ts index c8ef76a64..494f2ab55 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -1,10 +1,6 @@ import Provider from './components/Provider' import type { ProviderProps } from './components/Provider' -import connectAdvanced from './components/connectAdvanced' -import type { - ConnectAdvancedOptions, - ConnectProps, -} from './components/connectAdvanced' +import type { ConnectProps } from './components/connectAdvanced' import type { SelectorFactory, Selector, @@ -38,7 +34,6 @@ export type { MapStateToPropsParam, ConnectProps, ConnectedProps, - ConnectAdvancedOptions, MapDispatchToPropsFunction, MapDispatchToProps, MapDispatchToPropsFactory, @@ -49,7 +44,6 @@ export type { } export { Provider, - connectAdvanced, ReactReduxContext, connect, useDispatch, From 29f0efa4f979fe1617304d1f6f9abc61d64b2a75 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 18 Jul 2021 11:47:46 -0400 Subject: [PATCH 06/12] Formatting --- src/connect/mergeProps.ts | 56 ++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/src/connect/mergeProps.ts b/src/connect/mergeProps.ts index a6aeac91d..c4f1b9583 100644 --- a/src/connect/mergeProps.ts +++ b/src/connect/mergeProps.ts @@ -1,19 +1,37 @@ import { Dispatch } from 'redux' import verifyPlainObject from '../utils/verifyPlainObject' -type MergeProps = (stateProps: TStateProps, dispatchProps: TDispatchProps, ownProps: TOwnProps) => TMergedProps +type MergeProps = ( + stateProps: TStateProps, + dispatchProps: TDispatchProps, + ownProps: TOwnProps +) => TMergedProps -export function defaultMergeProps(stateProps: TStateProps, dispatchProps: TDispatchProps, ownProps: TOwnProps) { +export function defaultMergeProps( + stateProps: TStateProps, + dispatchProps: TDispatchProps, + ownProps: TOwnProps +) { return { ...ownProps, ...stateProps, ...dispatchProps } } -interface InitMergeOptions { - displayName: string; - pure?: boolean; - areMergedPropsEqual: (a: any, b: any) => boolean; +interface InitMergeOptions { + displayName: string + pure?: boolean + areMergedPropsEqual: (a: any, b: any) => boolean } -export function wrapMergePropsFunc(mergeProps: MergeProps): (dispatch: Dispatch, options: InitMergeOptions) => MergeProps { +export function wrapMergePropsFunc< + TStateProps, + TDispatchProps, + TOwnProps, + TMergedProps +>( + mergeProps: MergeProps +): ( + dispatch: Dispatch, + options: InitMergeOptions +) => MergeProps { return function initMergePropsProxy( dispatch, { displayName, pure, areMergedPropsEqual } @@ -21,7 +39,11 @@ export function wrapMergePropsFunc(mergeProps: MergeProps) { +export function whenMergePropsIsFunction< + TStateProps, + TDispatchProps, + TOwnProps, + TMergedProps +>( + mergeProps: MergeProps +) { return typeof mergeProps === 'function' ? wrapMergePropsFunc(mergeProps) : undefined } -export function whenMergePropsIsOmitted(mergeProps?: MergeProps) { +export function whenMergePropsIsOmitted< + TStateProps, + TDispatchProps, + TOwnProps, + TMergedProps +>( + mergeProps?: MergeProps +) { return !mergeProps ? () => defaultMergeProps : undefined } From 7e55217caa870fbe240a22997db0b0cee8ac8b1a Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 18 Jul 2021 12:00:43 -0400 Subject: [PATCH 07/12] Remove methodName option --- src/components/connectAdvanced.tsx | 13 +++---------- src/connect/connect.ts | 3 --- src/connect/selectorFactory.ts | 20 ++++---------------- src/connect/verifySubselectors.ts | 19 +++++++------------ 4 files changed, 14 insertions(+), 41 deletions(-) diff --git a/src/components/connectAdvanced.tsx b/src/components/connectAdvanced.tsx index 6d4852ca8..757db0283 100644 --- a/src/components/connectAdvanced.tsx +++ b/src/components/connectAdvanced.tsx @@ -185,7 +185,6 @@ export interface ConnectProps { export interface ConnectAdvancedOptions { getDisplayName?: (name: string) => string - methodName?: string shouldHandleStateChanges?: boolean forwardRef?: boolean context?: typeof ReactReduxContext @@ -217,10 +216,6 @@ function connectAdvanced( // probably overridden by wrapper functions such as connect() getDisplayName = (name) => `ConnectAdvanced(${name})`, - // shown in error messages - // probably overridden by wrapper functions such as connect() - methodName = 'connectAdvanced', - // determines whether this HOC subscribes to store changes shouldHandleStateChanges = true, @@ -247,10 +242,9 @@ function connectAdvanced( !isValidElementType(WrappedComponent) ) { throw new Error( - `You must pass a component to the function returned by ` + - `${methodName}. Instead received ${stringifyComponent( - WrappedComponent - )}` + `You must pass a component to the function returned by connect. Instead received ${stringifyComponent( + WrappedComponent + )}` ) } @@ -262,7 +256,6 @@ function connectAdvanced( const selectorFactoryOptions = { ...connectOptions, getDisplayName, - methodName, shouldHandleStateChanges, displayName, wrappedComponentName, diff --git a/src/connect/connect.ts b/src/connect/connect.ts index 1eb5fae3d..79a4c8d52 100644 --- a/src/connect/connect.ts +++ b/src/connect/connect.ts @@ -316,9 +316,6 @@ function connect( return connectAdvanced( defaultSelectorFactory as SelectorFactory, { - // used in error messages - methodName: 'connect', - // used to compute Connect's displayName from the wrapped component's displayName. getDisplayName: (name) => `Connect(${name})`, diff --git a/src/connect/selectorFactory.ts b/src/connect/selectorFactory.ts index 2a2990a02..5c9f5a3d7 100644 --- a/src/connect/selectorFactory.ts +++ b/src/connect/selectorFactory.ts @@ -207,23 +207,16 @@ export interface SelectorFactoryOptions< > extends PureSelectorFactoryComparisonOptions { initMapStateToProps: ( dispatch: Dispatch, - options: PureSelectorFactoryComparisonOptions & { - displayName: string - } + options: PureSelectorFactoryComparisonOptions ) => MapStateToPropsParam initMapDispatchToProps: ( dispatch: Dispatch, - options: PureSelectorFactoryComparisonOptions & { - displayName: string - } + options: PureSelectorFactoryComparisonOptions ) => MapDispatchToPropsParam initMergeProps: ( dispatch: Dispatch, - options: PureSelectorFactoryComparisonOptions & { - displayName: string - } + options: PureSelectorFactoryComparisonOptions ) => MergeProps - displayName: string } // TODO: Add more comments @@ -259,12 +252,7 @@ export default function finalPropsSelectorFactory< const mergeProps = initMergeProps(dispatch, options) if (process.env.NODE_ENV !== 'production') { - verifySubselectors( - mapStateToProps, - mapDispatchToProps, - mergeProps, - options.displayName - ) + verifySubselectors(mapStateToProps, mapDispatchToProps, mergeProps) } const selectorFactory = options.pure diff --git a/src/connect/verifySubselectors.ts b/src/connect/verifySubselectors.ts index dd7705a2f..487cdb197 100644 --- a/src/connect/verifySubselectors.ts +++ b/src/connect/verifySubselectors.ts @@ -1,19 +1,15 @@ import warning from '../utils/warning' -function verify( - selector: unknown, - methodName: string, - displayName: string -): void { +function verify(selector: unknown, methodName: string): void { if (!selector) { - throw new Error(`Unexpected value for ${methodName} in ${displayName}.`) + throw new Error(`Unexpected value for ${methodName} in connect.`) } else if ( methodName === 'mapStateToProps' || methodName === 'mapDispatchToProps' ) { if (!Object.prototype.hasOwnProperty.call(selector, 'dependsOnOwnProps')) { warning( - `The selector for ${methodName} of ${displayName} did not specify a value for dependsOnOwnProps.` + `The selector for ${methodName} of connect did not specify a value for dependsOnOwnProps.` ) } } @@ -22,10 +18,9 @@ function verify( export default function verifySubselectors( mapStateToProps: unknown, mapDispatchToProps: unknown, - mergeProps: unknown, - displayName: string + mergeProps: unknown ): void { - verify(mapStateToProps, 'mapStateToProps', displayName) - verify(mapDispatchToProps, 'mapDispatchToProps', displayName) - verify(mergeProps, 'mergeProps', displayName) + verify(mapStateToProps, 'mapStateToProps') + verify(mapDispatchToProps, 'mapDispatchToProps') + verify(mergeProps, 'mergeProps') } From a57dc19fd97680a2d871404136fb5ac4848186bc Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 18 Jul 2021 12:18:01 -0400 Subject: [PATCH 08/12] Remove displayName option --- src/components/connectAdvanced.tsx | 14 ++------------ src/connect/connect.ts | 9 +++------ test/components/connect.spec.js | 13 ------------- 3 files changed, 5 insertions(+), 31 deletions(-) diff --git a/src/components/connectAdvanced.tsx b/src/components/connectAdvanced.tsx index 757db0283..0290a6699 100644 --- a/src/components/connectAdvanced.tsx +++ b/src/components/connectAdvanced.tsx @@ -184,7 +184,6 @@ export interface ConnectProps { } export interface ConnectAdvancedOptions { - getDisplayName?: (name: string) => string shouldHandleStateChanges?: boolean forwardRef?: boolean context?: typeof ReactReduxContext @@ -212,10 +211,6 @@ function connectAdvanced( selectorFactory: SelectorFactory, // options object: { - // the func used to compute this HOC's displayName from the wrapped component's displayName. - // probably overridden by wrapper functions such as connect() - getDisplayName = (name) => `ConnectAdvanced(${name})`, - // determines whether this HOC subscribes to store changes shouldHandleStateChanges = true, @@ -251,11 +246,10 @@ function connectAdvanced( const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component' - const displayName = getDisplayName(wrappedComponentName) + const displayName = `Connect(${wrappedComponentName})` const selectorFactoryOptions = { ...connectOptions, - getDisplayName, shouldHandleStateChanges, displayName, wrappedComponentName, @@ -264,10 +258,6 @@ function connectAdvanced( const { pure } = connectOptions - function createChildSelector(store: 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. @@ -330,7 +320,7 @@ function connectAdvanced( const childPropsSelector = useMemo(() => { // The child props selector needs the store reference as an input. // Re-create this selector whenever the store changes. - return createChildSelector(store) + return selectorFactory(store.dispatch, selectorFactoryOptions) }, [store]) const [subscription, notifyNestedSubs] = useMemo(() => { diff --git a/src/connect/connect.ts b/src/connect/connect.ts index 79a4c8d52..7c99094e9 100644 --- a/src/connect/connect.ts +++ b/src/connect/connect.ts @@ -82,8 +82,8 @@ export interface ConnectOptions< TOwnProps = {}, TMergedProps = {} > extends ConnectAdvancedOptions { - pure?: boolean | undefined - areStatesEqual?: ((nextState: State, prevState: State) => boolean) | undefined + pure?: boolean + areStatesEqual?: (nextState: State, prevState: State) => boolean areOwnPropsEqual?: ( nextOwnProps: TOwnProps, @@ -98,7 +98,7 @@ export interface ConnectOptions< nextMergedProps: TMergedProps, prevMergedProps: TMergedProps ) => boolean - forwardRef?: boolean | undefined + forwardRef?: boolean } /* @public */ @@ -316,9 +316,6 @@ function connect( return connectAdvanced( defaultSelectorFactory as SelectorFactory, { - // used to compute Connect's displayName from the wrapped component's displayName. - getDisplayName: (name) => `Connect(${name})`, - // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes shouldHandleStateChanges: Boolean(mapStateToProps), diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 765596794..133d7ac86 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1729,19 +1729,6 @@ describe('React', () => { ).toBe('Connect(Component)') }) - it('should allow custom displayName', () => { - @connect(null, null, null, { - getDisplayName: (name) => `Custom(${name})`, - }) - class MyComponent extends React.Component { - render() { - return
- } - } - - expect(MyComponent.displayName).toEqual('Custom(MyComponent)') - }) - it('should expose the wrapped component as WrappedComponent', () => { class Container extends Component { render() { From cb82af2fdb0c7d9025de75acb5d8dd64bcccc9e8 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 18 Jul 2021 12:00:31 -0400 Subject: [PATCH 09/12] Remove connectAdvanced tests --- test/components/connectAdvanced.spec.js | 194 ---------------------- test/integration/server-rendering.spec.js | 2 +- 2 files changed, 1 insertion(+), 195 deletions(-) delete mode 100644 test/components/connectAdvanced.spec.js diff --git a/test/components/connectAdvanced.spec.js b/test/components/connectAdvanced.spec.js deleted file mode 100644 index 4df5df798..000000000 --- a/test/components/connectAdvanced.spec.js +++ /dev/null @@ -1,194 +0,0 @@ -import React, { Component } from 'react' -import * as rtl from '@testing-library/react' -import { Provider as ProviderMock, connectAdvanced } from '../../src/index' -import { createStore } from 'redux' -import '@testing-library/jest-dom/extend-expect' - -describe('React', () => { - describe('connectAdvanced', () => { - it('should map state and render on mount', () => { - const initialState = { - foo: 'bar', - } - - let mapCount = 0 - let renderCount = 0 - - const store = createStore(() => initialState) - - function Inner(props) { - renderCount++ - return
{JSON.stringify(props)}
- } - - const Container = connectAdvanced(() => { - return (state) => { - mapCount++ - return state - } - })(Inner) - - const tester = rtl.render( - - - - ) - - expect(tester.getByTestId('foo')).toHaveTextContent('bar') - - // Implementation detail: - // 1) Initial render - // 2) Post-mount subscription and update check - expect(mapCount).toEqual(2) - expect(renderCount).toEqual(1) - }) - - it('should render on reference change', () => { - let mapCount = 0 - let renderCount = 0 - - // force new reference on each dispatch - const store = createStore(() => ({ - foo: 'bar', - })) - - function Inner(props) { - renderCount++ - return
{JSON.stringify(props)}
- } - - const Container = connectAdvanced(() => { - return (state) => { - mapCount++ - return state - } - })(Inner) - - rtl.render( - - - - ) - - rtl.act(() => { - store.dispatch({ type: 'NEW_REFERENCE' }) - }) - - // Should have mapped the state on mount and on the dispatch - expect(mapCount).toEqual(3) - - // Should have rendered on mount and after the dispatch bacause the map - // state returned new reference - expect(renderCount).toEqual(2) - }) - - it('should not render when the returned reference does not change', () => { - const staticReference = { - foo: 'bar', - } - - let mapCount = 0 - let renderCount = 0 - - // force new reference on each dispatch - const store = createStore(() => ({ - foo: 'bar', - })) - - function Inner(props) { - renderCount++ - return
{JSON.stringify(props)}
- } - - const Container = connectAdvanced(() => { - return () => { - mapCount++ - // but return static reference - return staticReference - } - })(Inner) - - const tester = rtl.render( - - - - ) - - store.dispatch({ type: 'NEW_REFERENCE' }) - - expect(tester.getAllByTestId('foo')[0]).toHaveTextContent('bar') - - // The state should have been mapped 3 times: - // 1) Initial render - // 2) Post-mount update check - // 3) Dispatch - expect(mapCount).toEqual(3) - - // But the render should have been called only on mount since the map state - // did not return a new reference - expect(renderCount).toEqual(1) - }) - - it('should map state on own props change but not render when the reference does not change', () => { - const staticReference = { - foo: 'bar', - } - - let mapCount = 0 - let renderCount = 0 - - const store = createStore(() => staticReference) - - function Inner(props) { - renderCount++ - return
{JSON.stringify(props)}
- } - - const Container = connectAdvanced(() => { - return () => { - mapCount++ - // return the static reference - return staticReference - } - })(Inner) - - class OuterComponent extends Component { - constructor() { - super() - this.state = { foo: 'FOO' } - } - - setFoo(foo) { - this.setState({ foo }) - } - - render() { - return ( -
- -
- ) - } - } - - let outerComponent - rtl.render( - - (outerComponent = c)} /> - - ) - - outerComponent.setFoo('BAR') - - // The state should have been mapped 3 times: - // 1) Initial render - // 2) Post-mount update check - // 3) Prop change - expect(mapCount).toEqual(3) - - // render only on mount but skip on prop change because no new - // reference was returned - expect(renderCount).toEqual(1) - }) - }) -}) diff --git a/test/integration/server-rendering.spec.js b/test/integration/server-rendering.spec.js index 7d92ebf99..7585f0e30 100644 --- a/test/integration/server-rendering.spec.js +++ b/test/integration/server-rendering.spec.js @@ -2,7 +2,7 @@ * @jest-environment node * * Set this so that `window` is undefined to correctly mimic a Node SSR scenario. - * That allows connectAdvanced to fall back to `useEffect` instead of `useLayoutEffect` + * That allows connect to fall back to `useEffect` instead of `useLayoutEffect` * to avoid ugly console warnings when used with SSR. */ From ae97b093c82de2884e4243fdca272f3315f16f5a Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 18 Jul 2021 12:30:15 -0400 Subject: [PATCH 10/12] Consolidate connect logic into one file --- src/components/Provider.tsx | 4 +- src/components/connectAdvanced.tsx | 327 +++++++++++++++++++++++++++- src/connect/connect.ts | 338 ----------------------------- src/exports.ts | 6 +- 4 files changed, 329 insertions(+), 346 deletions(-) delete mode 100644 src/connect/connect.ts diff --git a/src/components/Provider.tsx b/src/components/Provider.tsx index 90a04b83e..a5ce101b7 100644 --- a/src/components/Provider.tsx +++ b/src/components/Provider.tsx @@ -12,8 +12,8 @@ export interface ProviderProps { store: Store /** * Optional context to be used internally in react-redux. Use React.createContext() to create a context to be used. - * If this is used, generate own connect HOC by using connectAdvanced, supplying the same context provided to the - * Provider. Initial value doesn't matter, as it is overwritten with the internal state of Provider. + * If this is used, you'll need to customize `connect` by supplying the same context provided to the Provider. + * Initial value doesn't matter, as it is overwritten with the internal state of Provider. */ context?: Context children: ReactNode diff --git a/src/components/connectAdvanced.tsx b/src/components/connectAdvanced.tsx index 0290a6699..1d4d3e8ae 100644 --- a/src/components/connectAdvanced.tsx +++ b/src/components/connectAdvanced.tsx @@ -1,11 +1,33 @@ +/* eslint-disable valid-jsdoc, @typescript-eslint/no-unused-vars */ import hoistStatics from 'hoist-non-react-statics' import React, { useContext, useMemo, useRef, useReducer } from 'react' import { isValidElementType, isContextConsumer } from 'react-is' -import type { Store } from 'redux' -import type { SelectorFactory } from '../connect/selectorFactory' +import type { Store, Dispatch, Action, AnyAction } from 'redux' + +import type { + AdvancedComponentDecorator, + ConnectedComponent, + DefaultRootState, + InferableComponentEnhancer, + InferableComponentEnhancerWithProps, + ResolveThunks, + DispatchProp, +} from '../types' + +import defaultSelectorFactory, { + MapStateToPropsParam, + MapDispatchToPropsParam, + MergeProps, + MapDispatchToPropsNonObject, + SelectorFactory, +} from '../connect/selectorFactory' +import defaultMapDispatchToPropsFactories from '../connect/mapDispatchToProps' +import defaultMapStateToPropsFactories from '../connect/mapStateToProps' +import defaultMergePropsFactories from '../connect/mergeProps' + import { createSubscription, Subscription } from '../utils/Subscription' import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect' -import type { AdvancedComponentDecorator, ConnectedComponent } from '../types' +import shallowEqual from '../utils/shallowEqual' import { ReactReduxContext, @@ -502,4 +524,301 @@ function connectAdvanced( return wrapWithConnect } -export default connectAdvanced +function match( + arg: unknown, + factories: ((value: unknown) => T)[], + name: string +): T { + for (let i = factories.length - 1; i >= 0; i--) { + const result = factories[i](arg) + if (result) return result + } + + return ((dispatch: Dispatch, options: { wrappedComponentName: string }) => { + throw new Error( + `Invalid value of type ${typeof arg} for ${name} argument when connecting component ${ + options.wrappedComponentName + }.` + ) + }) as any +} + +function strictEqual(a: unknown, b: unknown) { + return a === b +} + +/** + * Infers the type of props that a connector will inject into a component. + */ +export type ConnectedProps = + TConnector extends InferableComponentEnhancerWithProps< + infer TInjectedProps, + any + > + ? unknown extends TInjectedProps + ? TConnector extends InferableComponentEnhancer + ? TInjectedProps + : never + : TInjectedProps + : never + +export interface ConnectOptions< + State = DefaultRootState, + TStateProps = {}, + TOwnProps = {}, + TMergedProps = {} +> extends ConnectAdvancedOptions { + pure?: boolean + areStatesEqual?: (nextState: State, prevState: State) => boolean + + areOwnPropsEqual?: ( + nextOwnProps: TOwnProps, + prevOwnProps: TOwnProps + ) => boolean + + areStatePropsEqual?: ( + nextStateProps: TStateProps, + prevStateProps: TStateProps + ) => boolean + areMergedPropsEqual?: ( + nextMergedProps: TMergedProps, + prevMergedProps: TMergedProps + ) => boolean + forwardRef?: boolean +} + +/* @public */ +function connect(): InferableComponentEnhancer + +/* @public */ +function connect< + TStateProps = {}, + no_dispatch = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam +): InferableComponentEnhancerWithProps + +/* @public */ +function connect( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsNonObject +): InferableComponentEnhancerWithProps + +/* @public */ +function connect( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam +): InferableComponentEnhancerWithProps, TOwnProps> + +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsNonObject +): InferableComponentEnhancerWithProps + +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam +): InferableComponentEnhancerWithProps< + TStateProps & ResolveThunks, + TOwnProps +> + +/* @public */ +function connect< + no_state = {}, + no_dispatch = {}, + TOwnProps = {}, + TMergedProps = {} +>( + mapStateToProps: null | undefined, + mapDispatchToProps: null | undefined, + mergeProps: MergeProps +): InferableComponentEnhancerWithProps + +/* @public */ +function connect< + TStateProps = {}, + no_dispatch = {}, + TOwnProps = {}, + TMergedProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: null | undefined, + mergeProps: MergeProps +): InferableComponentEnhancerWithProps + +/* @public */ +function connect< + no_state = {}, + TDispatchProps = {}, + TOwnProps = {}, + TMergedProps = {} +>( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: MergeProps +): InferableComponentEnhancerWithProps + +/* @public */ +// @ts-ignore +function connect< + TStateProps = {}, + no_dispatch = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: null | undefined, + mergeProps: null | undefined, + options: ConnectOptions +): InferableComponentEnhancerWithProps + +/* @public */ +function connect( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsNonObject, + mergeProps: null | undefined, + options: ConnectOptions<{}, TStateProps, TOwnProps> +): InferableComponentEnhancerWithProps + +/* @public */ +function connect( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: null | undefined, + options: ConnectOptions<{}, TStateProps, TOwnProps> +): InferableComponentEnhancerWithProps, TOwnProps> + +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsNonObject, + mergeProps: null | undefined, + options: ConnectOptions +): InferableComponentEnhancerWithProps + +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: null | undefined, + options: ConnectOptions +): InferableComponentEnhancerWithProps< + TStateProps & ResolveThunks, + TOwnProps +> + +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + TMergedProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: MergeProps, + options?: ConnectOptions +): InferableComponentEnhancerWithProps + +/** + * 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?: unknown, + mapDispatchToProps?: unknown, + mergeProps?: unknown, + { + pure = true, + areStatesEqual = strictEqual, + areOwnPropsEqual = shallowEqual, + areStatePropsEqual = shallowEqual, + areMergedPropsEqual = shallowEqual, + ...extraOptions + }: ConnectOptions = {} +): unknown { + const initMapStateToProps = match( + mapStateToProps, + // @ts-ignore + defaultMapStateToPropsFactories, + 'mapStateToProps' + ) + const initMapDispatchToProps = match( + mapDispatchToProps, + // @ts-ignore + defaultMapDispatchToPropsFactories, + 'mapDispatchToProps' + ) + const initMergeProps = match( + mergeProps, + // @ts-ignore + defaultMergePropsFactories, + 'mergeProps' + ) + + return connectAdvanced( + defaultSelectorFactory as SelectorFactory, + { + // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes + shouldHandleStateChanges: Boolean(mapStateToProps), + + // passed through to selectorFactory + initMapStateToProps, + initMapDispatchToProps, + initMergeProps, + pure, + areStatesEqual, + areOwnPropsEqual, + areStatePropsEqual, + areMergedPropsEqual, + + // any extra options args can override defaults of connect or connectAdvanced + ...extraOptions, + } + ) +} + +export default connect diff --git a/src/connect/connect.ts b/src/connect/connect.ts deleted file mode 100644 index 7c99094e9..000000000 --- a/src/connect/connect.ts +++ /dev/null @@ -1,338 +0,0 @@ -/* eslint-disable valid-jsdoc, @typescript-eslint/no-unused-vars */ -import type { Dispatch, Action, AnyAction } from 'redux' -import connectAdvanced from '../components/connectAdvanced' -import type { ConnectAdvancedOptions } from '../components/connectAdvanced' -import shallowEqual from '../utils/shallowEqual' -import defaultMapDispatchToPropsFactories from './mapDispatchToProps' -import defaultMapStateToPropsFactories from './mapStateToProps' -import defaultMergePropsFactories from './mergeProps' -import defaultSelectorFactory, { - MapStateToPropsParam, - MapDispatchToPropsParam, - MergeProps, - MapDispatchToPropsNonObject, - SelectorFactory, -} from './selectorFactory' -import type { - DefaultRootState, - InferableComponentEnhancer, - InferableComponentEnhancerWithProps, - ResolveThunks, - DispatchProp, -} from '../types' - -/* - connect is a facade over connectAdvanced. It turns its args into a compatible - selectorFactory, which has the signature: - - (dispatch, options) => (nextState, nextOwnProps) => nextFinalProps - - connect passes its args to connectAdvanced as options, which will in turn pass them to - selectorFactory each time a Connect component instance is instantiated or hot reloaded. - - selectorFactory returns a final props selector from its mapStateToProps, - mapStateToPropsFactories, mapDispatchToProps, mapDispatchToPropsFactories, mergeProps, - mergePropsFactories, and pure args. - - The resulting final props selector is called by the Connect component instance whenever - it receives new props or store state. - */ - -function match( - arg: unknown, - factories: ((value: unknown) => T)[], - name: string -): T { - for (let i = factories.length - 1; i >= 0; i--) { - const result = factories[i](arg) - if (result) return result - } - - return ((dispatch: Dispatch, options: { wrappedComponentName: string }) => { - throw new Error( - `Invalid value of type ${typeof arg} for ${name} argument when connecting component ${ - options.wrappedComponentName - }.` - ) - }) as any -} - -function strictEqual(a: unknown, b: unknown) { - return a === b -} - -/** - * Infers the type of props that a connector will inject into a component. - */ -export type ConnectedProps = - TConnector extends InferableComponentEnhancerWithProps< - infer TInjectedProps, - any - > - ? unknown extends TInjectedProps - ? TConnector extends InferableComponentEnhancer - ? TInjectedProps - : never - : TInjectedProps - : never - -export interface ConnectOptions< - State = DefaultRootState, - TStateProps = {}, - TOwnProps = {}, - TMergedProps = {} -> extends ConnectAdvancedOptions { - pure?: boolean - areStatesEqual?: (nextState: State, prevState: State) => boolean - - areOwnPropsEqual?: ( - nextOwnProps: TOwnProps, - prevOwnProps: TOwnProps - ) => boolean - - areStatePropsEqual?: ( - nextStateProps: TStateProps, - prevStateProps: TStateProps - ) => boolean - areMergedPropsEqual?: ( - nextMergedProps: TMergedProps, - prevMergedProps: TMergedProps - ) => boolean - forwardRef?: boolean -} - -/* @public */ -function connect(): InferableComponentEnhancer - -/* @public */ -function connect< - TStateProps = {}, - no_dispatch = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam -): InferableComponentEnhancerWithProps - -/* @public */ -function connect( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsNonObject -): InferableComponentEnhancerWithProps - -/* @public */ -function connect( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsParam -): InferableComponentEnhancerWithProps, TOwnProps> - -/* @public */ -function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsNonObject -): InferableComponentEnhancerWithProps - -/* @public */ -function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsParam -): InferableComponentEnhancerWithProps< - TStateProps & ResolveThunks, - TOwnProps -> - -/* @public */ -function connect< - no_state = {}, - no_dispatch = {}, - TOwnProps = {}, - TMergedProps = {} ->( - mapStateToProps: null | undefined, - mapDispatchToProps: null | undefined, - mergeProps: MergeProps -): InferableComponentEnhancerWithProps - -/* @public */ -function connect< - TStateProps = {}, - no_dispatch = {}, - TOwnProps = {}, - TMergedProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: null | undefined, - mergeProps: MergeProps -): InferableComponentEnhancerWithProps - -/* @public */ -function connect< - no_state = {}, - TDispatchProps = {}, - TOwnProps = {}, - TMergedProps = {} ->( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: MergeProps -): InferableComponentEnhancerWithProps - -/* @public */ -// @ts-ignore -function connect< - TStateProps = {}, - no_dispatch = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: null | undefined, - mergeProps: null | undefined, - options: ConnectOptions -): InferableComponentEnhancerWithProps - -/* @public */ -function connect( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsNonObject, - mergeProps: null | undefined, - options: ConnectOptions<{}, TStateProps, TOwnProps> -): InferableComponentEnhancerWithProps - -/* @public */ -function connect( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: null | undefined, - options: ConnectOptions<{}, TStateProps, TOwnProps> -): InferableComponentEnhancerWithProps, TOwnProps> - -/* @public */ -function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsNonObject, - mergeProps: null | undefined, - options: ConnectOptions -): InferableComponentEnhancerWithProps - -/* @public */ -function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: null | undefined, - options: ConnectOptions -): InferableComponentEnhancerWithProps< - TStateProps & ResolveThunks, - TOwnProps -> - -/* @public */ -function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - TMergedProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: MergeProps, - options?: ConnectOptions -): InferableComponentEnhancerWithProps - -/** - * 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?: unknown, - mapDispatchToProps?: unknown, - mergeProps?: unknown, - { - pure = true, - areStatesEqual = strictEqual, - areOwnPropsEqual = shallowEqual, - areStatePropsEqual = shallowEqual, - areMergedPropsEqual = shallowEqual, - ...extraOptions - }: ConnectOptions = {} -): unknown { - const initMapStateToProps = match( - mapStateToProps, - // @ts-ignore - defaultMapStateToPropsFactories, - 'mapStateToProps' - ) - const initMapDispatchToProps = match( - mapDispatchToProps, - // @ts-ignore - defaultMapDispatchToPropsFactories, - 'mapDispatchToProps' - ) - const initMergeProps = match( - mergeProps, - // @ts-ignore - defaultMergePropsFactories, - 'mergeProps' - ) - - return connectAdvanced( - defaultSelectorFactory as SelectorFactory, - { - // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes - shouldHandleStateChanges: Boolean(mapStateToProps), - - // passed through to selectorFactory - initMapStateToProps, - initMapDispatchToProps, - initMergeProps, - pure, - areStatesEqual, - areOwnPropsEqual, - areStatePropsEqual, - areMergedPropsEqual, - - // any extra options args can override defaults of connect or connectAdvanced - ...extraOptions, - } - ) -} - -export default connect diff --git a/src/exports.ts b/src/exports.ts index 494f2ab55..6f48c8af7 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -1,6 +1,9 @@ import Provider from './components/Provider' import type { ProviderProps } from './components/Provider' -import type { ConnectProps } from './components/connectAdvanced' +import connect, { + ConnectProps, + ConnectedProps, +} from './components/connectAdvanced' import type { SelectorFactory, Selector, @@ -16,7 +19,6 @@ import type { } from './connect/selectorFactory' import { ReactReduxContext } from './components/Context' import type { ReactReduxContextValue } from './components/Context' -import connect, { ConnectedProps } from './connect/connect' import { useDispatch, createDispatchHook } from './hooks/useDispatch' import { useSelector, createSelectorHook } from './hooks/useSelector' From 75b4bddbad446ac000007a68ed8948a5cf200567 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 18 Jul 2021 12:31:11 -0400 Subject: [PATCH 11/12] Rename connectAdvanced to connect --- src/components/{connectAdvanced.tsx => connect.tsx} | 0 src/exports.ts | 5 +---- 2 files changed, 1 insertion(+), 4 deletions(-) rename src/components/{connectAdvanced.tsx => connect.tsx} (100%) diff --git a/src/components/connectAdvanced.tsx b/src/components/connect.tsx similarity index 100% rename from src/components/connectAdvanced.tsx rename to src/components/connect.tsx diff --git a/src/exports.ts b/src/exports.ts index 6f48c8af7..21deac22e 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -1,9 +1,6 @@ import Provider from './components/Provider' import type { ProviderProps } from './components/Provider' -import connect, { - ConnectProps, - ConnectedProps, -} from './components/connectAdvanced' +import connect, { ConnectProps, ConnectedProps } from './components/connect' import type { SelectorFactory, Selector, From 532f696d49e98efc6d3c2b73fbb592b8f0a74f67 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 18 Jul 2021 13:01:48 -0400 Subject: [PATCH 12/12] Consolidate connect implementation --- src/components/connect.tsx | 807 ++++++++++++++++----------------- src/connect/selectorFactory.ts | 3 +- 2 files changed, 384 insertions(+), 426 deletions(-) diff --git a/src/components/connect.tsx b/src/components/connect.tsx index 1d4d3e8ae..d018fc0ed 100644 --- a/src/components/connect.tsx +++ b/src/components/connect.tsx @@ -19,7 +19,7 @@ import defaultSelectorFactory, { MapDispatchToPropsParam, MergeProps, MapDispatchToPropsNonObject, - SelectorFactory, + SelectorFactoryOptions, } from '../connect/selectorFactory' import defaultMapDispatchToPropsFactories from '../connect/mapDispatchToProps' import defaultMapStateToPropsFactories from '../connect/mapStateToProps' @@ -205,152 +205,406 @@ export interface ConnectProps { store?: Store } -export interface ConnectAdvancedOptions { - shouldHandleStateChanges?: boolean - forwardRef?: boolean - context?: typeof ReactReduxContext - pure?: boolean -} - -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: SelectorFactory, - // options object: - { - // determines whether this HOC subscribes to store changes - shouldHandleStateChanges = true, +function match( + arg: unknown, + factories: ((value: unknown) => T)[], + name: string +): T { + for (let i = factories.length - 1; i >= 0; i--) { + const result = factories[i](arg) + if (result) return result + } - // use React's forwardRef to expose a ref of the wrapped component - forwardRef = false, + return ((dispatch: Dispatch, options: { wrappedComponentName: string }) => { + throw new Error( + `Invalid value of type ${typeof arg} for ${name} argument when connecting component ${ + options.wrappedComponentName + }.` + ) + }) as any +} - // the context consumer to use - context = ReactReduxContext, +function strictEqual(a: unknown, b: unknown) { + return a === b +} - // additional options are passed through to the selectorFactory - ...connectOptions - }: ConnectAdvancedOptions & Partial = {} -) { - const Context = context +/** + * Infers the type of props that a connector will inject into a component. + */ +export type ConnectedProps = + TConnector extends InferableComponentEnhancerWithProps< + infer TInjectedProps, + any + > + ? unknown extends TInjectedProps + ? TConnector extends InferableComponentEnhancer + ? TInjectedProps + : never + : TInjectedProps + : never - type WrappedComponentProps = TOwnProps & ConnectProps +export interface ConnectOptions< + State = DefaultRootState, + TStateProps = {}, + TOwnProps = {}, + TMergedProps = {} +> { + forwardRef?: boolean + context?: typeof ReactReduxContext + pure?: boolean + areStatesEqual?: (nextState: State, prevState: State) => boolean - const wrapWithConnect: AdvancedComponentDecorator< - TProps, - WrappedComponentProps - > = (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 - )}` - ) - } + areOwnPropsEqual?: ( + nextOwnProps: TOwnProps, + prevOwnProps: TOwnProps + ) => boolean - const wrappedComponentName = - WrappedComponent.displayName || WrappedComponent.name || 'Component' + areStatePropsEqual?: ( + nextStateProps: TStateProps, + prevStateProps: TStateProps + ) => boolean + areMergedPropsEqual?: ( + nextMergedProps: TMergedProps, + prevMergedProps: TMergedProps + ) => boolean +} - const displayName = `Connect(${wrappedComponentName})` +/* @public */ +function connect(): InferableComponentEnhancer - const selectorFactoryOptions = { - ...connectOptions, - shouldHandleStateChanges, - displayName, - wrappedComponentName, - WrappedComponent, - } +/* @public */ +function connect< + TStateProps = {}, + no_dispatch = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam +): InferableComponentEnhancerWithProps - const { pure } = connectOptions +/* @public */ +function connect( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsNonObject +): InferableComponentEnhancerWithProps - // 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. - const usePureOnlyMemo = pure - ? useMemo - : (callback: () => void) => callback() +/* @public */ +function connect( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam +): InferableComponentEnhancerWithProps, TOwnProps> - function ConnectFunction(props: ConnectProps & TOwnProps) { - 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, ...wrapperProps } = props - return [props.context, reactReduxForwardedRef, wrapperProps] - }, [props]) +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsNonObject +): InferableComponentEnhancerWithProps - const ContextToUse: ReactReduxContextInstance = 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() - ? propsContext - : Context - }, [propsContext, Context]) +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam +): InferableComponentEnhancerWithProps< + TStateProps & ResolveThunks, + TOwnProps +> - // Retrieve the store and ancestor subscription via context, if available - const contextValue = useContext(ContextToUse) +/* @public */ +function connect< + no_state = {}, + no_dispatch = {}, + TOwnProps = {}, + TMergedProps = {} +>( + mapStateToProps: null | undefined, + mapDispatchToProps: null | undefined, + mergeProps: MergeProps +): InferableComponentEnhancerWithProps - // 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) +/* @public */ +function connect< + TStateProps = {}, + no_dispatch = {}, + TOwnProps = {}, + TMergedProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: null | undefined, + mergeProps: MergeProps +): InferableComponentEnhancerWithProps - 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 , ` + - `or pass a custom React context provider to and the corresponding ` + - `React context consumer to ${displayName} in connect options.` - ) - } +/* @public */ +function connect< + no_state = {}, + TDispatchProps = {}, + TOwnProps = {}, + TMergedProps = {} +>( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: MergeProps +): InferableComponentEnhancerWithProps - // Based on the previous check, one of these must be true - const store: Store = didStoreComeFromProps - ? props.store! - : contextValue!.store +/* @public */ +// @ts-ignore +function connect< + TStateProps = {}, + no_dispatch = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: null | undefined, + mergeProps: null | undefined, + options: ConnectOptions +): InferableComponentEnhancerWithProps - const childPropsSelector = useMemo(() => { - // The child props selector needs the store reference as an input. - // Re-create this selector whenever the store changes. - return selectorFactory(store.dispatch, selectorFactoryOptions) - }, [store]) +/* @public */ +function connect( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsNonObject, + mergeProps: null | undefined, + options: ConnectOptions<{}, TStateProps, TOwnProps> +): InferableComponentEnhancerWithProps - const [subscription, notifyNestedSubs] = useMemo(() => { - if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY +/* @public */ +function connect( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: null | undefined, + options: ConnectOptions<{}, TStateProps, TOwnProps> +): InferableComponentEnhancerWithProps, TOwnProps> - // 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( +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsNonObject, + mergeProps: null | undefined, + options: ConnectOptions +): InferableComponentEnhancerWithProps + +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: null | undefined, + options: ConnectOptions +): InferableComponentEnhancerWithProps< + TStateProps & ResolveThunks, + TOwnProps +> + +/* @public */ +function connect< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + TMergedProps = {}, + State = DefaultRootState +>( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: MergeProps, + options?: ConnectOptions +): InferableComponentEnhancerWithProps + +/** + * 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< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + TMergedProps = {}, + State = DefaultRootState +>( + mapStateToProps?: MapStateToPropsParam, + mapDispatchToProps?: MapDispatchToPropsParam, + mergeProps?: MergeProps, + { + pure = true, + 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, + }: ConnectOptions = {} +): unknown { + const Context = context + + type WrappedComponentProps = TOwnProps & ConnectProps + + const initMapStateToProps = match( + mapStateToProps, + // @ts-ignore + defaultMapStateToPropsFactories, + 'mapStateToProps' + )! + const initMapDispatchToProps = match( + mapDispatchToProps, + // @ts-ignore + defaultMapDispatchToPropsFactories, + 'mapDispatchToProps' + )! + const initMergeProps = match( + mergeProps, + // @ts-ignore + defaultMergePropsFactories, + 'mergeProps' + )! + + const shouldHandleStateChanges = Boolean(mapStateToProps) + + const wrapWithConnect: AdvancedComponentDecorator< + TOwnProps, + WrappedComponentProps + > = (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: SelectorFactoryOptions = { + pure, + shouldHandleStateChanges, + displayName, + wrappedComponentName, + WrappedComponent, + initMapStateToProps, + initMapDispatchToProps, + // @ts-ignore + initMergeProps, + areStatesEqual, + areStatePropsEqual, + areOwnPropsEqual, + areMergedPropsEqual, + } + + // 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. + const usePureOnlyMemo = pure + ? useMemo + : (callback: () => void) => callback() + + function ConnectFunction(props: ConnectProps & TOwnProps) { + 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, ...wrapperProps } = props + return [props.context, reactReduxForwardedRef, wrapperProps] + }, [props]) + + const ContextToUse: ReactReduxContextInstance = 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() + ? 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 , ` + + `or pass a custom React context provider to and the corresponding ` + + `React context consumer to ${displayName} in connect options.` + ) + } + + // Based on the previous check, one of these must be true + const store: Store = didStoreComeFromProps + ? props.store! + : contextValue!.store + + 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 ) @@ -524,301 +778,4 @@ function connectAdvanced( return wrapWithConnect } -function match( - arg: unknown, - factories: ((value: unknown) => T)[], - name: string -): T { - for (let i = factories.length - 1; i >= 0; i--) { - const result = factories[i](arg) - if (result) return result - } - - return ((dispatch: Dispatch, options: { wrappedComponentName: string }) => { - throw new Error( - `Invalid value of type ${typeof arg} for ${name} argument when connecting component ${ - options.wrappedComponentName - }.` - ) - }) as any -} - -function strictEqual(a: unknown, b: unknown) { - return a === b -} - -/** - * Infers the type of props that a connector will inject into a component. - */ -export type ConnectedProps = - TConnector extends InferableComponentEnhancerWithProps< - infer TInjectedProps, - any - > - ? unknown extends TInjectedProps - ? TConnector extends InferableComponentEnhancer - ? TInjectedProps - : never - : TInjectedProps - : never - -export interface ConnectOptions< - State = DefaultRootState, - TStateProps = {}, - TOwnProps = {}, - TMergedProps = {} -> extends ConnectAdvancedOptions { - pure?: boolean - areStatesEqual?: (nextState: State, prevState: State) => boolean - - areOwnPropsEqual?: ( - nextOwnProps: TOwnProps, - prevOwnProps: TOwnProps - ) => boolean - - areStatePropsEqual?: ( - nextStateProps: TStateProps, - prevStateProps: TStateProps - ) => boolean - areMergedPropsEqual?: ( - nextMergedProps: TMergedProps, - prevMergedProps: TMergedProps - ) => boolean - forwardRef?: boolean -} - -/* @public */ -function connect(): InferableComponentEnhancer - -/* @public */ -function connect< - TStateProps = {}, - no_dispatch = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam -): InferableComponentEnhancerWithProps - -/* @public */ -function connect( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsNonObject -): InferableComponentEnhancerWithProps - -/* @public */ -function connect( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsParam -): InferableComponentEnhancerWithProps, TOwnProps> - -/* @public */ -function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsNonObject -): InferableComponentEnhancerWithProps - -/* @public */ -function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsParam -): InferableComponentEnhancerWithProps< - TStateProps & ResolveThunks, - TOwnProps -> - -/* @public */ -function connect< - no_state = {}, - no_dispatch = {}, - TOwnProps = {}, - TMergedProps = {} ->( - mapStateToProps: null | undefined, - mapDispatchToProps: null | undefined, - mergeProps: MergeProps -): InferableComponentEnhancerWithProps - -/* @public */ -function connect< - TStateProps = {}, - no_dispatch = {}, - TOwnProps = {}, - TMergedProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: null | undefined, - mergeProps: MergeProps -): InferableComponentEnhancerWithProps - -/* @public */ -function connect< - no_state = {}, - TDispatchProps = {}, - TOwnProps = {}, - TMergedProps = {} ->( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: MergeProps -): InferableComponentEnhancerWithProps - -/* @public */ -// @ts-ignore -function connect< - TStateProps = {}, - no_dispatch = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: null | undefined, - mergeProps: null | undefined, - options: ConnectOptions -): InferableComponentEnhancerWithProps - -/* @public */ -function connect( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsNonObject, - mergeProps: null | undefined, - options: ConnectOptions<{}, TStateProps, TOwnProps> -): InferableComponentEnhancerWithProps - -/* @public */ -function connect( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: null | undefined, - options: ConnectOptions<{}, TStateProps, TOwnProps> -): InferableComponentEnhancerWithProps, TOwnProps> - -/* @public */ -function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsNonObject, - mergeProps: null | undefined, - options: ConnectOptions -): InferableComponentEnhancerWithProps - -/* @public */ -function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: null | undefined, - options: ConnectOptions -): InferableComponentEnhancerWithProps< - TStateProps & ResolveThunks, - TOwnProps -> - -/* @public */ -function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - TMergedProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: MergeProps, - options?: ConnectOptions -): InferableComponentEnhancerWithProps - -/** - * 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?: unknown, - mapDispatchToProps?: unknown, - mergeProps?: unknown, - { - pure = true, - areStatesEqual = strictEqual, - areOwnPropsEqual = shallowEqual, - areStatePropsEqual = shallowEqual, - areMergedPropsEqual = shallowEqual, - ...extraOptions - }: ConnectOptions = {} -): unknown { - const initMapStateToProps = match( - mapStateToProps, - // @ts-ignore - defaultMapStateToPropsFactories, - 'mapStateToProps' - ) - const initMapDispatchToProps = match( - mapDispatchToProps, - // @ts-ignore - defaultMapDispatchToPropsFactories, - 'mapDispatchToProps' - ) - const initMergeProps = match( - mergeProps, - // @ts-ignore - defaultMergePropsFactories, - 'mergeProps' - ) - - return connectAdvanced( - defaultSelectorFactory as SelectorFactory, - { - // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes - shouldHandleStateChanges: Boolean(mapStateToProps), - - // passed through to selectorFactory - initMapStateToProps, - initMapDispatchToProps, - initMergeProps, - pure, - areStatesEqual, - areOwnPropsEqual, - areStatePropsEqual, - areMergedPropsEqual, - - // any extra options args can override defaults of connect or connectAdvanced - ...extraOptions, - } - ) -} - export default connect diff --git a/src/connect/selectorFactory.ts b/src/connect/selectorFactory.ts index 5c9f5a3d7..e76ae50a2 100644 --- a/src/connect/selectorFactory.ts +++ b/src/connect/selectorFactory.ts @@ -96,6 +96,7 @@ interface PureSelectorFactoryComparisonOptions< areStatesEqual: EqualityFn areOwnPropsEqual: EqualityFn areStatePropsEqual: EqualityFn + displayName: string pure?: boolean } @@ -222,7 +223,7 @@ export interface SelectorFactoryOptions< // TODO: Add more comments // If pure is true, the selector returned by selectorFactory will memoize its results, -// allowing connectAdvanced's shouldComponentUpdate to return false if final +// allowing connect's shouldComponentUpdate to return false if final // props have not changed. If false, the selector will always return a new // object and shouldComponentUpdate will always return true.