JavaScript
TypeScript
redux

reduxでredux-thunkとtypescript-fsaで非同期処理をする

reduxで非同期処理は、redux-sagaとかいろいろ設計があるけど、自分としてredux-thunkでやるのが理解しやすい。
また、Typescriptでそのままredux使おうとすると、型定義でつらいことになりがちなので、typescript-fsaを使うといいらしい。

コード

storeの定義

  • redux-thunkを入れる
store.ts
import { createStore, combineReducers, applyMiddleware } from "redux";
import thunk from "redux-thunk";

import * as doc from "./states/doc";

export interface RootStore {
    doc: doc.State
}

const store = createStore(
    combineReducers<RootStore>({
        doc: doc.reducer
    }),
    {},
    applyMiddleware(thunk)
);

export default store;

actionの定義

  • /actionsディレクトリ以下に置いてる
  • typescript-fsaでActionの定義をする
  • actionCreator.asyncを利用することで、非同期処理に使いそうなAction(started,done,failed)を定義してくれるので、非同期処理の状態(例ではfetchの開始・完了・失敗)に応じて、dispatchしてあげればよい
  • redux-thunkを使うのでAction Creator(下記loadAllDoc)は関数を返す
    • Promiseを返すようにしておくと、dispatchする側から結果の取得ができ便利
actions/doc.ts
import { Dispatch } from "redux";
import actionCreatorFactory from "typescript-fsa";

import { RootStore } from "../store";

// データの型定義
export interface Doc {
    id: string; 
    // ...
}

// typescript-fsaでActionの定義 
const actionCreator = actionCreatorFactory();
export const actions = {
    loadAllDocs: actionCreator.async<{}, Doc[]>("LOAD_ALL_DOCS"),
};

// redux-thunkを使うためActionは関数
export function loadAllDoc() {
    return (dispatch: Dispatch<RootStore>, getState: () => RootStore) => {
        return new Promise<Doc>((resolve, reject) => {
            // 開始
            dispatch(actions.loadAllDocs.started({params: {}}));
            fetch("http://....")
                .then(res => {
                    return res.json()
                        .then(json => {
                            if(res.ok) {
                                // 成功
                                dispatch(actions.loadAllDocs.done({result: json, params: {}}));
                                resolve(json);
                            } else {
                                // 失敗
                                dispatch(actions.loadAllDocs.failed({error: json, params: {}}));
                                reject(new Error("..."));
                            }
                        });
                }).catch(error => {
                    // 失敗
                    dispatch(actions.loadAllDocs.failed({error: error, params: {}}));
                    reject(error);
                });
        });
    }
}


stateの定義

  • /statesディレクトリ以下に置いてる
  • typescript-fsaを使うとactionで定義した型がreducerの定義時に使える
  • 非同期処理用の各Action(started,done,failed)ごとにstate を更新する
    • 非同期API呼び出しでは、たいていloading状態や失敗の状態の管理もいるので、StoreData<T>のようなものを定義して、状態の表現とデータをまとめてる
states/doc.ts
import { reducerWithInitialState } from "typescript-fsa-reducers";
import { actions, Doc } from "../actions/doc";

// 非同期の取得状態を表現するための型
export interface StoreData<T> {
    data: T;
    loading?: boolean;
    error?: any;
}

export interface State {
    all: StoreData<Doc[]>;
}

const initialState: State = {
    all: {
        data: [],
        loading: false
    }
};

// typescript-fsaでreducerの定義 
// started,done,failed でstate をそれぞれ更新
export const reducer = reducerWithInitialState(initialState)
    .case(actions.loadAllDocs.started, (state, payload) => {
        return { ...state, all: { data: [], loading: true, error: null } };
    }).case(actions.loadAllDocs.done, (state, payload) => {
        return { ...state, all: { data: payload.result, loading: false, error: null } };
    }).case(actions.loadAllDocs.failed, (state, payload) => {
        return { ...state, all: { data: [], loading: false, error: payload.error } };
    });

呼び出し

  • reactから呼ぶ、react-reduxでconnectしておく
view.tsx
class View extends React.Component<{}, {}> {

    componentDidMount() {
        // actionをdispatchする
        // redux-thunkでPromiseを返すように定義したので、ここの戻り値がそのPromiseになる
        // Promiseの結果で何か(localなステートに何かする等)できる
        this.props.dispatch(loadAllDoc())
            .then((all) => {
                // 必要なら成功したら画面遷移とか
                // this.props.history.replace(`/docs`);
            }).catch((error) => {
                // 必要なら失敗の通知とか
                // toast("....")
            })
    }

    render() {
        const { doc } = this.props;
        if(doc.all.loading) {
            // loading状態
            return <div>loading</div>
        } else if(doc.all.error){
            // 失敗した状態(エラー表示)
            return ...
        } else {
            // doc.dataを表示
            return ...
        }
    }
}