react-redux
を読んでみたので、そのメモ。
前回の redux
のソースコードを読んだ記事の続きです。
コード読むのに慣れていないので、間違っている部分もあるかもしれません。あったらご指摘ください。
対象読者
redux
を知っている人。
react-redux の connect
connect 部分が気になったので、そこを読んでみました。
まず、connect は
connect(mapStateToProps, mapDispatchToProps)(コンポーネント名)
のようにして書きますが、この ()()
の部分はクロージャーと呼ばれる書き方になっていて、
const appComponent = connect(mapStateToProps, mapDispatchToProps)
appComponent(コンポーネント名)
と同じ書き方になります。
その中身が、ここのソースコードに書いてあります。
ソースコードから、connect 部分だけを取り出すと、
・・・省略
function connect<
・・・省略 型
>(
mapStateToProps?: MapStateToPropsParam<TStateProps, TOwnProps, State>,
mapDispatchToProps?: MapDispatchToPropsParam<TDispatchProps, TOwnProps>,
mergeProps?: MergeProps<TStateProps, TDispatchProps, TOwnProps, TMergedProps>,
・・・省略
)
・・・省略
const Context = context
・・・省略 (mapStateToProps, mapDispatchToProps の処理)
const shouldHandleStateChanges = Boolean(mapStateToProps)
const wrapWithConnect: AdvancedComponentDecorator<
TOwnProps,
WrappedComponentProps
> = (WrappedComponent) => {
・・・省略
function ConnectFunction<TOwnProps>(
props: InternalConnectProps & TOwnProps
) {
・・・省略
・・・ここから 描画
const renderedWrappedComponent = useMemo(() => {
return (
// @ts-ignore
<WrappedComponent
{...actualChildProps}
ref={reactReduxForwardedRef}
/>
)
}, [reactReduxForwardedRef, WrappedComponent, actualChildProps])
const renderedChild = useMemo(() => {
if (shouldHandleStateChanges) {
return (
<ContextToUse.Provider value={overriddenContextValue}>
{renderedWrappedComponent}
</ContextToUse.Provider>
)
}
return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])
return renderedChild
}
const _Connect = React.memo(ConnectFunction)
・・・省略 型
const Connect = _Connect /* 省略 */
Connect.WrappedComponent = WrappedComponent
・・・省略
return hoistStatics(Connect, WrappedComponent)
}
return wrapWithConnect
}
export default connect as Connect
hoistStatics は オブジェクトのコピーをしているくらい で、connect
の後半の () に wrapWithConnect が入るので、connect 全体では wrapWithConnect の引数の <WrappedComponent>
か wrapWithConnect を children にした <ContextToUse.Provider value=>
のみを返しているのが分かります。
では、
・・・省略 (mapStateToProps, mapDispatchToProps の処理)
の部分を見てみましょう。
ここでは、connect()()
の前半の () から mapStateToProps と mapDispatchToProps を取り出して、以下の関数に当てはめています。
const initMapStateToProps = match(
mapStateToProps,
// @ts-ignore
defaultMapStateToPropsFactories,
'mapStateToProps'
)!
const initMapDispatchToProps = match(
mapDispatchToProps,
// @ts-ignore
defaultMapDispatchToPropsFactories,
'mapDispatchToProps'
)!
const initMergeProps = match(
mergeProps,
// @ts-ignore
defaultMergePropsFactories,
'mergeProps'
)!
match は、
function match<T>(
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
}
のように、factories[i](arg)
の返り値を返しています。
では、factories の中身の defaultMapStateToPropsFactories
と defaultMapDispatchToPropsFactories
はどうなっているんでしょうか?
defaultMapStateToPropsFactories
は src/connect/mapDispatchToProps.ts
に、 defaultMapDispatchToPropsFactories
は src/connect/mapStateToProps.ts
に、export default で定義されているんですが、それぞれで共通して使われているのが src/connect/wrapMapToProps.ts
にある関数 wrapMapToPropsConstant
と wrapMapToPropsFunc
です。
ここでは、wrapMapToPropsFunc
を見てみましょう。
export function wrapMapToPropsFunc<P = AnyProps>(
mapToProps: MapToProps,
methodName: string
) {
return function initProxySelector(
dispatch: Dispatch,
{ displayName }: { displayName: string }
) {
const proxy = function mapToPropsProxy(
stateOrDispatch: StateOrDispatch,
ownProps?: P
): MapToProps {
return proxy.dependsOnOwnProps
? proxy.mapToProps(stateOrDispatch, ownProps)
: proxy.mapToProps(stateOrDispatch, undefined)
}
// allow detectFactoryAndVerify to get ownProps
proxy.dependsOnOwnProps = true
proxy.mapToProps = function detectFactoryAndVerify(
stateOrDispatch: StateOrDispatch,
ownProps?: P
): MapToProps {
proxy.mapToProps = mapToProps
proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
let props = proxy(stateOrDispatch, ownProps)
・・・省略
return props
}
return proxy
}
}
読みづらいですが、
proxy.mapToProps = mapToProps
で、proxy の mapToProps に wrapMapToPropsFunc の引数の mapToProps を代入しているのが分かると思います。
この mapToProps の中に、下のような mapStateToProps と mapDispatchToProps が入ります。
function mapStateToProps(state) {
return {
data: state.hoge
};
}
function mapDispatchToProps(dispatch) {
return {
hoge: () => dispatch({
type: Actions.DISPATCH_HOGE,
hoge: true,
}),
}
};
これで、
const initMapStateToProps = match(・・・省略)
const initMapDispatchToProps = match(・・・省略)
const initMergeProps = match(・・・省略)
に無事に、返り値が入りました。
これらの initMapStateToProps
と initMapDispatchToProps
と initMergeProps
は、selectorFactoryOptions
に代入され、ConnectFunction
内の childPropsSelector
で src/connect/selectorFactory.ts
の finalPropsSelectorFactory
に渡されます。
const childPropsSelector = useMemo(() => {
return defaultSelectorFactory(store.dispatch, selectorFactoryOptions)
}, [store])
export default function finalPropsSelectorFactory
(
dispatch: Dispatch<Action>,
{
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
...options
}
){
const mapStateToProps = initMapStateToProps(dispatch, options)
const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
const mergeProps = initMergeProps(dispatch, options)
・・・省略
return pureFinalPropsSelectorFactory(mapStateToProps!, mapDispatchToProps, mergeProps, dispatch, options)
}
ここの返り値の pureFinalPropsSelectorFactory の中身は下のようになります。
export function pureFinalPropsSelectorFactory(mapStateToProps, mapDispatchToProps, mergeProps, dispatch, {areStatesEqual, areOwnPropsEqual, areStatePropsEqual}) {
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
// @ts-ignore
stateProps = mapStateToProps!(state, ownProps)
// @ts-ignore
dispatchProps = mapDispatchToProps!(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
hasRunAtLeastOnce = true
return mergedProps
}
function handleNewPropsAndNewState(){
/* mapDispatchToProps の dependsOnOwnProps 次第で 新しい dispatchProps を merge するか決めている */
・・・省略
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
function handleNewProps() {
/* mapDispatchToProps と mapStateToProps の dependsOnOwnProps 次第で、それぞれ 新しい dispatchProps と stateProps に merge するか決めている */
・・・省略
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
function handleNewState() {
/* State に変化があったかどうかで、mergeProps するか決めている */
・・・省略
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)
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)
}
}
見てみたら、hasRunAtLeastOnce が false の1回目は mapStateToProps
と mapDispatchToProps
を使ってできた Props を merge して mergedProps として返し、hasRunAtLeastOnce が true の2回目以降は prop と state の変化次第で 違う mergedProps を返しています。
上で出てきた childPropsSelector
は、actualChildPropsSelector
→ actualChildProps
に引き継がれ、actualChildProps
から <WrappedComponent>
の props に渡されます。
ここまでの流れで、connect
で描画がされる部分と、connect
の mapStateToProps
と mapDispatchToProps
が 描画された部分に props として渡される部分まで見てみました。
一旦ここまで。