LoginSignup
17
9

More than 3 years have passed since last update.

【React, redux, redux-saga】typesafe-actionsでコード量を減らそう

Last updated at Posted at 2019-05-30

はじめに

reduxのAction周りの型をしっかり付けようとすると、どうしてもコード量が多くなってしまいます。
さらに、redux-sagaを使って副作用を実装する場合は、副作用ごとに requestsuccessfailureの3ケースについてActionとActionCreatorを定義する必要があるため大変です。
この悩みを解決するために、本稿ではtypesafe-actionsというライブラリを使って少ないコードでしっかり型をつける方法を紹介します。

題材にするコード

今回はYouTube Data APIを利用して、動画を検索できるクライアントアプリを題材に説明していきます。
コードはこちら

実装内容

今回は入力されたテキストから動画を検索する機能をredux-sagaを用いて実装していきます。
これをredux-sagaのフローに分けると以下のようになります。

  1. 検索ボックスにテキストを入力してsubmitしたらAPIを叩く(REQUEST)
  2. 正常に値を受け取れた場合、Storeに保存する(SUCCESS)
  3. APIとの通信に失敗した場合、エラーを投げる(FAILURE)

Image from Gyazo

ActionType, ActionCreator, Actionの実装

まずはActionTypeを定義します。

actions/Video/VideoActionType.ts
enum VideoActionType {
  SEARCH_VIDEOS_REQUEST = "SEARCH_VIDEOS_REQUEST",
  SEARCH_VIDEOS_SUCCEEDED = "SEARCH_VIDEOS_SUCCEEDED",
  SEARCH_VIDEOS_FAILED = "SEARCH_VIDEOS_FAILED",
}

export default VideoActionType;

続いて、ActionCreatorを実装します。ここで、typesafe-actionsの出番です。
createAsyncActionを利用することで、簡単にActionCreatorを定義できます。

actions/Video/VideoActionCreator.ts
import VideoActionType from './VideoActionType';
import SearchListResponse from '../../states/SearchListResponse';
import { createAsyncAction } from 'typesafe-actions';

export const searchVideos = createAsyncAction(
  VideoActionType.SEARCH_VIDEOS_REQUEST,
  VideoActionType.SEARCH_VIDEOS_SUCCEEDED,
  VideoActionType.SEARCH_VIDEOS_FAILED
)<string, SearchListResponse, Error>();

以下の3つのActionCreatorをまとめて生成することができました。
- searchVideos.request(payload: string)
- searchVideos.success(payload: SearchListResponse)
- searchVideos.success(payload: Error)

それぞれのメソッドは、引数で指定したtypeとジェネリクスで指定した型のpayloadを返します。payloadがいらない場合はジェネリクスの該当部分にundefinedを指定しましょう。

素のTypeScriptだけで実装する場合、以下のように1つずつ定義する必要があるので、だいぶ楽ですね。

no-typesafe-actions.ts
function request(payload: string) {
  return {
    type: VideoActionType.SEARCH_VIDEOS_REQUEST,
    payload,
  };
}

function success(payload: SearchListResponse) {
  return {
    type: VideoActionType.SEARCH_VIDEOS_SUCCEEDED,
    payload,
  };
}

function failure(payload: Error) {
  return {
    type: VideoActionType.SEARCH_VIDEOS_FAILED,
    payload,
  };
}

最後にActionCreatorの戻り値のActionオブジェクトも一つの型にまとめましょう。
ここではtypesafe-actionsActionTypeを使うことでsearchVideosが持つ3つのActionCreatorの戻り値のActionオブジェクトを1つのunion型にまとめることができます。

この型は、Reducerの引数action: VideoActionやcontainerのmapDispatchToPropsの引数dispatch: Distatch<VideoAction>のように使用します。

actions/Video/VideoAction.ts
import { ActionType } from "typesafe-actions";
import * as ActionCreators from "./VideoActionCreator";

type VideoAction = ActionType<typeof ActionCreators>;

export default VideoAction;

sagaの実装

searchVideosSagasearchVideos.request(payload: string)が呼び出されると起動するジェネレータ関数です。この部分は最後の行のtakeLatestで設定しています。引数でrequestの戻り値のActionオブジェクトを受け取ります。

actionの型の指定はTypeScriptのReturnTypeを使います。今回の場合action: PayloadAction<VideoActionType.SEARCH_VIDEOS_REQUEST, string>ですが、ActionCreatorの引数が無い場合の型はEmptyAction<...>になるため、直接指定するのはナンセンスだからです。

さらに、yieldの戻り値は型がanyになってしまうため、yield call()で呼び出すapi.searchの戻り値の型を自分で指定する必要があります。

api.searchの型は、YouTubeApi.search(q: string): Promise<SearchListResponse | undefined>なため、ReturnTypePromise以下を取り出し、さらに下記のPromiseGenericTypePromiseのジェネリクス部分の型(SearchListResponse | undefined)を取り出しています。

sagas/Video/VideoSaga.ts
export function* searchVideosSaga(
  action: ReturnType<typeof VideoActionCreators.searchVideos.request>
) {
  const searchListResponse: PromiseGenericType<ReturnType<typeof api.search>> = yield call(
    api.search,
    action.payload
  );

  if (searchListResponse) {
    yield put(VideoActionCreators.searchVideos.success(searchListResponse));
  } else {
    yield put(
      VideoActionCreators.searchVideos.failure(new Error('searchListResponse is undefined'))
    );
  }
}

export const videoSagas = [takeLatest(VideoActionType.SEARCH_VIDEOS_REQUEST, searchVideosSaga)];
utils/TypeUtils.ts
export type PromiseGenericType<T> = T extends Promise<infer U> ? U : T;

その他

Redux storeのRootStateの定義

StateTypeを使うことでreducerからRoot Stateの型を定義することができます。
手動でRootStateを定義するとreducerを更新するたびに変更しなければいけないため、バグに繋がってしまうので、これを使うのがおすすめです。

state/index.ts
import { StateType } from 'typesafe-actions';
import rootReducer from 'src/reducers';

type RootState = StateType<typeof rootReducer>;
export default RootState;

RootStateはcontainerで以下のように使うと型推論が効いてミスを無くすことができます。

containers/VideoList.ts
import RootState from 'src/states';
import VideoList from 'src/components/VideoList/VideoList';
import { VideoListConnectedProps } from 'src/components/VideoList/VideoListProps';
import { connect } from 'react-redux';

const mapStateToProps = (state: RootState): VideoListConnectedProps => ({
  searchListResponse: state.searchListResponse,
});

export default connect(
  mapStateToProps,
  null
)(VideoList);

おわりに

typesafe-actionsには他にも便利なユーティリティがたくさんあるので、ぜひ使ってみてください。

17
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
9