import hoistStatics from 'hoist-non-react-statics' ; import invariant from 'invariant' ; import type { FunctionComponent } from 'react' ; import React , { useMemo , useRef , useReducer , Component } from 'react' ; import { isValidElementType } from 'react-is' ; import { Subscription } from './Subscription' ; import { useConsumerContext , useIsomorphicLayoutEffect } from './hooks' ; import type { ConsumerContext } from './types' ; const EMPTY_ARRAY = [ ] ; const stringifyComponent = ( Comp ) => { try { return JSON . stringify ( Comp ) ; } catch ( err ) { return String ( Comp ) ; } } ; function storeStateUpdatesReducer ( state , action ) { const [ , updateCount ] = state ; return [ action . payload , updateCount + 1 ] ; } const initStateUpdates = ( ) => [ null , 0 ] ; export function connectAdvanced ( selectorFactory , { getDisplayName = ( name ) => `ConnectAdvanced(${ name })` , methodName = 'connectAdvanced' , stores = [ ] , shouldHandleStateChanges = true , forwardRef = false , schedule = ( fn ) => fn ( ) , pure = true , ... connectOptions } = { } ) { return function wrapWithConnect < T extends React . ComponentType > ( WrappedComponent ) { if ( process . env . NODE_ENV !== 'production' ) { invariant ( isValidElementType ( WrappedComponent ) , `You must pass a component to the function returned by ` + `${ methodName }. Instead received ${ stringifyComponent ( WrappedComponent ) }` ) ; } const wrappedComponentName = ( WrappedComponent as any ) . displayName || ( WrappedComponent as any ) . name || 'Component' ; const displayName = getDisplayName ( wrappedComponentName ) ; const selectorFactoryOptions = { ... connectOptions , pure , getDisplayName , methodName , shouldHandleStateChanges , displayName , wrappedComponentName , WrappedComponent , } ; const usePureOnlyMemo = pure ? useMemo : ( callback ) => callback ( ) ; const ConnectFunction = ( props ) => { const [ forwardedRef , wrapperProps ] = useMemo ( ( ) => { const { forwardedRef , ... wrapperProps } = props ; return [ forwardedRef , wrapperProps ] ; } , [ props ] ) ; const contextValue = useConsumerContext ( ) ; invariant ( Boolean ( contextValue ) , `Could not find context in ` + `"${ displayName }". Either wrap the root component in a <Provider>, ` + `or pass a custom React context provider to <Provider> and the corresponding ` + `React context consumer to ${ displayName } in connect options.` ) ; const childPropsSelector = useMemo ( ( ) => { return selectorFactory ( contextValue , selectorFactoryOptions ) ; } , [ contextValue ] ) ; const subscription = useMemo ( ( ) => { if ( ! shouldHandleStateChanges ) return null ; return new Subscription ( stores . map ( ( store ) => contextValue . getStore ( store ) ) ) ; } , [ contextValue ] ) ; useMemo ( ( ) => { if ( subscription && typeof window !== 'undefined' ) { subscription . trySubscribe ( ) ; } } , [ subscription ] ) ; const [ [ previousStateUpdateResult ] , forceComponentUpdateDispatch ] = useReducer ( storeStateUpdatesReducer , EMPTY_ARRAY , initStateUpdates ) ; if ( previousStateUpdateResult && previousStateUpdateResult . error ) { throw previousStateUpdateResult . error ; } const lastChildProps = useRef ( ) ; const lastWrapperProps = useRef ( wrapperProps ) ; const childPropsFromStoreUpdate = useRef ( ) ; const renderIsScheduled = useRef ( false ) ; const actualChildProps = usePureOnlyMemo ( ( ) => { if ( childPropsFromStoreUpdate . current && wrapperProps === lastWrapperProps . current ) { return childPropsFromStoreUpdate . current ; } return childPropsSelector ( contextValue . getState ( ) , wrapperProps ) ; } , [ contextValue , previousStateUpdateResult , wrapperProps ] ) ; useIsomorphicLayoutEffect ( ( ) => { lastWrapperProps . current = wrapperProps ; lastChildProps . current = actualChildProps ; if ( childPropsFromStoreUpdate . current ) { childPropsFromStoreUpdate . current = undefined ; } } ) ; useIsomorphicLayoutEffect ( ( ) => { if ( ! subscription ) return ; let didUnsubscribe = false ; let lastThrownError = null ; const checkForUpdates = ( ) => { renderIsScheduled . current = false ; if ( didUnsubscribe ) { return ; } const latestStoreState = contextValue . getState ( ) ; let newChildProps ; let error ; try { newChildProps = childPropsSelector ( latestStoreState , lastWrapperProps . current ) ; } catch ( e ) { error = e ; lastThrownError = e ; } if ( ! error ) { lastThrownError = null ; } if ( newChildProps !== lastChildProps . current ) { lastChildProps . current = newChildProps ; childPropsFromStoreUpdate . current = newChildProps ; forceComponentUpdateDispatch ( { type : 'STORE_UPDATED' , payload : { latestStoreState , error , } , } ) ; } } ; subscription . setOnStateChange ( ( ) => { if ( ! renderIsScheduled . current ) { renderIsScheduled . current = true ; schedule ( checkForUpdates ) ; } } ) ; checkForUpdates ( ) ; const unsubscribeWrapper = ( ) => { didUnsubscribe = true ; subscription . tryUnsubscribe ( ) ; if ( lastThrownError ) { throw lastThrownError ; } } ; return unsubscribeWrapper ; } , [ contextValue , subscription , childPropsSelector ] ) ; const renderedWrappedComponent = useMemo ( ( ) => < WrappedComponent { ... actualChildProps } ref = { forwardedRef } / > , [ forwardedRef , actualChildProps ] ) ; return renderedWrappedComponent ; } ; const Connect = pure ? React . memo ( ConnectFunction ) : ConnectFunction ; ( Connect as any ) . WrappedComponent = WrappedComponent ; Connect . displayName = displayName ; if ( forwardRef ) { const forwarded = React . forwardRef ( function forwardConnectRef ( props , ref ) { return < Connect { ... props } forwardedRef = { ref } / > ; } ) ; forwarded . displayName = displayName ; ( forwarded as any ) . WrappedComponent = WrappedComponent ; return ( hoistStatics ( forwarded , WrappedComponent ) as any ) as T ; } return hoistStatics ( Connect , WrappedComponent ) as T ; } ; }