Table of Contents

1、概述

在上一个章节中,我们学习了如何基于 useEffect 来请求异步数据。

在真实的案例实践中,由于接口请求是一个高频的行为,因此,我们通常会单独封装一个自定义 hook 来处理接口请求

2、改造上一章的例子

我们先来一个简单的实现,直接改造上一章的例子,看代码

预览
index.tsx
useQuery.ts
api.ts
message.tsx
01
import Skeleton from 'components/zmui/skeleton'
02
import Message from './message'
03
import useQuery from './useQuery'
04
05
export default function Demo04() {
06
const { content, loading, setLoading } = useQuery()
07
08
return (
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 文件更加简洁

3、封装更通用的自定义 hook

但是我们在封装一个方法时,一定要考虑多个场景之下的通用性,因此,在封装 useQuery 时,就必须要按照封装的思维来进一步优化

  1. 思考多个场景下的差异,并将差异点作为参数传递给自定义 hook
  2. 将通用逻辑封装到自定义 hook 中

那么,我们要考虑的不同点包括

  1. 请求的 API 是不同
  2. 请求的返回结果的数据类型是不同的
  3. 状态的默认值可能是不同的

因此,综合考虑之下,我们可以将 useQuery 改造为,代码中使用了泛型作为类型约束,如果你对泛型还不是很理解,可以直接无视即可

useQuery.ts
01
import { useEffect, useState } from 'react'
02
03
export default function useQuery<T>(api: () => Promise<T>, initialValue: T) {
04
const [content, update] = useState<T>(initialValue)
05
const [loading, setLoading] = useState(true)
06
07
useEffect(() => {
08
if (!loading) return;
09
api().then(res => {
10
update(res)
11
setLoading(false)
12
})
13
}, [loading]);
14
15
return { content, loading, setLoading }
16
}

在使用时,如果我们的 api 已经做好了类型约束,那么此时所有的类型都可以自动推导,没有 ts 痕迹

完整代码与演示如下所示

预览
index.tsx
useQuery.ts
api.ts
message.tsx
01
import Skeleton from 'components/zmui/skeleton'
02
import Message from './message'
03
import useQuery from './useQuery'
04
import { getMessage } from './api'
05
06
export default function Demo04() {
07
const { content, loading, setLoading } = useQuery(getMessage, { value: '' })
08
09
return (
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

4、考虑接口请求的参数

在实际的开发中,接口请求的参数可能是不同的,因此,我们还要进一步优化 useQuery ,使其可以接收参数

此时,useQuery 的入参比较多了,因此,我们可以让入参聚合成为一个 options ,改造之后暂时约定的格式如下

useQuery.ts
1
interface UseQueryOptions<T> {
2
initialValue: T,
3
params: Record<string, any>,
4
// 是否手动触发请求,默认是自动触发
5
manual?: boolean
6
}

完整代码与演示如下所示

预览
index.tsx
useQuery.ts
api.ts
message.tsx
01
import Skeleton from 'components/zmui/skeleton'
02
import Message from './message'
03
import useQuery from './useQuery'
04
import { getMessage } from './api'
05
06
export default function Demo04() {
07
const { content, loading, run } = useQuery(getMessage, {
08
initialValue: { value: '' },
09
params: [1, 'active']
10
})
11
12
function __handler() {
13
run(2, 'inactive')
14
}
15
16
return (
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

5、总结

封装到这里,我们这个用于请求的自定义 hook 实际上已经趋于成熟了,直接运用到实践项目中,可以满足大部分的需求

不过,要涵盖所有的需求,有可能还会根据需求扩展更复杂的逻辑,例如还需要考虑

  1. 请求竞态问题
  2. 请求缓存问题
  3. 请求取消问题
  4. 请求重试与失败处理

我们可以直接利用成熟的三方工具如 useSwr/react-query/ahooks 等,来直接获得这样的能力,也可以自己尝试逐步处理,这不在这一章我们讨论的范围以内

由于官方文档中,一直在提倡尽可能的避免使用 useEffect,因此,上面的 useQuery 还可以做一些优化与调整,把接口请求的核心逻辑,直接放到 run 里面,然后仅在初始化时轻量的使用一下 useEffect

这样做的好处就是,在重新请求时,我们可以更早的触发请求,而无需等待 useEffect 的执行

useQuery.ts
01
import { useEffect, useState, useRef } from 'react'
02
03
interface UseQueryOptions<T> {
04
initialValue: T,
05
params?: any[],
06
// 是否手动触发请求,默认是自动触发
07
manual?: boolean
08
}
09
10
export default function useQuery<T>(api: (...args: any[]) => Promise<T>, options: UseQueryOptions<T>) {
11
const { initialValue, params = [], manual = false } = options;
12
const [content, update] = useState<T>(initialValue)
13
const [loading, setLoading] = useState(!manual)
14
// 额外添加一个状态用来存储错误信息
15
const [error, setError] = useState('')
16
17
useEffect(() => {
18
if (!loading) return;
19
run(...params)
20
}, []);
21
22
function run(...args: any[]) {
23
setLoading(true)
24
// 如果传入了参数,则更新参数,如果没有传入参数,则使用默认参数
25
const _parameter = args.length > 0 ? args : params
26
27
api(..._parameter).then(res => {
28
update(res)
29
setLoading(false)
30
}).catch(err => {
31
setError(err)
32
setLoading(false)
33
})
34
}
35
36
return { content, loading, error, run }
37
}
专栏首页
到顶
专栏目录