reduxの非同期処理で困ること
reduxのアクションで非同期処理を書くには以下のような方法があります。
- コンポーネントに直接書く
- redux-thunkを使う
- middlewareでapiアクションだけ非同期処理を挟む
- redux-sagaを使う
それぞれ利点・欠点があると思いますが、以下の観点を気にすると方法はかなり限られると思います。
- api通信後に処理を書きたい(Promiseが欲しい)場合
- 通信中のものをキャンセルしたい場合
愚直に書くとこうなってしまうと思います。
class App extends Component {
componentWillReceiveProps(nextProps) {
// nextPropsでdataが受け取れていれば次の画面に遷移する
if (nextProps.data) {
this.props.history.push('/next');
}
}
fetchData() {
// リクエストを投げるアクションを書く(成功可否はnextPropsにdataが入っているかで判断する)
this.props.dispatch(fetchData());
}
render() {
return (
<div>
<button onClick={() => { this.fetchData(); }}>fetch</button>
</div>
);
}
}
データのあるなしで判定するのは危険なので、通信結果のステータスをstoreに持たせるように書いても、結局willReceivePropsとかでチェックしないといけなくてかなり読みにくくなります。
redux-thunkで通信終了化の判定はいけるが・・・
redux-thunkはアクションを関数で渡すことができ、その結果も返すことができます。よって、通信中のpromiseを返してあげれば通信終了の判定が分かりやすくなります。
/**
* 汎用的なAPI Actionを作るための関数
*/
function createAPIAction(options) {
const {
method,
url,
query = {},
successType = null
} = options;
return (dispatch) => {
const requestOptions = { method, url };
requestOptions[method === 'get' ? 'params' : 'data'] = query;
return axios(requestOptions)
.then((res) => {
if (successType) {
dispatch({ type: successType, response: res });
}
});
};
}
/**
* createAPIActionを通して通信Actionを作る
*/
function fetchData() {
return createAPIAction({
method: 'get',
url: '/data',
successType: FETCH_DATA_SUCCESS
});
}
class App extends Component {
fetchData() {
// promiseが返ってくるので通信後の処理が書きやすい
this.props.dispatch(fetchData())
.then(() => {
this.props.history.push('/next');
});
}
render() {
return (
<div>
<button onClick={() => { this.fetchData(); }}>fetch</button>
</div>
);
}
}
ただこう書いた場合はdispatchした後に何が返ってくるか考えないといけなくなります。
// これはデータを取ってくるからpromiseが返る
this.props.dispatch(fetchData())
.then(() => { ... });
// selectDataはstoreを更新するだけなのかAPI通信をして更新をするのか分からない(promiseが返ってくるか分からない)
this.props.dispatch(selectData(data))
.then(() => { ... }); // これを書いて問題ない?
またただ変更をリクエストしたときはstoreをほぼ書き換えず終わることがあるため、dispatchしている意味が分からないときもあります。
function requestChangeData(data) {
// アクションを作りはするが、通信だけして実際はstoreの更新は何もしない
createAPIAction({
method: 'post',
url: '/change-data',
query: { data }
});
}
// storeに登録するというよりも、通信をするということに意識が向いている気がする
this.props.dispatch(requestChangeData(data))
.then(() => { ... });
promiseが欲しい場合は直接書いたほうが分かりやすい
今までさんざん悩み続けて、何でこんなに扱いづらいんだろうと思っていたのですが、そもそもreduxは目的が違っていました。reduxはあくまでstoreを管理するためのツールであり、非同期処理の成功可否とかを気にしていないです。actionを投げたら最後、actionが終了したかの判定も分からなくなります。単純にstoreに入ってきたデータを使うだけという感じです。
通信に成功したとか、そういったPromiseを求めるような処理をreduxに求めると良く分からないことになってしまいます。
そんなわけで、非同期処理は直接書いたほうが非常に分かりやすくなります。
class App extends Component {
fetchData() {
axios({
method: 'get',
url: '/data'
})
.then((res) => {
// レスポンスデータをセットする
this.props.dispatch((setData(res.data));
// 通信に成功したので次の画面に遷移する
this.props.history.push('/next');
});
}
}
とは言えそのまま書いちゃうとAPI通信が至る所に書くことになって管理がしづらくなってしまいます。その時はAPIクラスをオリジナルで用意してこれを介して通信するといいと思います。場合によってはstoreに直接dispatchしてもいいですし。
ただstoreに直接dispatchするのは一般的ではない上、「API通信 + actionの送信」という2つの処理を書いてしまっているのであんまりオススメできないかもしれないです。ただ例外的に許すとコンポーネント内のコード量はグッと減らせると思います。
// 実はstoreから直接dispatchができるのでimportする
import store from '~/store/store.js';
/**
* 汎用的な通信関数
*/
function request(options) {
const {
method,
url,
query = {},
successType = null
} = options;
const requestOptions = { method, url };
requestOptions[method === 'get' ? 'params' : 'data'] = query;
return axios(requestOptions)
.then((res) => {
if (successType) {
// dispatch処理まで書きたかったら含める
store.dispatch({ type: successType, response: res });
}
});
}
/**
* API通信をまとめるオブジェクト
*/
const API = {
fetchData: () => {
return request({
method: 'get',
url: '/data',
successType: FETCH_DATA_SUCCESS
});
}
};
class App extends Component {
fetchData() {
API.fetchData()
.then(() => {
// 通信に成功したので次の画面に遷移する
this.props.history.push('/next');
});
}
}
キャンセル処理は書いていませんが、request関数を介しているだけなので中でpromiseとcancel関数を返すようにしたらキャンセルすることもできるようになります。
まとめ
reduxは非同期処理がデフォルトでは入っていなくて、なかなか苦労させられると思います。非同期処理を何とかするために色々な手法が提案されてきましたが、結局いかに上手くstoreにアクションを送り付けるかという話で、actionが同期的なものと非同期のものが混ざり合うことは変わりありません。
そもそも非同期処理はAPI通信ぐらいなものなので、API通信側で直接dispatchしてあげたほうが見通しがよくなるんじゃないかと思いました。この書き方の場合はpromiseがもらえるので通信後の処理が書きやすくなります。
結局非同期処理をredux内部に押し込むか、API通信用のメソッドを自分で作ってグローバル変数を一つ増やすかの違いになると思います。reduxの設計の参考になれば幸いです。