import type { Dispatch, Action } from 'redux' import type { ComponentType } from 'react' import verifySubselectors from './verifySubselectors' import type { EqualityFn, ExtendedEqualityFn } from '../types' export type SelectorFactory = ( dispatch: Dispatch>, factoryOptions: TFactoryOptions ) => Selector export type Selector = TOwnProps extends | null | undefined ? (state: S) => TProps : (state: S, ownProps: TOwnProps) => TProps export type MapStateToProps = ( state: State, ownProps: TOwnProps ) => TStateProps export type MapStateToPropsFactory = ( initialState: State, ownProps: TOwnProps ) => MapStateToProps export type MapStateToPropsParam = | MapStateToPropsFactory | MapStateToProps | null | undefined export type MapDispatchToPropsFunction = ( dispatch: Dispatch>, ownProps: TOwnProps ) => TDispatchProps export type MapDispatchToProps = | MapDispatchToPropsFunction | TDispatchProps export type MapDispatchToPropsFactory = ( dispatch: Dispatch>, ownProps: TOwnProps ) => MapDispatchToPropsFunction export type MapDispatchToPropsParam = | MapDispatchToPropsFactory | MapDispatchToProps export type MapDispatchToPropsNonObject = | MapDispatchToPropsFactory | MapDispatchToPropsFunction export type MergeProps = ( stateProps: TStateProps, dispatchProps: TDispatchProps, ownProps: TOwnProps ) => TMergedProps interface PureSelectorFactoryComparisonOptions { readonly areStatesEqual: ExtendedEqualityFn readonly areStatePropsEqual: EqualityFn readonly areOwnPropsEqual: EqualityFn } export function pureFinalPropsSelectorFactory< TStateProps, TOwnProps, TDispatchProps, TMergedProps, State >( mapStateToProps: WrappedMapStateToProps, mapDispatchToProps: WrappedMapDispatchToProps, mergeProps: MergeProps, dispatch: Dispatch>, { areStatesEqual, areOwnPropsEqual, areStatePropsEqual, }: PureSelectorFactoryComparisonOptions ) { let hasRunAtLeastOnce = false let state: State let ownProps: TOwnProps let stateProps: TStateProps let dispatchProps: TDispatchProps let mergedProps: TMergedProps function handleFirstCall(firstState: State, firstOwnProps: TOwnProps) { state = firstState ownProps = firstOwnProps stateProps = mapStateToProps(state, ownProps) dispatchProps = mapDispatchToProps(dispatch, ownProps) mergedProps = mergeProps(stateProps, dispatchProps, ownProps) hasRunAtLeastOnce = true return mergedProps } function handleNewPropsAndNewState() { stateProps = mapStateToProps(state, ownProps) if (mapDispatchToProps.dependsOnOwnProps) dispatchProps = mapDispatchToProps(dispatch, ownProps) mergedProps = mergeProps(stateProps, dispatchProps, ownProps) return mergedProps } function handleNewProps() { if (mapStateToProps.dependsOnOwnProps) stateProps = mapStateToProps(state, ownProps) if (mapDispatchToProps.dependsOnOwnProps) dispatchProps = mapDispatchToProps(dispatch, ownProps) mergedProps = mergeProps(stateProps, dispatchProps, ownProps) return mergedProps } function handleNewState() { const nextStateProps = mapStateToProps(state, ownProps) const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps) stateProps = nextStateProps if (statePropsChanged) mergedProps = mergeProps(stateProps, dispatchProps, ownProps) return mergedProps } function handleSubsequentCalls(nextState: State, nextOwnProps: TOwnProps) { const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps) const stateChanged = !areStatesEqual( nextState, state, nextOwnProps, ownProps ) state = nextState ownProps = nextOwnProps if (propsChanged && stateChanged) return handleNewPropsAndNewState() if (propsChanged) return handleNewProps() if (stateChanged) return handleNewState() return mergedProps } return function pureFinalPropsSelector( nextState: State, nextOwnProps: TOwnProps ) { return hasRunAtLeastOnce ? handleSubsequentCalls(nextState, nextOwnProps) : handleFirstCall(nextState, nextOwnProps) } } interface WrappedMapStateToProps { (state: State, ownProps: TOwnProps): TStateProps readonly dependsOnOwnProps: boolean } interface WrappedMapDispatchToProps { (dispatch: Dispatch>, ownProps: TOwnProps): TDispatchProps readonly dependsOnOwnProps: boolean } export interface InitOptions extends PureSelectorFactoryComparisonOptions { readonly shouldHandleStateChanges: boolean readonly displayName: string readonly wrappedComponentName: string readonly WrappedComponent: ComponentType readonly areMergedPropsEqual: EqualityFn } export interface SelectorFactoryOptions< TStateProps, TOwnProps, TDispatchProps, TMergedProps, State > extends InitOptions { readonly initMapStateToProps: ( dispatch: Dispatch>, options: InitOptions ) => WrappedMapStateToProps readonly initMapDispatchToProps: ( dispatch: Dispatch>, options: InitOptions ) => WrappedMapDispatchToProps readonly initMergeProps: ( dispatch: Dispatch>, options: InitOptions ) => MergeProps } // TODO: Add more comments // The selector returned by selectorFactory will memoize its results, // allowing connect's shouldComponentUpdate to return false if final // props have not changed. export default function finalPropsSelectorFactory< TStateProps, TOwnProps, TDispatchProps, TMergedProps, State >( dispatch: Dispatch>, { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options }: SelectorFactoryOptions< TStateProps, TOwnProps, TDispatchProps, TMergedProps, State > ) { const mapStateToProps = initMapStateToProps(dispatch, options) const mapDispatchToProps = initMapDispatchToProps(dispatch, options) const mergeProps = initMergeProps(dispatch, options) if (process.env.NODE_ENV !== 'production') { verifySubselectors(mapStateToProps, mapDispatchToProps, mergeProps) } return pureFinalPropsSelectorFactory< TStateProps, TOwnProps, TDispatchProps, TMergedProps, State >(mapStateToProps, mapDispatchToProps, mergeProps, dispatch, options) }