📑

react-reduxのv8で消えたconnectAdvancedのマイグレーション

2022/07/13に公開

概要

react-redux@8.0.0からconnectAdvancedという関数が消えました。
connectAdvancedを使用していたプロジェクトでのv8系のマイグレーションに少し詰まってしまったため、その解決策を記述します。

そもそもconnectAdvancedとはどんな関数なのか

ReduxのstateをReactのpropsに変換するための関数です。

function connectAdvanced(selectorFactory, connectOptions?)

公式のドキュメントにもあるように、selectorFactory型の関数を引数として受け取ります。
selectorFactoryのインターフェースはこうで

selectorFactory(dispatch, factoryOptions): selector(state, ownProps): props

dispatchを受け取って、stateとownPropsを受け取る関数を返却する関数ということになります。

これだけ見てもよくわからないと思うので、プロジェクト内での使用例を紹介して説明します。

import { connectAdvanced, SelectorFactory } from "react-redux";
import { Dispatch, Action } from "redux";
import { createSelector } from "reselect";

type State = {
    hoge: string;
}
type Props = {
    text: string;
    onClick: () => void;
}
const getHoge = (state: State) => state.hoge;
const getDispatch = (state: State, dispatch: Dispatch<any>) => dispatch;

// 3
const getProps = createSelector(getDispatch, getHoge, (dispatch, hoge): Props => {
    return {
        text: hoge,
        onClick: () => {
            dispatch(someAction());
        }
    }
});

// 2
const selector: SelectorFactory<State, Props, Props, {}> = (
    dispatch: Dispatch<Action>
): ((state: State) => Props) => {
    return (state: State): Props => getProps(state, dispatch);
}

// 4
const Component: React.FC<Props> = (props) => {
  return (
      <div>
	  <button onClick={props.onClick}/>
		  {props.text}
	  </button>
      </div>
  )  
};

// 1
export default connectAdvanced(selector)(Component);

簡単な擬似TSコードですが、この例ではReduxのStateとして管理している値をReactのPropsに変換し、かつReduxのActionとの繋ぎ込みもしています。

  1. connectAdvancedにはselectorとComponentをそれぞれ渡すことで、ReduxのStateをReactのPropsに変換し、Reactコンポーネントに渡すことを実現しています。
  2. connectAdvancedの引数に与えたselectorの中では、stateと、第二引数としてdispatchを、getPropsという関数に渡します。
  3. getProps内では、stateから任意の値(ここではhoge)を取得する関数と、2.で渡したdispatchを受け取るための関数をselectorの形で渡します。そうすると、getPropsが返す関数内では、hogeとdispath を受け取り自由にオブジェクトを返すことができます。
    ここではdispatchを参照できるため、onClickというコールバック関数の中でsomeActionというreduxのActionを実行することができます。
  4. Reactコンポーネントの中ではgetPropsで作成したPropsオブジェクトを使って表示やユーザーアクションとの結合をします。

このやり方だとselectorがネストしている状態でもそれぞれのselector内でdispatchを取り回すことができて大変便利だったのですが、残念ながらv8からは定義が消えてしまったので、マイグレーションする必要が出てしまいました。

解決策 connect関数を使うという案

react-reduxにはconnectという関数があります。

function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)

mapStateToPropsとmapDispatchToPropsを引数としてそれぞれ受け取ります。
それぞれ先ほどの例と同様おそらくこのように使用することでしょう

import { connect } from "react-redux";
import { Dispatch, Action } from "redux";

type State = {
    hoge: string;
}
type Props = {
    text: string;
}
const mapStateToProps = (state: State) => {
  return { text: state.hoge }; 
};
const mapDispatchToProps = (dispatch: Dispatch<Action>) => {
  return {
    onClick: () => dispatch(someAction()),
  };
};
const Component: React.FC<Props& {onClick: () => void}> = (props) => {
  return (
      <div>
	  <button onClick={props.onClick}/>
		  {props.text}
	  </button>
      </div>
  )  
};
export default connect(mapStateToProps, mapDispatchToProps)(Component);

なお、mapStateToPropsにはreselectのcreateSelectorを入れても問題ありません。

先ほどの例と似ていますが、大きな違いは、stateをpropsに変換する関数と、dispatchを関数に変換する関数がそれぞれ分離していることです。
connectするとReactのコンポーネントに渡されるpropsには、mapStateToPropsの結果と、mapDispatchToPropsの結果がマージされるため、Reactコンポーネント内では今までと同様扱うことが可能になります。

connect では扱いにくいパターン

しかし、Propsやコンポーネントが入れ子になっていくとconnectでは扱いにくくなります。

type Child1Props = {
    text: string;
}
type Child2Props = {
    text: string;
    onClick: () => void;
}
type Props = {
    child1: Child1Props;
    child2: Child2Props;
}
const Child1: React.FC<Child1Props> = (props) => {
  return (
      <button>
          {props.text}
      </button>
  )
};
const Child2: React.FC<Child2Props> = (props) => {
    return (
        <button onClick={props.onClick}>
            {props.text}
        </button>
    )
};
const Component: React.FC<Props> = (props) => {
    return (
        <>
            <Child1 {...props.child1}/>
            <Child2 {...props.child2}/>
        </>
    )
};

たとえばこんな風にコンポーネントが複数のコンポーネントによって構成されている例を考えてください。
特にdispatchを引き渡す際に、connectではmapDispatchToPropsで関数に変換したものをReactのpropsとして注入していたわけですが、mapStateToPropsの結果とフラットにしかマージされないため、階層化されている場合、ネストしたコンポーネントにそれぞれdispatchを引き渡していかなければなりません。
特に今回のプロジェクトでは複雑にコンポーネントがネストされているため、既存の設計に大きく手を入れる必要がありました。

解決策 useSelectorとuseDispatchを使用する

ここまでずらずらと書いてきましたが、結果的にはreact-reduxのuseSelectoruseDispatchというhooksを使用することで設計に大きく手を入れずにマイグレーションすることが可能でした。

import { useDispatch, useSelector } from "react-redux";
import { Dispatch, Action } from "redux";
import { createSelector } from "reselect";

type State = {
    hoge: string;
}
type Props = {
    text: string;
    onClick: () => void;
}
const getHoge = (state: State) => state.hoge;
const getDispatch = (state: State, dispatch: Dispatch<any>) => dispatch;

const getProps = createSelector(getDispatch, getHoge, (dispatch, hoge): Props => {
    return {
        text: hoge,
        onClick: () => {
            dispatch(someAction());
        }
    }
});

const Component: React.FC<Props> = (props) => { 
  const dispatch = useDispatch();
  const selector = useSelector((state: State) => getProps(state, dispatch));
  return (
      <div>
	  <button onClick={props.onClick}/>
		  {props.text}
	  </button>
      </div>
  )  
};
export default Component;

このようにReactコンポーネントの中でuseDispatchでdispatchを取得し、useSelectorを使用して、connectAdvancedと同様propsを作成する関数の第二引数に引き渡します。
この方法であればselectorの既存コードには一切手を入れる必要がありません。

まとめ

connectAdvancedが消えましたが、useSelectorとuseDispatchというhooksを使用することで今まで通りの使用感を維持しながらバージョンを上げることができました。

Discussion