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(結合)して返すためのコールバックを提供しています。
このmergePropsをmapDispatchToPropsの代わりに、マッピングする関数の定義場所として使うことで、ステートを参照しながらPresentational Componentへマッピングする関数の定義を行えるようになります。
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の中でstateとownPropsとの結合を毎回やらないといけないのが面倒だし忘れる可能性がある - できれば
mergePropsの引数をもう少しスマートにしたい(例えば{ dispatch }を止めたい) -
mapDispatchToPropsの内容は変わらないので、できれば定義を省略したい
など、まだ改善の余地があります。
connect関数のラッパを作りmergePropsをmapDispatchToPropsのように使う
よりDRYな実装にするために、以下のようなreact-reduxのconnectのラッパを定義しておくことで、mapDispatchToPropsと同様の挙動でmergePropsを使えるようになり、mergeProps特有のコードを隠蔽することができます。Object.assignで渡ってくる引数をすべて結合しているのがポイントです。
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特有のコードが減らせてスマートになりました。
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);