LoginSignup
0
0

More than 1 year has passed since last update.

React-Redux のソースコード を読んでみる

Posted at

react-redux を読んでみたので、そのメモ。
前回の redux のソースコードを読んだ記事の続きです。
コード読むのに慣れていないので、間違っている部分もあるかもしれません。あったらご指摘ください。

対象読者

redux を知っている人。

react-redux の connect

connect 部分が気になったので、そこを読んでみました。
まず、connect は

connect(mapStateToProps, mapDispatchToProps)(コンポーネント名)

のようにして書きますが、この ()() の部分はクロージャーと呼ばれる書き方になっていて、

const appComponent = connect(mapStateToProps, mapDispatchToProps)
appComponent(コンポーネント名)

と同じ書き方になります。
その中身が、ここのソースコードに書いてあります。
ソースコードから、connect 部分だけを取り出すと、

src/components/connect.tsx
・・・省略
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=> のみを返しているのが分かります。

では、

src/components/connect.tsx 489〜506行目
  ・・・省略 (mapStateToProps, mapDispatchToProps の処理)

の部分を見てみましょう。
ここでは、connect()() の前半の () から mapStateToProps と mapDispatchToProps を取り出して、以下の関数に当てはめています。

src/components/connect.tsx
  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 は、

src/components/connect.tsx
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 の中身の defaultMapStateToPropsFactoriesdefaultMapDispatchToPropsFactories はどうなっているんでしょうか?

defaultMapStateToPropsFactoriessrc/connect/mapDispatchToProps.tsに、 defaultMapDispatchToPropsFactoriessrc/connect/mapStateToProps.ts に、export default で定義されているんですが、それぞれで共通して使われているのが src/connect/wrapMapToProps.ts にある関数 wrapMapToPropsConstantwrapMapToPropsFunc です。

ここでは、wrapMapToPropsFunc を見てみましょう。

src/connect/wrapMapToProps.ts
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
  }
}

読みづらいですが、

src/connect/wrapMapToProps.ts 92行目
      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,
    }),
  }
};

これで、

src/components/connect.tsx
  const initMapStateToProps = match(・・・省略)
  const initMapDispatchToProps = match(・・・省略)
  const initMergeProps = match(・・・省略)

に無事に、返り値が入りました。
これらの initMapStateToPropsinitMapDispatchToPropsinitMergeProps は、selectorFactoryOptions に代入され、ConnectFunction 内の childPropsSelectorsrc/connect/selectorFactory.tsfinalPropsSelectorFactory に渡されます。

src/components/connect.tsx
      const childPropsSelector = useMemo(() => {
        return defaultSelectorFactory(store.dispatch, selectorFactoryOptions)
      }, [store])
src/connect/selectorFactory.ts
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 の中身は下のようになります。

src/connect/selectorFactory.ts
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回目は mapStateToPropsmapDispatchToProps を使ってできた Props を merge して mergedProps として返し、hasRunAtLeastOnce が true の2回目以降は prop と state の変化次第で 違う mergedProps を返しています。

上で出てきた childPropsSelector は、actualChildPropsSelectoractualChildProps に引き継がれ、actualChildProps から <WrappedComponent> の props に渡されます。

ここまでの流れで、connect で描画がされる部分と、connectmapStateToPropsmapDispatchToProps が 描画された部分に props として渡される部分まで見てみました。

一旦ここまで。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0