Reducerを書くのにhandleAction"s"をやめた

redux-actionsを用いてActionCreatorとReducerを書いていますが、TypeScriptに移行して書き方がちょっと変わりました。

これまではActionCreatorをまとめて作るcreateActionsと、Reducerをまとめて作るhandleReducersを便利に用いてました。しかしTypeScriptに移行してみるとこのまとめた書き方は型情報をうまく活かせない。

// @types/redux-actionsのindex.d.tsより
export function handleActions<State, Payload>( // <- 型引数PayloadがreducerMap内で共通化
    reducerMap: ReducerMap<State, Payload>,
    initialState: State
): Reducer<State, Payload>;

ActionCreatorもReducerも、まとめるとどうしてもPayloadの型をアプリケーションワイドで大きなものにするか、anyで受けるかになってしまいます。アプリケーションワイドにすれば注目すべき情報の粒度が大きくなるし、anyで受けたら型の恩恵をうけられない。それならhandleAction(handleAction"s"ではなく)で個別にReducerを定義したほうがよさそう。

type State = Map<string, any>;
type MoveToPayload = { compiler: string, framework: string }; // 粒度が小さくany無しで書ける

export const moveToAction = createAction<MoveToPayload, string, string>(
    'MOVE_TO',
    (compiler, framework) => ({ compiler, framework }),
);

export const moveToReducer = handleAction<State, MoveToPayload>(
    moveToAction,
    (state, action) => action.payload ? state.merge(action.payload) : state,
    Map({}),
);

そうなるとActionCreatorとReducerはセットで書いたほうが、型も共通するしテストも一体だし、見通しが良いので格段に書きやすくなった気がします。

とはいえ、一つのReducerが一つのActionしか処理しないとなるとStoreを作るときにさすがに粒度があわない。小さなアプリであれば一つ、大きなアプリでも数個の名前空間にまとめたい。どうしたものか手段を探したけどしっくり来るものが見つからなかったので、最終的にはcreateStore(combineReducer())で複数名前空間を一つにまとめる前段階として、手書きで複数のアトミックなReducerを連結することにしました。

function reduceReducers<S>(reducers: Reducer<S, any>[]) {
    // Array#reduce()で連結して一個のReducerにまとめる
    return reducers.reduce<Reducer<S, any>>(
            (previousValue, currentValue) =>
                (state, action) => currentValue(previousValue(state, action), action),
            (state, action) => state, // <- reduceに初期値を省略するTypeScript型定義がなかった
        );
}

const app = reduceReducers<State>([
    // handleActionで作ったアトミックなReducerたちを以下に並べる。
    togglePageLayout,
    toggleAdjustPage,
    requestBooks,
    receiveBooks,
    moveToReducer,
    locationChangeReducer,
]);

// そしてStoreを作る。
const store = createStore(
    combineReducers({ router, form, app }), // react-router-redux + redux-form + アプリ
    composeWithDevTools(applyMiddleware(
        routerMiddleware(history),
        moveToMiddleware(),
    )),
);

// Containerではこんな感じ。connect()はreact-reduxのもの
export default connect(state => state.app.toJS(), () => ({}))(NavigationComponent);

結果として上記のように、アトミックなReducerの配列を数珠繋ぎすることになりました。結構、正解な気がする。