在上一个章节中,我们学习了如何基于 useEffect 来请求异步数据。
在真实的案例实践中,由于接口请求是一个高频的行为,因此,我们通常会单独封装一个自定义 hook 来处理接口请求
我们先来一个简单的实现,直接改造上一章的例子,看代码
01import Skeleton from 'components/zmui/skeleton'02import Message from './message'03import useQuery from './useQuery'0405export default function Demo04() {06const { content, loading, setLoading } = useQuery()0708return (09<div className='p-4'>10<div className='text-right mb-4'>11<button className='button' onClick={() => setLoading(!loading)}>更新数据</button>12</div>13{loading ? <Skeleton type='header' /> : <Message message={content.value} />}14</div>15)16}17
在这个案例中,我们把请求数据的行为都放到了 useQuery 中,如果我们把他看成是一个公共的工具方法,那么这样做就避免在组件中重复编写请求数据的逻辑,从而使得 index.tsx 文件更加简洁
但是我们在封装一个方法时,一定要考虑多个场景之下的通用性,因此,在封装 useQuery 时,就必须要按照封装的思维来进一步优化
那么,我们要考虑的不同点包括
因此,综合考虑之下,我们可以将 useQuery 改造为,代码中使用了泛型作为类型约束,如果你对泛型还不是很理解,可以直接无视即可
01import { useEffect, useState } from 'react'0203export default function useQuery<T>(api: () => Promise<T>, initialValue: T) {04const [content, update] = useState<T>(initialValue)05const [loading, setLoading] = useState(true)0607useEffect(() => {08if (!loading) return;09api().then(res => {10update(res)11setLoading(false)12})13}, [loading]);1415return { content, loading, setLoading }16}
在使用时,如果我们的 api 已经做好了类型约束,那么此时所有的类型都可以自动推导,没有 ts 痕迹
完整代码与演示如下所示
01import Skeleton from 'components/zmui/skeleton'02import Message from './message'03import useQuery from './useQuery'04import { getMessage } from './api'0506export default function Demo04() {07const { content, loading, setLoading } = useQuery(getMessage, { value: '' })0809return (10<div className='p-4'>11<div className='text-right mb-4'>12<button className='button' onClick={() => setLoading(!loading)}>更新数据</button>13</div>14{loading ? <Skeleton type='header' /> : <Message message={content.value} />}15</div>16)17}18
在实际的开发中,接口请求的参数可能是不同的,因此,我们还要进一步优化 useQuery ,使其可以接收参数
此时,useQuery 的入参比较多了,因此,我们可以让入参聚合成为一个 options ,改造之后暂时约定的格式如下
1interface UseQueryOptions<T> {2initialValue: T,3params: Record<string, any>,4// 是否手动触发请求,默认是自动触发5manual?: boolean6}
完整代码与演示如下所示
01import Skeleton from 'components/zmui/skeleton'02import Message from './message'03import useQuery from './useQuery'04import { getMessage } from './api'0506export default function Demo04() {07const { content, loading, run } = useQuery(getMessage, {08initialValue: { value: '' },09params: [1, 'active']10})1112function __handler() {13run(2, 'inactive')14}1516return (17<div className='p-4'>18<div className='text-right mb-4'>19<button className='button' onClick={__handler}>更新数据</button>20</div>21{loading ? <Skeleton type='header' /> : <Message message={content.value} />}22</div>23)24}25
封装到这里,我们这个用于请求的自定义 hook 实际上已经趋于成熟了,直接运用到实践项目中,可以满足大部分的需求
不过,要涵盖所有的需求,有可能还会根据需求扩展更复杂的逻辑,例如还需要考虑
我们可以直接利用成熟的三方工具如 useSwr/react-query/ahooks 等,来直接获得这样的能力,也可以自己尝试逐步处理,这不在这一章我们讨论的范围以内
由于官方文档中,一直在提倡尽可能的避免使用 useEffect,因此,上面的 useQuery 还可以做一些优化与调整,把接口请求的核心逻辑,直接放到 run 里面,然后仅在初始化时轻量的使用一下 useEffect
这样做的好处就是,在重新请求时,我们可以更早的触发请求,而无需等待 useEffect 的执行
01import { useEffect, useState, useRef } from 'react'0203interface UseQueryOptions<T> {04initialValue: T,05params?: any[],06// 是否手动触发请求,默认是自动触发07manual?: boolean08}0910export default function useQuery<T>(api: (...args: any[]) => Promise<T>, options: UseQueryOptions<T>) {11const { initialValue, params = [], manual = false } = options;12const [content, update] = useState<T>(initialValue)13const [loading, setLoading] = useState(!manual)14// 额外添加一个状态用来存储错误信息15const [error, setError] = useState('')1617useEffect(() => {18if (!loading) return;19run(...params)20}, []);2122function run(...args: any[]) {23setLoading(true)24// 如果传入了参数,则更新参数,如果没有传入参数,则使用默认参数25const _parameter = args.length > 0 ? args : params2627api(..._parameter).then(res => {28update(res)29setLoading(false)30}).catch(err => {31setError(err)32setLoading(false)33})34}3536return { content, loading, error, run }37}