npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@eficy/reactive-async

v1.1.0

Published

A reactive async request library with ahooks useRequest compatible API, powered by @eficy/reactive

Readme

@eficy/reactive-async

一个基于 @eficy/reactive 的响应式异步请求库,提供与 ahooks useRequest 完全兼容的 API。

特性

🔄 响应式状态管理 - 基于 @eficy/reactive 的细粒度响应式更新
高性能 - 智能缓存和批量更新优化
🎯 完全兼容 - 与 ahooks useRequest 和 useAntdTable API 完全兼容
🛡️ TypeScript - 完整的 TypeScript 类型支持
🔧 功能丰富 - 支持轮询、防抖、节流、重试、缓存、表格分页等高级特性
📦 轻量级 - 基于 @preact/signals 的轻量级实现
📋 表格支持 - 内置 Ant Design Table 集成,支持分页、排序、筛选、搜索
🌍 组件外使用 - signals 可以在组件外独立使用,提供更灵活的状态管理

安装

npm install @eficy/reactive-async
# 或
yarn add @eficy/reactive-async
# 或
pnpm add @eficy/reactive-async

JSX Import Source 配置

为了在 JSX 中使用 signals,需要配置 JSX Import Source:

在文件顶部添加注释

/** @jsxImportSource eficy */

import { asyncSignal } from '@eficy/reactive-async';

const userService = (userId) => fetch(`/api/user/${userId}`).then(res => res.json());

// signals 在组件外定义和使用
const userSignal = asyncSignal(() => userService('123'));

function UserProfile() {
  return (
    <div>
      <h1>用户信息</h1>
      {userSignal.loading() && <div>加载中...</div>}
      {userSignal.error() && <div>错误: {userSignal.error().message}</div>}
      {userSignal.data() && <div>用户名: {userSignal.data().name}</div>}
      <button onClick={() => userSignal.refresh()}>刷新</button>
    </div>
  );
}

TypeScript 配置

tsconfig.json 中配置:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "eficy"
  }
}

Vite 配置

// vite.config.js
export default {
  esbuild: {
    jsxImportSource: 'eficy'
  }
}

基础用法

自动请求

/** @jsxImportSource eficy */

import { asyncSignal } from '@eficy/reactive-async';

// 定义服务函数
const getUserInfo = (userId) => {
  return fetch(`/api/user/${userId}`).then(res => res.json());
};

// 在组件外定义 signal - 这是关键特性
const userDataSignal = asyncSignal(() => getUserInfo('123'));

function UserCard() {
  return (
    <div className="user-card">
      {userDataSignal.loading() && <div>加载中...</div>}
      {userDataSignal.error() && (
        <div className="error">错误: {userDataSignal.error().message}</div>
      )}
      {userDataSignal.data() && (
        <div>
          <h2>{userDataSignal.data().name}</h2>
          <p>{userDataSignal.data().email}</p>
        </div>
      )}
      <button onClick={() => userDataSignal.refresh()}>刷新</button>
    </div>
  );
}

手动请求

/** @jsxImportSource eficy */

import { asyncSignal } from '@eficy/reactive-async';

const createUser = (userData) => {
  return fetch('/api/user', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(userData),
  }).then(res => res.json());
};

// 在组件外定义手动触发的 signal
const createUserSignal = asyncSignal(createUser, {
  manual: true,
  onSuccess: (result) => {
    console.log('用户创建成功:', result);
  },
  onError: (error) => {
    console.error('创建失败:', error);
  },
});

function CreateUserForm() {
  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const userData = {
      name: formData.get('name'),
      email: formData.get('email'),
    };
    createUserSignal.run(userData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="姓名" required />
      <input name="email" type="email" placeholder="邮箱" required />
      <button 
        type="submit" 
        disabled={createUserSignal.loading()}
      >
        {createUserSignal.loading() ? '创建中...' : '创建用户'}
      </button>
      
      {createUserSignal.error() && (
        <div className="error">
          创建失败: {createUserSignal.error().message}
        </div>
      )}
      
      {createUserSignal.data() && (
        <div className="success">
          用户创建成功: {createUserSignal.data().name}
        </div>
      )}
    </form>
  );
}

组件外使用 - 核心特性

signals 的最大优势是可以在组件外定义和使用,实现真正的状态共享:

/** @jsxImportSource eficy */

import { asyncSignal } from '@eficy/reactive-async';

// ============ 在组件外定义 signals ============
const userService = (userId) => fetch(`/api/user/${userId}`).then(res => res.json());
const todosService = (userId) => fetch(`/api/user/${userId}/todos`).then(res => res.json());

// 全局用户状态
const userSignal = asyncSignal(() => userService(getCurrentUserId()), {
  staleTime: 5 * 60 * 1000, // 5分钟内认为数据新鲜
  cacheKey: 'current-user'
});

// 依赖用户数据的待办事项
const todosSignal = asyncSignal(() => {
  const user = userSignal.data();
  if (!user) return Promise.resolve([]);
  return todosService(user.id);
}, {
  refreshDeps: [userSignal.data] // 当用户数据变化时自动刷新
});

// ============ 多个组件可以共享同一个 signal ============

function Header() {
  return (
    <header>
      <div>
        欢迎, {userSignal.data()?.name || '游客'}
        {userSignal.loading() && <span> (加载中...)</span>}
      </div>
      <button onClick={() => userSignal.refresh()}>刷新用户信息</button>
    </header>
  );
}

function Profile() {
  return (
    <div className="profile">
      <h2>个人资料</h2>
      {userSignal.loading() && <div>加载用户信息中...</div>}
      {userSignal.error() && <div>加载失败: {userSignal.error().message}</div>}
      {userSignal.data() && (
        <div>
          <p>姓名: {userSignal.data().name}</p>
          <p>邮箱: {userSignal.data().email}</p>
          <p>注册时间: {userSignal.data().createdAt}</p>
        </div>
      )}
    </div>
  );
}

function TodoList() {
  return (
    <div className="todos">
      <h2>待办事项</h2>
      {todosSignal.loading() && <div>加载待办事项中...</div>}
      {todosSignal.error() && <div>加载失败: {todosSignal.error().message}</div>}
      {todosSignal.data() && (
        <ul>
          {todosSignal.data().map(todo => (
            <li key={todo.id}>{todo.title}</li>
          ))}
        </ul>
      )}
      <button onClick={() => todosSignal.refresh()}>刷新待办</button>
    </div>
  );
}

// ============ 在任何地方都可以访问和操作 signals ============

// 在事件处理函数中
function handleLogout() {
  userSignal.mutate(null); // 清除用户数据
  todosSignal.mutate([]); // 清除待办数据
}

// 在工具函数中
function getCurrentUser() {
  return userSignal.data();
}

// 在异步函数中
async function refreshAllUserData() {
  await userSignal.refresh();
  await todosSignal.refresh();
}

function getCurrentUserId() {
  return localStorage.getItem('userId') || '1';
}

API 参考

asyncSignal

const { data, loading, error, run, refresh, cancel, mutate } = asyncSignal(service, options);

参数

  • service: (...args: TParams) => Promise<TData> - 异步服务函数
  • options: AsyncSignalOptions<TData, TParams> - 配置选项

返回值

| 属性 | 类型 | 描述 | | -------- | ---------------------------------------- | -------------------- | | data | Signal<TData \| undefined> | 响应数据 signal | | loading | Signal<boolean> | 加载状态 signal | | error | Signal<Error \| undefined> | 错误信息 signal | | run | (...params: TParams) => Promise<TData> | 手动触发请求 | | refresh | () => Promise<TData> | 使用上次参数重新请求 | | cancel | () => void | 取消当前请求 | | mutate | (data) => void | 修改数据 |

配置选项

| 选项 | 类型 | 默认值 | 描述 | | ------------- | --------------------------------- | ------- | ------------ | | manual | boolean | false | 是否手动触发 | | defaultParams | TParams | - | 默认参数 | | initialData | TData | - | 初始数据 | | onBefore | (params) => void | - | 请求前回调 | | onSuccess | (data, params) => void | - | 成功回调 | | onError | (error, params) => void | - | 失败回调 | | onFinally | (params, data?, error?) => void | - | 完成回调 | | formatResult | (response) => TData | - | 格式化结果 |

高级特性

轮询

/** @jsxImportSource eficy */

import { asyncSignal } from '@eficy/reactive-async';

const getStatus = () => fetch('/api/status').then(res => res.json());

const statusSignal = asyncSignal(getStatus, {
  pollingInterval: 1000, // 每秒轮询一次
});

function StatusMonitor() {
  return (
    <div>
      <h3>系统状态监控</h3>
      {statusSignal.loading() && <div>检查状态中...</div>}
      <div>系统状态: {statusSignal.data()?.status || '未知'}</div>
      <div>最后更新: {new Date().toLocaleTimeString()}</div>
    </div>
  );
}

防抖搜索

/** @jsxImportSource eficy */

import { asyncSignal } from '@eficy/reactive-async';

const searchUsers = (keyword) => {
  return fetch(`/api/users?q=${keyword}`).then(res => res.json());
};

const searchSignal = asyncSignal(searchUsers, {
  manual: true,
  debounceWait: 300, // 300ms 防抖
});

function SearchUsers() {
  const handleSearch = (e) => {
    const keyword = e.target.value.trim();
    if (keyword) {
      searchSignal.run(keyword);
    }
  };

  return (
    <div>
      <input 
        type="text" 
        placeholder="搜索用户..." 
        onChange={handleSearch}
      />
      
      {searchSignal.loading() && <div>搜索中...</div>}
      {searchSignal.error() && <div>搜索失败: {searchSignal.error().message}</div>}
      
      <div className="search-results">
        {searchSignal.data()?.map(user => (
          <div key={user.id} className="user-item">
            {user.name}
          </div>
        ))}
      </div>
    </div>
  );
}

缓存和智能刷新

/** @jsxImportSource eficy */

import { asyncSignal } from '@eficy/reactive-async';

const getUserInfo = (userId) => {
  return fetch(`/api/user/${userId}`).then(res => res.json());
};

const userInfoSignal = asyncSignal(() => getUserInfo(currentUserId), {
  cacheKey: 'user-info', // 缓存键
  cacheTime: 60000, // 缓存1分钟
  staleTime: 30000, // 30秒内认为数据新鲜
  refreshOnWindowFocus: true, // 窗口重新获得焦点时刷新
});

function UserInfo() {
  return (
    <div>
      <h2>用户信息</h2>
      {userInfoSignal.loading() && <div>加载中...</div>}
      {userInfoSignal.error() && <div>加载失败</div>}
      {userInfoSignal.data() && (
        <div>
          <p>姓名: {userInfoSignal.data().name}</p>
          <p>最后登录: {userInfoSignal.data().lastLogin}</p>
        </div>
      )}
    </div>
  );
}

依赖刷新

/** @jsxImportSource eficy */

import { asyncSignal } from '@eficy/reactive-async';
import { signal } from '@eficy/reactive';

// 用户ID state
const userIdSignal = signal('1');

const getUserInfo = (userId) => {
  return fetch(`/api/user/${userId}`).then(res => res.json());
};

// 当 userId 变化时自动重新请求
const userInfoSignal = asyncSignal(() => getUserInfo(userIdSignal.value), {
  refreshDeps: [userIdSignal.value], // 依赖 userId
});

function UserSelector() {
  return (
    <div>
      <select 
        value={userIdSignal.value} 
        onChange={e => userIdSignal.value = e.target.value}
      >
        <option value="1">用户 1</option>
        <option value="2">用户 2</option>
        <option value="3">用户 3</option>
      </select>
      
      <div className="user-info">
        {userInfoSignal.loading() && <div>加载用户信息中...</div>}
        {userInfoSignal.error() && <div>加载失败: {userInfoSignal.error().message}</div>}
        {userInfoSignal.data() && (
          <div>
            <h3>{userInfoSignal.data().name}</h3>
            <p>{userInfoSignal.data().email}</p>
          </div>
        )}
      </div>
    </div>
  );
}

antdTableSignal

专为 Ant Design Table 组件设计的响应式表格数据管理工具,提供与 ahooks useAntdTable 完全兼容的 API。

基础用法

/** @jsxImportSource eficy */

import { antdTableSignal } from '@eficy/reactive-async';
import { Table } from 'antd';

// 定义数据类型
interface UserData {
  id: number;
  name: string;
  email: string;
  status: 'active' | 'inactive';
}

// 定义 API 服务函数
const getUserList = async (
  { current, pageSize, sorter, filters },
  formData?
): Promise<{ total: number; list: UserData[] }> => {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      page: current,
      size: pageSize,
      sortField: sorter?.field,
      sortOrder: sorter?.order,
      ...formData,
      ...filters,
    }),
  });
  
  return response.json();
};

// 在组件外定义表格 signal
const userTableSignal = antdTableSignal(getUserList, {
  defaultPageSize: 10,
  onSuccess: (data) => {
    console.log(`加载了 ${data.list.length} 条数据,总共 ${data.total} 条`);
  }
});

function UserTable() {
  const columns = [
    {
      title: 'ID',
      dataIndex: 'id',
      key: 'id',
      sorter: true,
    },
    {
      title: '姓名',
      dataIndex: 'name',
      key: 'name',
      sorter: true,
    },
    {
      title: '邮箱',
      dataIndex: 'email',
      key: 'email',
    },
    {
      title: '状态',
      dataIndex: 'status',
      key: 'status',
      filters: [
        { text: '激活', value: 'active' },
        { text: '禁用', value: 'inactive' },
      ],
    },
  ];

  return (
    <Table
      {...userTableSignal.tableProps}
      columns={columns}
      rowKey="id"
    />
  );
}

带搜索表单的用法

/** @jsxImportSource eficy */

import { antdTableSignal } from '@eficy/reactive-async';
import { Form, Input, Button, Table, Card } from 'antd';

const [form] = Form.useForm();

const userTableWithSearchSignal = antdTableSignal(getUserList, {
  form,
  defaultType: 'simple',
});

function UserTableWithSearch() {
  // 简单搜索表单
  const renderSimpleForm = () => (
    <Form form={form} layout="inline">
      <Form.Item name="name" label="姓名">
        <Input placeholder="请输入姓名" />
      </Form.Item>
      <Form.Item>
        <Button type="primary" onClick={userTableWithSearchSignal.search.submit}>
          搜索
        </Button>
        <Button onClick={userTableWithSearchSignal.search.reset} style={{ marginLeft: 8 }}>
          重置
        </Button>
        <Button type="link" onClick={userTableWithSearchSignal.search.changeType}>
          高级搜索
        </Button>
      </Form.Item>
    </Form>
  );

  return (
    <Card>
      {userTableWithSearchSignal.search.type() === 'simple' ? renderSimpleForm() : null}
      <Table 
        {...userTableWithSearchSignal.tableProps} 
        columns={columns} 
        rowKey="id" 
      />
    </Card>
  );
}

API 参考

antdTableSignal

const { tableProps, search, loading, error, refresh, mutate } = antdTableSignal(service, options);
参数
  • service: (params: AntdTableParams, formData?: any) => Promise<{ total: number; list: TData[] }> - 异步服务函数
  • options: AntdTableSignalOptions<TData> - 配置选项
返回值

| 属性 | 类型 | 描述 | | ---------- | ---------------------------------------------- | -------------- | | tableProps | { dataSource, loading, onChange, pagination } | 表格属性 | | search | { type, changeType, submit, reset } | 搜索控制 | | loading | Signal<boolean> | 加载状态 | | error | Signal<Error \| undefined> | 错误信息 | | refresh | () => Promise<any> | 刷新数据 | | mutate | (data) => void | 修改数据 |

迁移指南

从 ahooks useRequest 迁移

本库与 ahooks useRequest 100% API 兼容,只需要:

  1. 替换导入
  2. 添加 JSX Import Source
  3. 在组件外定义 signals(推荐)
// 之前
import { useRequest } from 'ahooks';

function MyComponent() {
  const { data, loading, run } = useRequest(fetchData, options);
  // ...
}

// 现在
/** @jsxImportSource eficy */
import { asyncSignal } from '@eficy/reactive-async';

// 在组件外定义(推荐)
const dataSignal = asyncSignal(fetchData, options);

function MyComponent() {
  // 直接使用 signals
  return (
    <div>
      {dataSignal.loading() && <div>加载中...</div>}
      {dataSignal.data() && <div>{dataSignal.data().title}</div>}
      <button onClick={() => dataSignal.run()}>刷新</button>
    </div>
  );
}

从 ahooks useAntdTable 迁移

// 之前
import { useAntdTable } from 'ahooks';

function TableComponent() {
  const { tableProps, search, loading, error, refresh } = useAntdTable(service, options);
  // ...
}

// 现在
/** @jsxImportSource eficy */
import { antdTableSignal } from '@eficy/reactive-async';

// 在组件外定义
const tableSignal = antdTableSignal(service, options);

function TableComponent() {
  return (
    <Table {...tableSignal.tableProps} columns={columns} rowKey="id" />
  );
}

核心优势

1. 组件外使用

  • signals 可以在组件外定义和使用
  • 实现真正的全局状态管理
  • 多个组件可以共享同一个 signal
  • 在任何地方都可以访问和操作数据

2. 细粒度响应式

  • 基于 @preact/signals 的细粒度更新
  • 只有依赖变化的组件会重新渲染
  • 避免不必要的性能开销

3. 完全兼容

  • 与 ahooks API 100% 兼容
  • 平滑迁移,学习成本低
  • 支持所有高级特性

4. 现代化开发体验

  • TypeScript 原生支持
  • JSX Import Source 配置
  • 声明式的响应式编程

许可证

MIT