<blockquote>本文是学习了2018年新鲜出炉的React Hooks提案之后,针对<strong>异步请求数据</strong>写的一个案例。注意,本文假设了:<br>1.你已经初步了解<code>hooks</code>的含义了,如果不了解还请移步<a href="https://reactjs.org/docs/hooks-intro.html" rel="nofollow noreferrer">官方文档</a>。(其实有过翻译的想法,不过印记中文一直在翻译,就是比较慢啦)<br>2.你使用<code>Redux</code>实现过异步<code>Action</code>(非必需,只是本文不涉及该部分知识而直接使用)<br>3.你听说过<code>axios</code>或者<code>fetch</code>(如果没有,那么想象一下原生js的<code>promise</code>实现异步请求,或者去学习下这俩库)<br>全部代码参见仓库: <a href="https://github.com/Marckon/React-MPA/tree/master/pages/online-shop" rel="nofollow noreferrer">github | Marckon</a>选择<a href="https://github.com/Marckon/React-MPA/tree/hooks-onlineShop/pages/online-shop" rel="nofollow noreferrer"><code>hooks-onlineShop</code></a>分支以及<code>master</code>分支查看</blockquote> <p>❗ <em><strong>本文并非最佳实践,如有更好的方法或发现文中纰漏,欢迎指正!</strong></em></p> <h1>前序方案(不想看可以直接跳过)</h1> <ul><li><strong>不考虑引入<code>Redux</code></strong></li></ul> <p>通过学习<a href="https://segmentfault.com/a/1190000017175195"><code>React</code>生命周期</a>,我们知道适合进行异步请求的地方是<code>componentDidMount</code>钩子函数内。因此,当你不需要考虑状态管理时,以往的方法很简单:</p>
1class App extends React.Component{
2 componentDidMount(){
3 axios.get('/your/api')
4 .then(res=>/*...*/)
5 }
6}
<ul><li><strong>引入<code>Redux</code>进行状态管理</strong></li></ul> <p>当你决定使用<code>Redux</code>进行状态管理时,比如将异步获取到的数据储存在<code>store</code>中,事情就开始复杂起来了。根据<code>Redux</code>的官方文档案例来看,为了实现异步<code>action</code>,你还得需要一个类似于<code>redux-thunk</code>的第三方库来解析你的异步<code>action</code>。</p> <p><em>requestAction.js:</em> 定义异步请求action的地方</p>
1//这是一个异步action,分发了两个同步action,redux-thunk能够理解它
2const fetchGoodsList = url => dispatch => {
3 dispatch(requestGoodsList());
4 axios.get(url)
5 .then(res=>{
6 dispatch(receiveGoodsList(res.data))
7 })
8};
9
<p><em>requestReducer.js:</em> 处理同步action</p>
1const requestReducer=(state=initialState,action)=>{
2 switch (action.type) {
3 case REQUEST_GOODSLIST:
4 return Object.assign({},state,{
5 isFetching: true
6 });
7 case RECEIVE_GOODSLIST:
8 return Object.assign({},state,{
9 isFetching:false,
10 goodsList:action.goodsList
11 });
12 default:
13 return state;
14 }
15};
<p><em>App Component</em> :你引入redux store和redux-thunk中间件的地方</p>
1import {Provider} from 'react-redux';
2import thunkMiddleWare from 'redux-thunk';
3import {createStore,applyMiddleware} from 'redux';
4//other imports
5
6let store=createStore(
7 rootReducer,
8 //这里要使用中间件,才能够完成异步请求
9 applyMiddleware(
10 thunkMiddleWare,
11 myMiddleWare,
12
13 )
14);
15class App extends React.Component{
16 render(){
17 return (
18 <Provider store={store}>
19 <RootComponent/>
20 </Provider>
21 )
22 }
23}
<p><em>GoodsList Component</em> :需要进行异步请求的组件</p>
1class GoodsList extends React.Component{
2 //...
3 componentDidMount(){
4 this.props.fetchGoodsList('your/url');
5 }
6 //...
7}
8const mapDispatchToProps={
9 fetchGoodsList
10}
11export default connect(
12 mapStateToProps,
13 mapDispatchToProps
14)(GoodsList);
<p>完整代码:<a href="https://github.com/Marckon/React-MPA/tree/master/pages/online-shop" rel="nofollow noreferrer">branch:master-onlineShop</a></p> <h1>使用<code>Hooks</code>-<code>useReducer()</code>和<code>useContext()</code> </h1> <p>总之使用<code>Redux</code>很累,当然,你可以不使用Redux,直接通过<code>props</code>层层传递,或者使用<code>context</code>都可以。只不过本文我们学过了<code>useReducer</code>,使用到了<code>Redux</code>的思想,总要试着用一下。</p> <p><strong>这里你不需要引入别的任何第三方库了,简简单单地使用<code>React@16.7.0-alpha.2</code>版本就好啦</strong></p> <p>很重要的一点就是——<strong>函数式组件</strong>,现在React推荐我们这么做,可以基本上代替<code>class</code>写法。</p> <h2>函数签名</h2> <ol> <li><code>useReducer(reducer,initialState)</code></li> <li><code>useContext(ctxObj)</code></li> <li><code>useEffect(effectFunction,\[dependencyValues\])</code></li> </ol> <h2>概览-你需要编写什么</h2> <ol> <li> <p><strong>action.js:</strong></p> <ul><li>我们还使用<code>redux</code>的思想,编写action</li></ul> </li> <li> <p><strong>reducer.js:</strong></p> <ul><li>处理action,不同于<code>redux</code>的<code>reducer</code>,这里我们可以不用提供初始状态</li></ul> </li> <li> <p><strong>根组件:</strong></p> <ul> <li> <code>Provider</code>提供给子组件<code>context</code> </li> <li> <code>useReducer</code>定义的位置,引入一个<code>reducer</code>并且提供初始状态<code>initialState</code> </li> </ul> </li> <li> <p><strong>子组件:</strong></p> <ul> <li> <code>useContext</code>定义的位置,获取祖先组件提供的<code>context</code> </li> <li> <code>useEffect</code>用于进行异步请求</li> </ul> </li> </ol> <h2>实现</h2> <h3>1.action.js:我们使用action创建函数</h3>
1const REQUEST_GOODSLIST = "REQUEST_GOODSLIST";
2const RECEIVE_GOODSLIST = "RECEIVE_GOODSLIST";
3
4//开始请求
5const requestGoodsList = () => ({
6 type: REQUEST_GOODSLIST
7});
8
9//接收到数据
10const receiveGoodsList = json => ({
11 type: RECEIVE_GOODSLIST,
12 goodsList: json.goodsList,
13 receivedAt: Date.now()
14});
15
16export {
17 RECEIVE_GOODSLIST,
18 REQUEST_GOODSLIST,
19 receiveGoodsList,
20 requestGoodsList,
21}
<h3>2.reducer.js:判断action的类型并进行相应处理,更新<code>state</code> </h3>
1import {
2 RECEIVE_GOODSLIST,
3 REQUEST_GOODSLIST,
4} from "../..";
5
6
7export const fetchReducer=(state,action)=>{
8 switch (action.type) {
9 case REQUEST_GOODSLIST:
10 return Object.assign({},state,{
11 isFetching: true
12 });
13 case RECEIVE_GOODSLIST:
14 return Object.assign({},state,{
15 isFetching:false,
16 goodsList:state.goodsList.concat(action.goodsList)
17 });
18 default:
19 return state;
20 }
21};
22
23
<h3>3.根组件:引入<code>reducer.js</code> </h3>
1import React,{useReducer} from 'react';
2import {fetchReducer} from '..';
3
4//创建并export上下文
5export const FetchesContext = React.createContext(null);
6
7function RootComponent() {
8 //第二个参数为state的初始状态
9 const [fetchesState, fetchDispatch] = useReducer(fetchReducer, {
10 isFetching: false,
11 goodsList: []
12 });
13 return (
14 //将dispatch方法和状态都作为context传递给子组件
15 <FetchesContext.Provider value={{fetchesState,dispatch:fetchDispatch}}>
16 //...
17 //用到context的一个子组件
18 <ComponentToUseContext/>
19 </FetchesContext.Provider>
20 )
21}
<h3>4.子组件:引入<code>FetchesContext</code> </h3>
1import {FetchesContext} from "../RootComponent";
2import React, {useContext, useEffect,useState} from 'react';
3import axios from 'axios';
4
5function GoodsList() {
6
7 //获取上下文
8 const ctx = useContext(FetchesContext);
9
10 //一个判断是否重新获取的state变量
11 const [reFetch,setReFetch]=useState(false);
12
13 //具有异步调用副作用的useEffect
14 useEffect(() => {
15 //首先分发一个开始异步获取数据的action
16 ctx.dispatch(requestGoodsList());
17 axios.get(proxyGoodsListAPI())
18 .then(res=>{
19 //获取到数据后分发一个action,通知reducer更新状态
20 ctx.dispatch(receiveGoodsList(res.data))
21 })
22 //第二个参数reFetch指的是只有当reFetch变量值改变才重新渲染
23 },[reFetch]);
24
25 return (
26 <div onScroll={handleScroll}>
27 {
28 //children
29 }
30 </div>
31 )
32}
33
<p>完整代码参见:<a href="https://github.com/Marckon/React-MPA/tree/hooks-onlineShop/pages/online-shop" rel="nofollow noreferrer">branch:hooks-onlineShop</a></p> <h3>目录结构</h3> <p>我的目录结构大概这样:</p>
1src
2 |- actions
3 |- fetchAction.js
4 |- components
5 |-...
6 |- reducers
7 |- fetchReducer.js
8 |- index.js
<h2>注意点</h2> <ol> <li>使用<code>useContext()</code>时候我们不需要使用<code>Consumer</code>了。但不要忘记<code>export</code>和<code>import</code>上下文对象</li> <li> <p><code>useEffect()</code>可以看做是<code>class</code>写法的<code>componentDidMount</code>、<code>componentDidUpdate</code>以及<code>componentWillUnMount</code>三个钩子函数的组合。</p> <ul> <li>当返回了一个函数的时候,这个函数就在<code>compnentWillUnMount</code>生命周期调用</li> <li>默认地,传给useEffect的第一个参数会在每次(包含第一次)数据更新时重新调用</li> <li>当给<code>useEffect()</code>传入了第二个参数(数组类型)的时候,effect函数会在第一次渲染时调用,其余仅当数组中的<strong>任一</strong>元素发生改变时才会调用。<strong>这相当于我们控制了组件的<code>update</code>生命周期</strong> </li> <li> <code>useEffect()</code>第二个数组为空则意味着仅在<code>componentDidMount</code>周期执行一次</li> </ul> </li> <li>代码仓库里使用了<code>Mock.js</code>拦截api请求以及<code>ant-design</code>第三UI方库。目前代码比较简陋。</li> </ol> 来源:https://segmentfault.com/a/1190000017209855