react-use 中的 useAsync:用 Hooks 优雅处理异步函数与 Promise 状态
【免费下载链接】react-useReact Hooks — 👍项目地址: https://gitcode.com/gh_mirrors/re/react-use
导读
useAsync是 react-use 提供的一个副作用类(Side effects)Hook,用于在 React 函数组件中解析一个async函数或任何返回 Promise 的函数,并自动管理loading、error、value三种异步状态。它把"发起请求 → 等待加载 → 成功取值 / 失败取错"这一整套异步生命周期封装为声明式状态,让你在组件里只需关注渲染逻辑,而不用手写useState+useEffect的样板代码。读完本文,你将掌握useAsync的完整 API、源码内部的执行原理、依赖变更时的重新求值机制,以及它与useAsyncFn、useAsyncRetry的搭配用法。
核心用法:让异步结果成为组件状态
useAsync接收一个返回 Promise 的函数,并在函数执行期间自动更新返回的状态对象。官方文档给出的典型用法如下:
import {useAsync} from 'react-use'; const Demo = ({url}) => { const state = useAsync(async () => { const response = await fetch(url); const result = await response.text(); return result }, [url]); return ( <div> {state.loading ? <div>Loading...</div> : state.error ? <div>Error: {state.error.message}</div> : <div>Value: {state.value}</div> } </div> ); };从 useAsync 的官方文档 可以看出,这个 Hook 的核心价值在于:你只需要提供"做什么"(async 函数)和"何时重做"(依赖数组),剩下的状态管理全部交给 Hook 完成。在上面的例子中,当url变化时,useAsync会自动重新发起fetch请求,并把最新结果同步到state中供渲染层消费。
API 签名与返回结构
文档中的参考签名非常简洁:
useAsync(fn, args?: any[]);结合源码 src/useAsync.ts 可以得出更精确的类型定义:
useAsync<T extends FunctionReturningPromise>( fn: T, deps: DependencyList = [] )其中:
fn:一个async函数或返回 Promise 的函数,类型为FunctionReturningPromise(即(...args: any[]) => Promise<any>,定义见 src/misc/types.ts)。deps:依赖数组(DependencyList),默认值为[]。当依赖变化时,Hook 会重新执行fn。
返回值是一个AsyncState<T>状态对象,其类型定义在 src/useAsyncFn.ts 中,属于可辨识联合(discriminated union),按loading标志划分出四种互斥形态:
| 状态形态 | loading | error | value |
|---|---|---|---|
| 初始/加载中 | true | 可为Error(尚未完成) | 可为旧值 |
| 失败 | false | Error | undefined |
| 成功 | false | undefined | T(Promise 解析值) |
由于PromiseType(见 src/misc/types.ts)会从 Promise 中推导出解析值类型,因此state.value具备完整的类型提示。在 JSX 中你可以直接通过state.loading/state.error/state.value三个字段分支渲染,文档示例中的三元嵌套正是这一联合类型在渲染层的典型应用。
源码原理:useAsync 是如何工作的
useAsync本身实现非常精简,只有约 20 行,它本质上是useAsyncFn的一层"自动执行"封装。完整实现如下(src/useAsync.ts):
export default function useAsync<T extends FunctionReturningPromise>( fn: T, deps: DependencyList = [] ) { const [state, callback] = useAsyncFn(fn, deps, { loading: true, }); useEffect(() => { callback(); }, [callback]); return state; }其中有两个关键设计:
- 初始状态为
loading: true:它把initialState设置为{ loading: true },这意味着首次挂载时组件就处于加载态,符合"请求已发起、结果未返回"的真实语义。这也是测试中initially starts loading断言成立的原因(见 tests/useAsync.test.tsx)。 - 通过
useEffect自动触发:挂载后立即调用callback()执行异步函数,并把callback作为依赖。由于callback由useCallback(fn, deps)创建(见 src/useAsyncFn.ts),当deps变化时callback引用更新,useEffect会重新执行,从而自动重新发起异步任务。
底层状态机:useAsyncFn 的执行细节
useAsync的所有异步逻辑都委托给useAsyncFn(src/useAsyncFn.ts),其核心机制包括:
- 并发安全(callId 守卫):内部用
lastCallId引用计数,每次调用callback时callId自增;Promise 完成回调里只有callId === lastCallId.current的"最新一次调用"才允许写入状态(src/useAsyncFn.ts)。这保证了快速连续触发时,过期请求的结果不会覆盖新请求,防止竞态条件。 - 卸载安全(isMounted 守卫):借助
useMountedState(src/useMountedState.ts)跟踪组件挂载状态,组件卸载后异步回调不会再去setState,避免 React 的"对已卸载组件更新状态"警告。 - 成功与失败分流:
.then的成功回调写入{ value, loading: false },失败回调写入{ error, loading: false }。注意失败分支把error原样存入状态,因此state.error可能是 Error 实例,也可能是任意 rejected 值(测试中 reject 了一个字符串'yay',见 tests/useAsync.test.tsx)。
这些实现细节共同保证了:即使在高频依赖变更、组件提前卸载、异步任务交错等场景下,useAsync返回的状态始终与"当前最新一次请求"保持一致。
依赖变更与重新求值
useAsync的第二个参数deps是重新执行的触发器。官方文档的[url]用法已经体现了"URL 变化 → 重新请求"的语义。从测试用例可以更清楚地看到两种触发方式(tests/useAsync.test.tsx):
- fn 本身作为依赖:传入新的函数引用会触发重新执行,且
callCount会递增,证明新请求确实被发起。 - 额外依赖变化:测试中通过
useCallback(() => fn(counter), [counter])把外部状态counter织入回调,counter从 0 变为 1 后,useAsync自动用新参数重新执行并返回'counter is 1 and callCount is 2'。
因此在实际项目中,凡是异步函数内部读取的外部值,都应列入deps数组;同时建议像测试那样结合useCallback/useMemo稳定函数引用,避免因函数每次渲染都变化而导致不必要的重复请求。
搭配使用:useAsyncFn 与 useAsyncRetry
围绕useAsync,react-use 还提供了两个高度关联的 Hook,适合不同的场景(均从 src/index.ts 统一导出):
useAsyncFn:useAsync的"手动版"。它返回[state, callback]元组,异步执行完全由你掌控(例如绑定到表单提交、按钮点击事件),且支持向fn传递任意参数。useAsync正是useAsyncFn(fn, deps, { loading: true })加上自动调用的组合。useAsyncRetry:在useAsync之上增加retry()方法(见 src/useAsyncRetry.ts),通过递增attempt计数并把其并入依赖数组来触发重新执行,非常适合"加载失败后点击重试"的场景。其官方文档示例(docs/useAsyncRetry.md)展示了!loading && <button onClick={() => state.retry()}>Start loading</button>的用法;实现中还在开发环境下对"加载中调用 retry"给出 no-op 提示,避免重复触发。
实践建议与注意事项
- 渲染分支要覆盖三种状态:
loading、error、value是互斥且完备的,官方示例的三元嵌套或 Storybook 示例(见 stories/useAsync.story.tsx)都值得直接借鉴;必要时可以像 Story 那样用JSON.stringify(state)调试完整状态。 - 泛型参数可显式指定:如
useAsync<string>(...),可以约束state.value的类型,让state.error.message、state.value的访问都获得类型保护。 - 注意依赖稳定性:
fn若在渲染期间每次重建,请用useCallback包裹或将其作为依赖传入,以控制请求频率。 - 失败时 error 的类型:
state.error的类型是Error | undefined,但实际运行时 rejected 值可能不是 Error 实例(如测试中的字符串),生产代码建议对error做容错处理。
总结
useAsync用不到 20 行源码,把"执行 Promise + 管理三态 + 依赖重跑 + 防竞态 + 防泄漏"这一整套异步状态管理收拢为声明式 API,是 react-use 中副作用类 Hook 的典型代表。掌握它之后,再遇到useAsyncFn(手动触发)与useAsyncRetry(带重试)时,就能理解它们只是"自动执行"与"重试增强"两个方向上的变体。无论是数据拉取、接口轮询还是任何 Promise 驱动的场景,useAsync都能让组件代码更简洁、更可控。
【免费下载链接】react-useReact Hooks — 👍项目地址: https://gitcode.com/gh_mirrors/re/react-use
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考