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>
のようなものを定義して、状態の表現とデータをまとめてる
- 非同期API呼び出しでは、たいていloading状態や失敗の状態の管理もいるので、
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 ...
}
}
}