ahooks

一套高质量可靠的 React Hooks 库

ahooks 函数处理规范

  1. 所有输入函数永远使用最新的(useRef 缓存)

    针对输入函数,通过 useRef 做一次记录,以保证在任何地方都能访问到最新的函数。

1
2
const fnRef = useRef(fn)
fnRef.current = fn

​ 比如 useUmount 代码如下:

1
2
3
4
5
6
7
8
9
10
11
const useUnmount = (fn) => {
const fnRef = useRef(fn)
fnRef.current = fn

useEffect(
() => () => {
fnRef.current()
},
[]
)
}
  1. 所有的输出函数地址都不会变化 (useMemo/useCallback/useMemoizedFn)

    举个例子,假设有个自定义 hook useToggle ,其代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 自定义hook useToggle
    function useToggle<T, D>(left: T, right: D): [D | T, () => void] {
    const [state, setState] = useState<T | D>(left)
    /**
    * 地址改变
    */
    const toggle = () => {
    setState((pre) => (pre === left ? right : left))
    }

    return [state, toggle]
    }

    只要 state 发生变化,那么 toggle 函数地址就会改变,显然不是那么能接受的。

    改造一下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 自定义hook useToggle
    function useToggle<T, D>(left: T, right: D): [D | T, () => void] {
    const [state, setState] = useState<T | D>(left)

    /**
    * 缓存地址 useMemoizedFn
    */
    // const toggle = useMemoizedFn(() => {
    // setState((pre) => (pre === left ? right : left))
    // })

    /**
    * 缓存地址 useCallback (无依赖)
    */
    const toggle = useCallback(() => {
    setState((pre) => (pre === left ? right : left))
    }, [])

    return [state, toggle]
    }

    现在无论 state 怎么改变,渲染几次,toggle 函数的地址都不会改变。so easy !

useRequest

useRequest 是一个强大的异步数据管理的 Hooks

useRequest 通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括:

  • 自动请求/手动请求
  • 轮询
  • 防抖
  • 节流
  • 屏幕聚焦重新请求
  • 错误重试
  • loading delay
  • SWR(stale-while-revalidate)
  • 缓存

接下来让我们先从两个最简单的例子认识 useRequest

  • useRequest 的第一个参数是一个异步函数,在组件初次加载时,会自动触发该函数执行。同时自动管理该异步函数的 loading , data , error 等状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useRequest } from 'ahooks'
import Mock from 'mockjs'
import React from 'react'

function getUsername(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(Mock.mock('@name'))
}, 1000)
})
}

export default () => {
const { data, error, loading } = useRequest(getUsername)

if (error) {
return <div>failed to load</div>
}
if (loading) {
return <div>loading...</div>
}
return <div>Username: {data}</div>
}
  • 如果设置了 options.manual = true,则 useRequest 不会默认执行,需要通过 run 来触发执行。
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
import { message } from 'antd'
import React, { useState } from 'react'
import { useRequest } from 'ahooks'

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function changeUsername(username: string): Promise<{ success: boolean }> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ success: true })
}, 1000)
})
}

export default () => {
const [state, setState] = useState('')

const { loading, run } = useRequest(changeUsername, {
manual: true,
onSuccess: (result, params) => {
if (result.success) {
setState('')
message.success(`The username was changed to "${params[0]}" !`)
}
},
})

return (
<div>
<input
onChange={(e) => setState(e.target.value)}
value={state}
placeholder="Please enter username"
style={{ width: 240, marginRight: 16 }}
/>
<button disabled={loading} type="button" onClick={() => run(state)}>
{loading ? 'Loading' : 'Edit'}
</button>
</div>
)
}

理解插件式

useRequest 入口文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function useRequest<TData, TParams extends any[]>(
service: Service<TData, TParams>,
options?: Options<TData, TParams>,
plugins?: Plugin<TData, TParams>[]
) {
return useRequestImplement<TData, TParams>(service, options, [
...(plugins || []),
useDebouncePlugin,
useLoadingDelayPlugin,
usePollingPlugin,
useRefreshOnWindowFocusPlugin,
useThrottlePlugin,
useAutoRunPlugin,
useCachePlugin,
useRetryPlugin,
] as Plugin<TData, TParams>[])
}

其中 useRequestImplement 函数批量注入插件,初始化插件

1
2
// run all plugins hooks
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions))

useRequestImplement 函数使用 useCreate hook 实现 Fetch 的单例模式 (为什么不用 useCallback/useMemo?):

1
2
3
4
5
6
7
8
9
10
11
12
const fetchInstance = useCreation(() => {
const initState = plugins
.map((p) => p?.onInit?.(fetchOptions))
.filter(Boolean)

return new Fetch<TData, TParams>(
serviceRef,
fetchOptions,
update,
Object.assign({}, ...initState)
)
}, [])

往下关注 Fetch 类,这个 Fetch.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
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
64
65
export default class Fetch<TData, TParams extends any[]> {
pluginImpls: PluginReturn<TData, TParams>[];

count: number = 0;
// 维护的状态
state: FetchState<TData, TParams> = {
loading: false,
params: undefined,
data: undefined,
error: undefined,
};

constructor(
public serviceRef: MutableRefObject<Service<TData, TParams>>,
public options: Options<TData, TParams>,
public subscribe: Subscribe,
public initState: Partial<FetchState<TData, TParams>> = {},
) {
// 初始化状态
this.state = {
...this.state,
loading: !options.manual,
...initState,
};
}

...

runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
return Object.assign({}, ...r);
}

async runAsync(...params: TParams): Promise<TData> {
...
try {
// replace service => useCachePlugin
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);

if (!servicePromise) {
servicePromise = this.serviceRef.current(...params);
}
}
}

run(...params: TParams) {
...
}

cancel() {
...
}

refresh() {
...
}

refreshAsync() {
...
}

mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
...
}
}