diff --git a/src/components/Provider.tsx b/src/components/Provider.tsx index ffe9197fa..cb22b55bf 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/connect.tsx similarity index 60% rename from src/components/connectAdvanced.tsx rename to src/components/connect.tsx index 77e433814..d018fc0ed 100644 --- a/src/components/connectAdvanced.tsx +++ b/src/components/connect.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, + SelectorFactoryOptions, +} 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, @@ -17,6 +39,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 +49,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 +64,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,12 +76,13 @@ 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, renderIsScheduled: React.MutableRefObject, - wrapperProps: React.MutableRefObject, - actualChildProps: React.MutableRefObject, + wrapperProps: unknown, + actualChildProps: unknown, childPropsFromStoreUpdate: React.MutableRefObject, notifyNestedSubs: () => void ) { @@ -64,6 +98,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 +196,7 @@ function subscribeUpdates( return unsubscribeWrapper } +// Reducer initial state creation for our update reducer const initStateUpdates = () => EMPTY_ARRAY export interface ConnectProps { @@ -168,70 +205,301 @@ export interface ConnectProps { store?: Store } -export interface ConnectAdvancedOptions { - getDisplayName?: (name: string) => string - methodName?: string - shouldHandleStateChanges?: boolean +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 = {} +> { forwardRef?: boolean context?: typeof ReactReduxContext 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 } -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: +/* @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< + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + TMergedProps = {}, + State = DefaultRootState +>( + mapStateToProps?: MapStateToPropsParam, + mapDispatchToProps?: MapDispatchToPropsParam, + mergeProps?: MergeProps, { - // 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})`, - - // shown in error messages - // probably overridden by wrapper functions such as connect() - methodName = 'connectAdvanced', - - // determines whether this HOC subscribes to store changes - shouldHandleStateChanges = true, + 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, - - // additional options are passed through to the selectorFactory - ...connectOptions - }: ConnectAdvancedOptions & Partial = {} -) { + }: ConnectOptions = {} +): unknown { const Context = context type WrappedComponentProps = TOwnProps & ConnectProps - /* - return function wrapWithConnect< - WC extends React.ComponentType< - Matching, GetProps> - > - >(WrappedComponent: WC) { - */ + 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< - TProps, + TOwnProps, WrappedComponentProps > = (WrappedComponent) => { if ( @@ -239,32 +507,31 @@ 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 + )}` ) } const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component' - const displayName = getDisplayName(wrappedComponentName) + const displayName = `Connect(${wrappedComponentName})` - const selectorFactoryOptions = { - ...connectOptions, - getDisplayName, - methodName, + const selectorFactoryOptions: SelectorFactoryOptions = { + pure, shouldHandleStateChanges, displayName, wrappedComponentName, WrappedComponent, - } - - const { pure } = connectOptions - - function createChildSelector(store: Store) { - return selectorFactory(store.dispatch, selectorFactoryOptions) + initMapStateToProps, + initMapDispatchToProps, + // @ts-ignore + initMergeProps, + areStatesEqual, + areStatePropsEqual, + areOwnPropsEqual, + areMergedPropsEqual, } // If we aren't running in "pure" mode, we don't want to memoize values. @@ -329,7 +596,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 defaultSelectorFactory(store.dispatch, selectorFactoryOptions) }, [store]) const [subscription, notifyNestedSubs] = useMemo(() => { @@ -511,4 +778,4 @@ function connectAdvanced( return wrapWithConnect } -export default connectAdvanced +export default connect diff --git a/src/connect/connect.ts b/src/connect/connect.ts deleted file mode 100644 index ae95fe28b..000000000 --- a/src/connect/connect.ts +++ /dev/null @@ -1,500 +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 | undefined - areStatesEqual?: ((nextState: State, prevState: State) => boolean) | undefined - - areOwnPropsEqual?: ( - nextOwnProps: TOwnProps, - prevOwnProps: TOwnProps - ) => boolean - - areStatePropsEqual?: ( - nextStateProps: TStateProps, - prevStateProps: TStateProps - ) => boolean - areMergedPropsEqual?: ( - nextMergedProps: TMergedProps, - prevMergedProps: TMergedProps - ) => boolean - 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({ - 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< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState - >( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsNonObject - ): InferableComponentEnhancerWithProps< - TStateProps & TDispatchProps, - TOwnProps - > - - /* @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< - ResolveThunks, - 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: 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< - 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' - ) - - return connectHOC(selectorFactory as SelectorFactory, { - // used in error messages - methodName: 'connect', - - // 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), - - // passed through to selectorFactory - initMapStateToProps, - initMapDispatchToProps, - initMergeProps, - pure, - areStatesEqual, - areOwnPropsEqual, - areStatePropsEqual, - areMergedPropsEqual, - - // any extra options args can override defaults of connect or connectAdvanced - ...extraOptions, - }) - } - - return connect -} - -/* @public */ -const connect = /*#__PURE__*/ createConnect() - -export default connect 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 } diff --git a/src/connect/selectorFactory.ts b/src/connect/selectorFactory.ts index 2a2990a02..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 } @@ -207,29 +208,22 @@ 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 // 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. @@ -259,12 +253,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') } diff --git a/src/exports.ts b/src/exports.ts index 69b0aca1b..94fd09b03 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 connect, { ConnectProps, ConnectedProps } from './components/connect' import type { SelectorFactory, Selector, @@ -20,7 +16,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' @@ -39,7 +34,6 @@ export type { MapStateToPropsParam, ConnectProps, ConnectedProps, - ConnectAdvancedOptions, MapDispatchToPropsFunction, MapDispatchToProps, MapDispatchToPropsFactory, @@ -51,7 +45,6 @@ export type { } export { Provider, - connectAdvanced, ReactReduxContext, connect, useDispatch, diff --git a/test/components/connect.spec.tsx b/test/components/connect.spec.tsx index a63660e99..5b97021f9 100644 --- a/test/components/connect.spec.tsx +++ b/test/components/connect.spec.tsx @@ -1836,19 +1836,6 @@ describe('React', () => { ).toBe('Connect(Component)') }) - it('should allow custom displayName', () => { - class MyComponent extends React.Component { - render() { - return
- } - } - const ConnectedMyComponent = connect(null, null, null, { - getDisplayName: (name) => `Custom(${name})`, - })(MyComponent) - - expect(ConnectedMyComponent.displayName).toEqual('Custom(MyComponent)') - }) - it('should expose the wrapped component as WrappedComponent', () => { class Container extends Component { render() { 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.tsx b/test/integration/server-rendering.spec.tsx index 7bf3975a5..605738be1 100644 --- a/test/integration/server-rendering.spec.tsx +++ b/test/integration/server-rendering.spec.tsx @@ -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. */