本文是学习了2018年新鲜出炉的React Hooks提案之后,针对异步请求数据写的一个案例。注意,本文假设了:1.你已经初步了解​​hooks​​的含义了,如果不了解还请移步​​官方文档​​。(其实有过翻译的想法,不过印记中文一直在翻译,就是比较慢啦)2.你使用​​Redux​​实现过异步​​Action​​(非必需,只是本文不涉及该部分知识而直接使用)3.你听说过​​axios​​或者​​fetch​​(如果没有,那么想象一下原生js的​​promise​​实现异步请求,或者去学习下这俩库)全部代码参见仓库: ​​github | Marckon​​选择​​hooks-onlineShop​​分支以及​​master​​分支查看

本文并非最佳实践,如有更好的方法或发现文中纰漏,欢迎指正!

前序方案(不想看可以直接跳过)
  • 不考虑引入Redux

通过学习​​React生命周期​​,我们知道适合进行异步请求的地方是​​componentDidMount​​钩子函数内。因此,当你不需要考虑状态管理时,以往的方法很简单:

class App extends React.Component{
componentDidMount(){
axios.get('/your/api')
.then(res=>/*...*/)
}
}


  • 引入Redux进行状态管理

当你决定使用​​Redux​​进行状态管理时,比如将异步获取到的数据储存在​​store​​中,事情就开始复杂起来了。根据​​Redux​​的官方文档案例来看,为了实现异步​​action​​,你还得需要一个类似于​​redux-thunk​​的第三方库来解析你的异步​​action​​。

requestAction.js: 定义异步请求action的地方

//这是一个异步action,分发了两个同步action,redux-thunk能够理解它
const fetchGoodsList = url => dispatch => {
dispatch(requestGoodsList());
axios.get(url)
.then(res=>{
dispatch(receiveGoodsList(res.data))
})
};


requestReducer.js: 处理同步action

const requestReducer=(state=initialState,action)=>{
switch (action.type) {
case REQUEST_GOODSLIST:
return Object.assign({},state,{
isFetching: true
});
case RECEIVE_GOODSLIST:
return Object.assign({},state,{
isFetching:false,
goodsList:action.goodsList
});
default:
return state;
}
};


App Component :你引入redux store和redux-thunk中间件的地方

import {Provider} from 'react-redux';
import thunkMiddleWare from 'redux-thunk';
import {createStore,applyMiddleware} from 'redux';
//other imports

let store=createStore(
rootReducer,
//这里要使用中间件,才能够完成异步请求
applyMiddleware(
thunkMiddleWare,
myMiddleWare,

)
);
class App extends React.Component{
render(){
return (
<Provider store={store}>
<RootComponent/>
</Provider>
)
}
}


GoodsList Component :需要进行异步请求的组件

class GoodsList extends React.Component{
//...
componentDidMount(){
this.props.fetchGoodsList('your/url');
}
//...
}
const mapDispatchToProps={
fetchGoodsList
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(GoodsList);


完整代码:​​branch:master-onlineShop​

使用​​Hooks​​-​​useReducer()​​和​​useContext()​

总之使用​​Redux​​很累,当然,你可以不使用Redux,直接通过​​props​​层层传递,或者使用​​context​​都可以。只不过本文我们学过了​​useReducer​​,使用到了​​Redux​​的思想,总要试着用一下。

这里你不需要引入别的任何第三方库了,简简单单地使用React@16.7.0-alpha.2版本就好啦

很重要的一点就是——函数式组件,现在React推荐我们这么做,可以基本上代替​​class​​写法。

函数签名

  1. ​useReducer(reducer,initialState)​
  2. ​useContext(ctxObj)​
  3. ​useEffect(effectFunction,[dependencyValues])​

概览-你需要编写什么

  1. action.js:
  • 我们还使用​​redux​​的思想,编写action
  1. reducer.js:
  • 处理action,不同于​​redux​​的​​reducer​​,这里我们可以不用提供初始状态
  1. 根组件:
  • ​Provider​​提供给子组件​​context​
  • ​useReducer​​定义的位置,引入一个​​reducer​​并且提供初始状态​​initialState​
  1. 子组件:
  • ​useContext​​定义的位置,获取祖先组件提供的​​context​
  • ​useEffect​​用于进行异步请求

实现

1.action.js:我们使用action创建函数

const REQUEST_GOODSLIST = "REQUEST_GOODSLIST";
const RECEIVE_GOODSLIST = "RECEIVE_GOODSLIST";

//开始请求
const requestGoodsList = () => ({
type: REQUEST_GOODSLIST
});

//接收到数据
const receiveGoodsList = json => ({
type: RECEIVE_GOODSLIST,
goodsList: json.goodsList,
receivedAt: Date.now()
});

export {
RECEIVE_GOODSLIST,
REQUEST_GOODSLIST,
receiveGoodsList,
requestGoodsList,
}


2.reducer.js:判断action的类型并进行相应处理,更新​​state​

import {
RECEIVE_GOODSLIST,
REQUEST_GOODSLIST,
} from "../..";


export const fetchReducer=(state,action)=>{
switch (action.type) {
case REQUEST_GOODSLIST:
return Object.assign({},state,{
isFetching: true
});
case RECEIVE_GOODSLIST:
return Object.assign({},state,{
isFetching:false,
goodsList:state.goodsList.concat(action.goodsList)
});
default:
return state;
}
};


3.根组件:引入​​reducer.js​

import React,{useReducer} from 'react';
import {fetchReducer} from '..';

//创建并export上下文
export const FetchesContext = React.createContext(null);

function RootComponent() {
//第二个参数为state的初始状态
const [fetchesState, fetchDispatch] = useReducer(fetchReducer, {
isFetching: false,
goodsList: []
});
return (
//将dispatch方法和状态都作为context传递给子组件
<FetchesContext.Provider value={{fetchesState,dispatch:fetchDispatch}}>
//...
//用到context的一个子组件
<ComponentToUseContext/>
</FetchesContext.Provider>
)
}


4.子组件:引入​​FetchesContext​

import {FetchesContext} from "../RootComponent";
import React, {useContext, useEffect,useState} from 'react';
import axios from 'axios';

function GoodsList() {

//获取上下文
const ctx = useContext(FetchesContext);

//一个判断是否重新获取的state变量
const [reFetch,setReFetch]=useState(false);

//具有异步调用副作用的useEffect
useEffect(() => {
//首先分发一个开始异步获取数据的action
ctx.dispatch(requestGoodsList());
axios.get(proxyGoodsListAPI())
.then(res=>{
//获取到数据后分发一个action,通知reducer更新状态
ctx.dispatch(receiveGoodsList(res.data))
})
//第二个参数reFetch指的是只有当reFetch变量值改变才重新渲染
},[reFetch]);

return (
<div onScroll={handleScroll}>
{
//children
}
</div>
)
}


完整代码参见:​​branch:hooks-onlineShop​

目录结构

我的目录结构大概这样:

src
|- actions
|- fetchAction.js
|- components
|-...
|- reducers
|- fetchReducer.js
|- index.js


注意点

  1. 使用​​useContext()​​时候我们不需要使用​​Consumer​​了。但不要忘记​​export​​和​​import​​上下文对象
  2. useEffect()可以看做是class写法的componentDidMountcomponentDidUpdate以及componentWillUnMount三个钩子函数的组合。
  • 当返回了一个函数的时候,这个函数就在​​compnentWillUnMount​​生命周期调用
  • 默认地,传给useEffect的第一个参数会在每次(包含第一次)数据更新时重新调用
  • 当给​​useEffect()​​传入了第二个参数(数组类型)的时候,effect函数会在第一次渲染时调用,其余仅当数组中的任一元素发生改变时才会调用。这相当于我们控制了组件的update生命周期
  • ​useEffect()​​第二个数组为空则意味着仅在​​componentDidMount​​周期执行一次
  1. 代码仓库里使用了​​Mock.js​​拦截api请求以及​​ant-design​​第三UI方库。目前代码比较简陋。