在上篇文章我们讲了如何使用 React 的 Suspense 组件和 lazy 方法来实现模块的懒加载,后面还讲了如何使用
React 的 useState 方法来实现自定义的 Hooks,从而达到复用的目的。
我们知道,不管在做什么样的前端项目,列表页肯定是存在的,那如何获取列表的数据呢?大部分情况下我们都是在每个模块内部自己实现一个获取数据的方法,然后调用 setState 来更新数据。那有没有更好的方式可以做到这些,并且能够在一个项目中处处复用这个功能呢?答案就是使用 React Hooks。
useEffect 介绍
简单的说,useEffect 就是在组件挂载完成或者更新完成的时候,需要执行的一系列操作,这些操作可能是 ajax 请求,dom 操作,事件处理等等。
官方文档里面有句话说的是,useEffect 是 componentDidMount, componentDidUpdate, 和 componentWillUnmount 三个生命周期钩子的组合,那就是之前分别在这三个地方干的事情,现在可以统一在一个地方干了,是不是很方便?。为了保证文章简洁,这里不过多介绍,有需要可以参考官方文档
useReducer 介绍
如果你用过 redux,那你应该知道 redux 就是通过 reducer 来处理 dispatch 出来的各种 action 的。每个 reducer 都是一个纯函数,处理完成之后,返回新的 state,然后触发 React 的更新。官方文档
自定义一个获取数据的 React Hooks
先来分析下,我们要从服务器获取数据,需要做哪些事情。
- 构造请求参数
- 发送请求
- 解析返回结果或异常处理
- 展示结果或异常错误提示
从上面的几个点我们可以分析出,我们的自定义 Hook 要能够传入请求人 url 以及请求的参数,在请求失败的时候能够有后台返回的提示信息,在请求成功的时候能够返回后台返回的数据,我们还需要知道请求是否失败。
这里我们将 action 拆分为 3 个:
- FETCH_INIT // 开始加载数据,用来展示 Loading 状态
- FETCH_SUCCESS // 加载数据成功,用来展示数据
- FETCH_ERROR // 加载数据失败,用来展示错误信息
基于上面的分析,我们先定义一个 reducer,用来处理每个 action。
reducer.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
export const dataFetchReducer = (state: any, action: {[type: string]: any}) => { switch(action.type) { case 'FETCH_INIT': return { ...state, isLoading: true, isError: false } case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, data: action.payload } case 'FETCH_ERROR': return { ...state, isLoading: false, isError: true, msg: action.payload } default: throw new Error(`Unsupport action type:${action.type}`); } } |
上面的 reducer 非常简单,就是处理上面我们定义的三个 action,然后每次都返回一个新的 state,解释下上面返回的 state 的各个字段的用意:
- isLoading: 是否展示加载中的提示,比如我们请求正在处理,那需要有一个提示信息给到用户
- isError: 请求是否失败,如果这个值为 true 的话,那页面就要展示 msg 中的错误提示信息
- msg: 错误提示信息,当 isError 为 true 的时候不为空
介绍了 reducer 之后,我们来看下 Hook 是什么实现的:
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
interface RequestConfig extends AxiosRequestConfig { url: string } export const useDataApi = (initData: Array<any> | any, initRequestConfig: RequestConfig) => { if (!initRequestConfig.method) { initRequestConfig.method = 'get'; } const [requestConfig, setRequestConfig] = useState(initRequestConfig); const [state, dispatch] = useReducer(dataFetchReducer, { data: initData, isLoading: false, isError: false }); useEffect(() => { const fetchData = async () => { try{ dispatch({ type: 'FETCH_INIT' }); if (!requestConfig.url) { dispatch({ type: 'FETCH_SUCCESS', payload: [] }); }else { const response = await axios(requestConfig).catch(e => { return e.response; }); if (response.data){ const data = response.data; const { success, result, message } = data; if (!success) { dispatch({ type: 'FETCH_ERROR', payload: message }); } else { dispatch({ type: 'FETCH_SUCCESS', payload: result }); } }else { dispatch({ type: 'FETCH_ERROR', msg: '加载数据失败' }); } } }catch(e) { dispatch({ type: 'FETCH_ERROR', msg: '加载失败' }); } }; fetchData(); }, [requestConfig]); return [state, setRequestConfig]; } |
在上面的代码中,我们定义了一个自定义的 Hooks,名称为 useDataApi,这个 Hooks 有 2 个参数,第一个参数是表示初始化时候的数据,第二个参数就是我们请求需要用到的各种参数了。
上面的 useState 我们就不多介绍了,主要来介绍下,下面代码中的 useReducer。
1 2 3 4 5 |
const [state, dispatch] = useReducer(dataFetchReducer, { data: initData, isLoading: false, isError: false }); |
这里 useReducer 的第一个参数,就是传入我们刚刚定义好的 reducer,那个 reducer 里面我们处理了 3 中类型的 action,对不对?
第二个参数就是传入一个初始化的 satte 对象了。
然后看下这里的返回值,他是一个数组,为什么要返回一个数组呢?
因为这样通过解构之后,你可以随意命名他们的两个返回值(一本正经)。
在上面的两个返回值中,第一个 state 是我们渲染的时候需要用到的,第二个 dispatch 用来分发 action 的,这个 dispatch 分发的 actio 你,就会被我们自定义的 reducer 去处理。
然后再来看下下面的 useEffect 的代码,这里代码比较多,我们一点点看:
1 2 3 |
dispatch({ type: 'FETCH_INIT' }); |
上面的代码主要用来分发一个 FETCH_INIT
的 action,这个时候就回返回 isLoading 为 true 的 state,组件根据这个来展示 loading 或者加载中... 等提示。
1 2 3 |
const response = await axios(requestConfig).catch(e => { return e.response; }); |
这段代码就是具体的发送请求的代码,这里我使用了 axios 这个库,当然你也可以替换成别的库。
在请求完成之后,需要解析返回的结果,然后来决定是触发 FETCH_SUCCESS
还是触发 FETCH_ERROR
action.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const data = response.data; const { success, result, message } = data; if (!success) { dispatch({ type: 'FETCH_ERROR', payload: message }); } else { dispatch({ type: 'FETCH_SUCCESS', payload: result }); } |
这段代码,我们获取 response 的 data,然后解析 data 的数据,这里 data 就是后台返回的数据结构了,我这里后台返回的格式就是会包括这三个字段,success 标识请求是否成功,result 标识请求的结果,message 表示提示信息,一般用来返回错误的提示信息。
到这里我们自定义的 Hooks 就算完成了,然后我们来看下怎么使用这个 Hooks 来加载数据。
使用自定义的 Hooks 来加载数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
const [{ isLoading, isError, msg, data }, setRequestConfig] = useDataApi([], { url: apiPath }); if (isError) { message.error(msg); } return ( <div> <div className="toolbar"> <Link to={window.location.pathname + "/detail"}> <Button type="primary" icon='plus'> 添加 </Button> </Link> </div> <CustomModal /> <Table columns={columns} dataSource={data} loading={isLoading} rowKey={'id'} /> </div> ); |
看上面的代码,是不是足够简单了?而且你可以再任何地方使用这个自定义的 Hooks。
总结
本篇文章主要讲了以下几个点:
- useEffect 和 useReducer 简单介绍
- 实现自定义的获取数据的 Hooks
上面的内容有任何不对的地方,欢迎扶正!
。
楚门的世界 2019 年 8 月 28 日
我理解错了,能不能撤回评论
楚门的世界 2019 年 8 月 28 日
你上面例子的自定义 hooks 不会提示 Hooks can only be called inside of the body a function component 吗?
Charles 2019 年 7 月 23 日
谢谢分享!这种项目中整理出来的比那些实例有用太多了!
DI 2019 年 7 月 15 日
这个系列大概会有几篇呢?
TAT. zhongzhong 2019 年 7 月 17 日
不确定,主要是项目里面实际用到的,就会抽时间写一篇