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);