素の Redux の型の対応状況を確認してみた

2020-08-06

Redux と TypeScript の組み合わせで開発を行う際は、いつも TypeScript FSA を使用している自分ですが。

今回は素の Redux でどこまで型が補完されるのか、確認してみました。

使用したパッケージは以下のとおりです。

  • axios
  • create-react-app
  • redux
  • react-redux
  • redux-promise-middleware
  • type-to-reducer
  • typescript

なるべく現場での使い方に合わせたいので、api のコールまで実装してあります。

今回は前から組んでみたかった redux-promise-middleware を使用しています、type-to-reducer は付随するパッケージみたいですね。


index.tsx

import React, { StrictMode } from "react";

import Containers from "conainers";
import { Provider } from "react-redux";
import ReactDOM from "react-dom";
import store from "store";

ReactDOM.render(
  <StrictMode>
    <Provider store={store}>
      <Containers />
    </Provider>
  </StrictMode>,
  document.getElementById("root"),
);

普通ですね、特に特筆することもなく。

containers/index.tsx

import React, { FC, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";

import { State } from "reducer";
import getFloof from "actions/getFloof";

const Containers: FC = () => {
  const dispatch = useDispatch();
  const { image, link } = useSelector<State, { image: string; link: string }>(
    ({ floof }) => floof,
  );

  useEffect(() => {
    dispatch(getFloof());
  }, [dispatch]);

  return image && link ? (
    <a href={link} rel="noopener noreferrer" target="_blank">
      <img alt="floof" src={image} />
    </a>
  ) : null;
};

export default Containers;

今まで通りきちんと型が付与されています。

今回は action に引数を渡していないのであまり良い例ではないですが、誤差の範囲かなと。

actions/getFloof/index.ts

import axios from "axios";

export const GET_FLOOF = "GET_FLOOF";

const getFloof = () => ({
  payload: axios
    .get<{ image: string; link: string }>("https://randomfox.ca/floof/")
    .then(({ data }) => data),
  type: GET_FLOOF,
});

export default getFloof;

redux-promise-middleware を使用すると、どうしても action がごちゃつきますね。

個人的には Redux っぽくないのであまり好きではないですが、小さいアプリケーションならアリかなーという印象です。

あと、当たり前ですが payload の形がフワっとします、FSA の形が強制されないのはやっぱりイマイチですね。

reducers/floof/index.ts

import { GET_FLOOF } from "actions/getFloof";
import typeToReducer from "type-to-reducer";

export type FloofState = {
  image: string;
  link: string;
};

const initialState: FloofState = {
  image: "",
  link: "",
};

const floof = typeToReducer(
  {
    [GET_FLOOF]: {
      // action が any
      FULFILLED: (state, { payload }) => ({
        ...state,
        ...payload,
      }),
    },
  },
  initialState,
);

export default floof;

意外だったのが reducer 内の action に型が付与されません、もしかしたら見落としかもですが。

結構致命的だなーと思いますが、どーなんでしょうか。

普通に書いたらこんな感じですかね?

const floof = (state = initialState, { payload, type }: any) => {
  switch (type) {
    case `${GET_FLOOF}_FULFILLED`: {
      return { ...state, ...payload };
    }
    default: {
      return state;
    }
  }
};

redux-promise-middleware だと reducer の書きっぷりもイマイチですね、これは意外でした。

reducer/index.ts

import floof, { FloofState } from "reducers/floof";

import { combineReducers } from "redux";

export type State = {
  floof: FloofState;
};

const reducer = combineReducers<State>({ floof });

export default reducer;

特に問題なく。

store/index.ts

import { applyMiddleware, createStore } from "redux";

import promise from "redux-promise-middleware";
import reducer from "reducer";

const store = createStore(reducer, {}, applyMiddleware(promise));

export default store;

こちらも特に問題なく。

余談ですが、preloadedState ってどういうときに流し込むんですかね…?


結論としては、無難に FSA 系のパッケージを使うのがベターだなぁという印象でした。

やはり action の書きっぷりが FSA に強制されないのは結構痛いです。

フロント歴の浅い方が Redux を使用する場合、action の形をぐちゃぐちゃに書いてしまうのがあるあるだと思うのですが。

そういった事態を起こさないようにするためにも、やはり TypeScript FSA などの外部パッケージの恩恵は強いと感じました。

あと redux-promise-middleware を使ってしまうと FULFILLED 時の action の型、つまり payload の型が浮いてしまうのが非常に bad だなーと。

やはり api 周りは直接 Redux に絡ませるのではなく、saga や epic で外出しにしてやるのが無難だと思います。

そうすれば action にもきちんと型がつきますし、困ることがないよなーと。

今回のソースコードサンプルは公開していますので、ぜひぜひご参考までに。