JavaScript
TypeScript
React
frontend

react-reduxのconnectでmergePropsを活用する

react-reduxでPresentational ComponentとContainer Componentの接続にconnect関数を使っているとひとつ困ることがあって、それはmapDispatchToPropsの中でステートを知ることができないということです。

const mapDispatchToProps = (dispatch: Dispatch<void>) => {
  borrowBook(bookId: BookId, currentUserId: UserId, price: number) {
    ...
  }
};

ステートに全部データが入っているのに、わざわざこうして呼び出しに引数で渡さないといけないというのも違和感が有りますが、なにより接続されるPresentational Component側でどのようなデータがpropsから渡ってきているかというのを知っていることが呼び出しの前提というのが責務上あまり良くない気がします。

なので、今回はmergePropsを活用して、こんな風にmapDispatchToPropsでステートを受け取ることができるようにしてみます。このようにすることで、Componentを経由して関数へステートのデータを渡す必要がなくなります。

const mapDispatchToProps = (state: MappedState, dispatch: Dispatch<void>) => {
  borrowBook() {
    const bookId = state.books.currentBook.id;
    const currentUsrId = state.users.currentUser.id;
    const price = state.books.currentBook.price * state.orders.currentQuantity;

    ...
  }
};

mapDispatchToPropsの代わりにmergePropsを使う

mergePropsというconnect関数の第三引数は、ステートとReduxのDispatch、接続されるPresentational Componentのpropsをすべて引数として受け取り、それらをmerge(結合)して返すためのコールバックを提供しています。

このmergePropsmapDispatchToPropsの代わりに、マッピングする関数の定義場所として使うことで、ステートを参照しながらPresentational Componentへマッピングする関数の定義を行えるようになります。

HogeContainer.ts
const mapStateToProps = (state: State) => state;

const mapDispatchToProps = (dispatch: Dispatch<void>) => ({ dispatch });

const mergeProps = 
  (state: State, { dispatch }: { dispatch: Dispatch<Void> }, ownProps: OwnProps)  => ({
    ...state,
    ...ownProps,

    borrowBook() {
      ...
    }
  });

export const connectHogeContainer = 
  connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component);

この実装で今回やりたいことはある程度実現できていますが、このコードの微妙な点はいくつかあり、

  • mergePropsの中でstateownPropsとの結合を毎回やらないといけないのが面倒だし忘れる可能性がある
  • できればmergePropsの引数をもう少しスマートにしたい(例えば{ dispatch }を止めたい)
  • mapDispatchToPropsの内容は変わらないので、できれば定義を省略したい

など、まだ改善の余地があります。

connect関数のラッパを作りmergePropsmapDispatchToPropsのように使う

よりDRYな実装にするために、以下のようなreact-reduxのconnectのラッパを定義しておくことで、mapDispatchToPropsと同様の挙動でmergePropsを使えるようになり、mergeProps特有のコードを隠蔽することができます。Object.assignで渡ってくる引数をすべて結合しているのがポイントです。

customConnect.ts
import { connect as reduxConnect } from 'react-redux';
import { Dispatch } from 'redux';
import { State } from '../store';

export const connect = <S, M>(
  mapStateToProps: (state: State) => S,
  mapDispatchToProps: (state: S, dispatch: Dispatch<void>) => M
) =>
  reduxConnect(
    mapStateToProps,
    (dispatch: Dispatch<void>) => ({ dispatch }),
    (state: S, { dispatch }: { dispatch: Dispatch<void> }, ownProps: {}) =>
      Object.assign({}, state, ownProps, mapDispatchToProps(state, dispatch))
    );

これを使うことによって、Container Component内でのmapDispatchToPropsは第一引数にmapStateToPropsで定義されたステートを受け取ることができるようになり、最初の実装のようなmergeProps特有のコードが減らせてスマートになりました。

HogeContainer.ts
import { connect } from '../customConnect';
import { Dispatch } from 'redux';
import { State } from '../store';

interface MappedState {
  currentBookId: number;
  currentUserId: number;
  subtotal: number;
}

const mapStateToProps = (state: State): MappedState => {
  currentBookId: state.books.currentBook.id,
  currentUsrId: state.users.currentUser.id,
  subtotal: state.books.currentBook.price * state.orders.currentQuantity
};

interface MappedActions {
  borrowBook: () => void;
}

const mapDispatchToProps = (state: MappedState, dispatch: Dispatch<void>) => {
  borrowBook() {
    const bookId = state.currentBookId;
    const currentUsrId = state.currentUserId;
    const subtotal = state.subtotal;

    ...
  }
};

export connectHogeContainer = 
  connect<MappedState, MappedActions>(mapStateToProps, mapDispatchToProps);

参考: http://anect.hatenablog.com/entry/2017/06/09/151000