react-use 中的 useAsync:用 Hooks 优雅处理异步函数与 Promise 状态
2026/9/18 23:39:23 网站建设 项目流程

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 的函数,并自动管理loadingerrorvalue三种异步状态。它把"发起请求 → 等待加载 → 成功取值 / 失败取错"这一整套异步生命周期封装为声明式状态,让你在组件里只需关注渲染逻辑,而不用手写useState+useEffect的样板代码。读完本文,你将掌握useAsync的完整 API、源码内部的执行原理、依赖变更时的重新求值机制,以及它与useAsyncFnuseAsyncRetry的搭配用法。

核心用法:让异步结果成为组件状态

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标志划分出四种互斥形态:

状态形态loadingerrorvalue
初始/加载中true可为Error(尚未完成)可为旧值
失败falseErrorundefined
成功falseundefinedT(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; }

其中有两个关键设计:

  1. 初始状态为loading: true:它把initialState设置为{ loading: true },这意味着首次挂载时组件就处于加载态,符合"请求已发起、结果未返回"的真实语义。这也是测试中initially starts loading断言成立的原因(见 tests/useAsync.test.tsx)。
  2. 通过useEffect自动触发:挂载后立即调用callback()执行异步函数,并把callback作为依赖。由于callbackuseCallback(fn, deps)创建(见 src/useAsyncFn.ts),当deps变化时callback引用更新,useEffect会重新执行,从而自动重新发起异步任务。

底层状态机:useAsyncFn 的执行细节

useAsync的所有异步逻辑都委托给useAsyncFn(src/useAsyncFn.ts),其核心机制包括:

  • 并发安全(callId 守卫):内部用lastCallId引用计数,每次调用callbackcallId自增;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 统一导出):

  • useAsyncFnuseAsync的"手动版"。它返回[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 提示,避免重复触发。

实践建议与注意事项

  • 渲染分支要覆盖三种状态loadingerrorvalue是互斥且完备的,官方示例的三元嵌套或 Storybook 示例(见 stories/useAsync.story.tsx)都值得直接借鉴;必要时可以像 Story 那样用JSON.stringify(state)调试完整状态。
  • 泛型参数可显式指定:如useAsync<string>(...),可以约束state.value的类型,让state.error.messagestate.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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询