redux-thunkを使って非同期処理をする。
【所要時間】
約1時間(2019年1月11日)
【概要】
- redux-thunkというmiddlewareを使うことでredux側のactionに関数を与えることができるようになる。
- redux-thunkの完成コード
https://codesandbox.io/s/2078r0x1xr
【要約・学んだこと】
まずはReactとreduxだけで非同期処理を実行するファイルを作成する。この場合下記のようにReactで関数を置く。
ボタンをクリックすると1000mm秒後にpromise 1、その1000mm秒後にpromise 2が表示される。
https://codesandbox.io/s/2o92xyv15r
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
import store from "./redux/store";
import { Provider } from "react-redux";
import { getActions } from "./redux/reducer";
import { connect } from "react-redux";
import { getTxt } from "./redux/action";const mapStateToProps = state => {
return {
txt: state.getActions.txt,
number: state.getActions.number
};
};const ConnectedApp = ({ txt, number }) => {
console.log(txt)function clickHandler() {
const promiseTest = res => {
return new Promise((resolve, reject) => {
setTimeout(() => {
// console.log("promiseTest");
resolve(`promise ${res}`);
}, 1000);
});
};
const asyncTest = async () => {
try {
const newTxt1 = await promiseTest(1);
store.dispatch(getTxt(newTxt1, number));
const newTxt2 = await promiseTest(2);
store.dispatch(getTxt(txt, newTxt2));
} catch (err) {
store.dispatch(getTxt("err!!!"));
}
};
asyncTest();
}return (
<div className="App">
<button onClick={() => clickHandler()}>get txt</button>
<p>{txt}</p>
<p>{number}</p>
</div>
);
};const App = connect(mapStateToProps)(ConnectedApp);const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
- redux-thunkで同じ処理を行う。
redux-thunkを使うことで、actionに関数を与えることができるようになる。reduxの原則を無効にする魔改造を可能にする邪道なミドルウェアというイメージ。
これを使うことで、React側は見た目に関わる部分のコードを、Redux側ではその他の処理をするコードを、というように分けることができる。
まずはactionに非同期処理をする関数を入れる。
// redux/action.jsexport const clickTxt = () => {
return (dispatch, getState) => {
const promiseTest = res => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`promise ${res}`);
}, 1000);
});
};
const asyncTest = async () => {
try {
const newTxt1 = await promiseTest(1);
const reduxNumber = getState();dispatch(getTxt(newTxt1, reduxNumber.getActions.number));
const newTxt2 = await promiseTest(2);const reduxTxt = getState();dispatch(getTxt(reduxTxt.getActions.txt, newTxt2));
} catch (err) {
dispatch(getTxt("err!!!"));
}
};
asyncTest();
};
};export const GETTXT = "GETTXT";export const getTxt = (txt, number) => ({
type: GETTXT,
txt: txt,
number: number
});
ここでは第一引数にdispatch, 第二引数にgetStateを入れることで、dispatchとstateの取得が可能となるようだ。
export const clickTxt = () => {
return (dispatch, getState) => {
・・・
非同期処理を行う関数を移したことで、React部分のコードは下記のようになった。
const mapStateToProps = state => {
return {
txt: state.getActions.txt,
number: state.getActions.number
};
};const ConnectedApp = ({ txt, number }) => {
return (
<div className="App">
<button onClick={() => store.dispatch(clickTxt())}>get txt</button>
<p>{txt}</p>
<p>{number}</p>
</div>
);
};const App = connect(mapStateToProps)(ConnectedApp);
非常にすっきり。reduxのstoreでstateを保持し、dispatchした時の処理はすべてactionとreducerに記述されている。
ちなみにreduxを使わずreactのみで同じことをするとこのようになる。
https://codesandbox.io/s/pjv5x66j4x
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
txt: "initial",
number: 0
};
}clickHandler() {
const promiseTest = res => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`promise ${res}`);
}, 1000);
});
};
const asyncTest = async () => {
try {
const intentionalErr = +1;
const newTxt1 = await promiseTest(1);
this.setState({
txt: newTxt1
});
const newTxt2 = await promiseTest(2);
this.setState({
number: newTxt2
});
} catch (err) {
this.setState({
number: "err!!!"
});
}
};
asyncTest();
}render() {
return (
<div className="App">
<button onClick={() => this.clickHandler()}>get txt</button>
<p>{this.state.txt}</p>
<p>{this.state.number}</p>
</div>
);
}
}
同じことを
- Reactのみ https://codesandbox.io/s/pjv5x66j4x
- React, reduxを使う https://codesandbox.io/s/2o92xyv15r
- React, redux, redux-thunkを使う https://codesandbox.io/s/2078r0x1xr
の3パターンのやり方で行った。
これくらい単純であればReactのみで行うのが当然一番早く書けるが、アプリの規模によって色々とベストな方法は変わるのかと思う。
ただreduxを使わないと、stateの受け渡しが非常にわかりにくくなるパターンになってしまうことは想像できるので、自分設計する際はreactとあわせてreduxも使おうと思う。
【わからなかったこと】
- reduxにdispatchしたのに、reduxのstateを取得するとdispatchする前のstateを取得してしまう。
const mapStateToProps = state => {
return {
txt: state.getActions.txt,
number: state.getActions.number
};
};const ConnectedApp = ({ txt, number }) => {
console.log(txt);function clickHandler() {
const promiseTest = res => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`promise ${res}`);
}, 1000);
});
};
const asyncTest = async () => {
try {
const newTxt1 = await promiseTest(1);
store.dispatch(getTxt(newTxt1, number));
const newTxt2 = await promiseTest(2);
store.dispatch(getTxt(txt, newTxt2));
} catch (err) {
store.dispatch(getTxt("err!!!"));
}
};
asyncTest();
}return (
<div className="App">
<button onClick={() => clickHandler()}>get txt</button>
<p>{txt}</p>
<p>{number}</p>
</div>
);
};const App = connect(mapStateToProps)(ConnectedApp);//結果
initial(クリック前のstate.txtの値)
Object {getActions: Object}
getActions: Object
txt: "promise 1"
number: 0
promise 1
Object {getActions: Object}
getActions: Object
txt: "initial"
number: "promise 2"
initial(クリック後のstate.txtの値)
これはデータフローの問題。txtはスコープ外にあるため
clickHandler()
→asyncTest()
→promiseTest(1)→store.dispatch(getTxt(newTxt1, number));
→promiseTest(2)→store.dispatch(getTxt(txt, newTxt2));
となっており、2回目のdispatchの第一引数で渡しているtxtは初期のreduxのstateから取得してしまっている。
変更した値を取得するには、同じスコープ内でreduxのstateを取得して、その値を渡してあげればよい。
function clickHandler() {
const promiseTest = res => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`promise ${res}`);
}, 1000);
});
};
const asyncTest = async () => {
try {
const newTxt1 = await promiseTest(1);
const reduxNumber = store.getState();store.dispatch(getTxt(newTxt1, reduxNumber.getActions.number));
const newTxt2 = await promiseTest(2);
const reduxTxt = store.getState();
store.dispatch(getTxt(reduxTxt.getActions.txt, newTxt2));
} catch (err) {
store.dispatch(getTxt("err!!!"));
}
};
asyncTest();
}//結果Object {getActions: Object}
getActions: Object
txt: "promise 1" (クリック前のstate.txtの値)
number: 0
Object {getActions: Object}
getActions: Object
txt: "promise 1" (クリック後のstate.txtの値)
number: "promise 2"
これで2回目のdispatchで現在のstate.textの値(1回目のdispatchで変更された後の値)を渡すことができている。
追記
applyMiddlewareを宣言する箇所で、追加の引数を乗せることも可能。
// redux/store.jsimport { createStore, applyMiddleware } from "redux";
import rootReducer from "./reducer";
import thunk from "redux-thunk";const obj = { title: "text" };const store = createStore(
rootReducer,
applyMiddleware(thunk.withExtraArgument(obj))
);export default store;
actionでは第三引数にのせる。
export const clickTxt = () => {
return (dispatch, getState, obj) => {
const promiseTest = res => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`promise ${res}`);
}, 1000);
});
};const asyncTest = async () => {
try {
const newTxt1 = await promiseTest(1);
const reduxNumber = getState();dispatch(getTxt(newTxt1, reduxNumber.getActions.number));
const newTxt2 = await promiseTest(2);const reduxTxt = getState();
console.log(obj.title);
dispatch(getTxt(reduxTxt.getActions.txt, newTxt2));
} catch (err) {
dispatch(getTxt("err!!!"));
}
};
asyncTest();
};
};//結果
text
【感想】
一つのファイルに色々書いてあるのも読みにくいが、ファイルをまたいでデータを渡しまくるのもとても読みにくかった経験があるので、使いやすそう。(Redux公式のtodo listサンプルがわかりにくかった)
開発とは全く関係ないが、基本邪道は大好き。でもなぜかコンセプトを崩す心意気は好きじゃないことがわかった。でもredux-thunkは使いたい。というマインド。