ahooks
一套高质量可靠的 React Hooks 库
ahooks 函数处理规范
所有输入函数永远使用最新的(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() }, [] ) }
|
所有的输出函数地址都不会变化 (useMemo/useCallback/useMemoizedFn)
举个例子,假设有个自定义 hook useToggle
,其代码如下:
1 2 3 4 5 6 7 8 9 10 11 12
| 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
| function useToggle<T, D>(left: T, right: D): [D | T, () => void] { const [state, setState] = useState<T | D>(left)
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'
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
| 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 { 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)) { ... } }
|