继续来看几个概念。
Thunk
在看到Redux的Thunk概念之前,我对于与服务端交互还停留在以前写Vue的时候把交互部分写在事件里的水平。看了之后发现自己有点low了,正确的方式是应该发送异步Action。
为什么要使用Redux-Thunk
这个问题,在SegmentFault:Redux异步解决方案之Redux-Thunk原理及源码解析里讲的很透彻,我只是觉得不这么用,自己包装会比较麻烦,这篇文章直接把官网的回答翻译了。看了很多东西只要到官网里都能找到答案的。
写点代码准备一下
通过create-react-app新创建一个项目,然后清空App.js,之后写一个按钮和一个展示结果的组件:
export const MyButton = (props) => (
<button {...props}>访问API</button>
);
export const MyResult = (props) => (
<h2>{props.value}</h2>
);
然后定义几个action,分别为开始获取API,获取失败和获取成功,以及创建对应的actionCreator:
export const actionTypes = {
FETCH_POKEMON: "FETCH_POKEMON",
FETCH_SUCCESS: "FETCH_SUCCESS",
FETCH_FAILED: "FETCH_FAILED",
};
export const fetchData = () => ({
type: actionTypes.FETCH_POKEMON
});
export const fetchSuccess = (data) => ({
type: actionTypes.FETCH_SUCCESS,
data,
});
export const fetchFailed = () => ({
type: actionTypes.FETCH_FAILED,
});
之后是reducer和store。这里考虑state中有三个属性,一个表示是否正在异步通信中,一个用于保存数据,一个表示是否失败。reducer就是更改这两个属性,一并编写如下:
export const rootReducer = (state, action) => {
switch (action.type) {
case actionTypes.FETCH_POKEMON:
return {isFetching: true, data: "开始获取数据", failed: false};
case actionTypes.FETCH_SUCCESS:
return {isFetching: false, data: action.data.next, failed: false}
case actionTypes.FETCH_FAILED:
return {isFetching: false, data: "获取失败", failed: true}
default:
return state;
}
};
const store = createStore(rootReducer, {
isFetching: false,
data: "没有数据",
failed: false
});
之后安装react-redux,使用Provider组件:
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
然后在App.js里连接一下MyResult到State。
function App() {
const myMapStateToProps = (state) => ({
disabled: state.isFetching,
value:state.data,
failed:state.failed,
});
const WrappedResult = connect(myMapStateToProps, null)(MyResult);
return (
<div className="App">
<MyButton/>
<WrappedResult/>
</div>
);
}
编写按钮的事件
这里我们准备访问的API是口袋妖怪的API:https://pokeapi.co/api/v2/pokemon
,这里为了方便,把按钮改成类组件:
import React, {Component} from 'react';
import {store} from "./index";
import {fetchFailed, fetchSuccess} from "./Actions";
class MyButton extends Component {
constructor(props) {
super(props);
}
fetchPokemon = () => {
fetch("https://pokeapi.co/api/v2/pokemon").then(response => response.json()).then(data => store.dispatch(fetchSuccess(data))).catch(error => store.dispatch(fetchFailed()));
}
render() {
return (
<button {...this.props} onClick={this.fetchPokemon}>访问API</button>
);
}
}
export default MyButton;
这其中把点击改成fetchAPI,根据结果来派发两个不同的dispatch,但是很难再派发其他控制的东西,因为Promise链还必须一个接一个的返回东西。
这个时候就可以使用到Thunk了。
派发函数而不是action
由于我们现在要派发函数而不是直接派发对象,我们先写这个要被派发的函数:
export const fetchTodos = () => {
return (dispatch) => {
dispatch(fetchTodosRequest());
return fetch("./mock/todos.json").then(
response => {
response.json().then(data => {
dispatch(fetchTodosSuccess(data))
}
)
},
error => {
dispatch(fetchTodosFailure(error));
console.log("An error occured: " + error)
}
);
}
}
这个函数运行之后,返回一个以dispatch为参数的函数,其中先用dispatch派发了开始访问API的动作,然后根据访问结果的不同,进行派发不同的动作。
然后我们需要派发这个函数,修改一下MyButton的代码如下:
class MyButton extends Component {
constructor(props) {
super(props);
}
handleClick = () => {
store.dispatch(fetchPokemon());
}
render() {
return (
<button {...this.props} onClick={this.handleClick}>访问API</button>
);
}
}
一运行,按下按钮,发现报错如下:
Error: Actions must be plain objects. Instead, the actual type was: 'function'.
提示action只能是纯对象,结果我们派发了一个函数,自然这也是我们预料之中的结果。
使用redux-thunk
这个时候就要使用redux-thunk了,先用npm安装:
npm install redux-thunk
之后需要在创建store的时候使用中间件,代码如下:
import thunk from "redux-thunk";
export const store = createStore(rootReducer, {
isFetching: false,
data: "没有数据",
failed: false
}, applyMiddleware(thunk));
redux-thunk就是用来让dispatch可以派发一个函数,这个函数的形态上边已经说了,就是返回一个以dispatch为参数的函数,来根据异步访问API的结果实际派发不同的action。
有了这种操作之后,可以把这个特定的函数就看成一个action,当成dispatch的参数进行使用,不仅仅可以使用在点击按钮这些事件交互中,也可以直接在组件挂载的时候就去派发这个函数,根据访问结果更新state,这种在Redux下使用异步action的方法非常优雅。