[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [[\"@babel/env\"], \"@babel/react\"],\n  \"plugins\": [\"@babel/plugin-transform-runtime\"]\n}\n"
  },
  {
    "path": ".coveralls.yml",
    "content": "service_name: travis-pro\nrepo_token: dO6l2UfWXIPwCzK3lmEUKQyQsfzXZUZml\n"
  },
  {
    "path": ".cursor/rules/demo.mdc",
    "content": "---\ndescription:\nglobs: components/*/demo/**\nalwaysApply: false\n---\n\n# Demo 规范\n\n- demo 代码尽可能简洁\n- 避免冗余代码，方便用户复制到项目直接使用\n- 每个 demo 聚焦展示一个功能点\n- 提供中英文两个版本的说明\n- demo 文件命名：\n  - 英文 demo: index.en-US.md\n  - 中文 demo: index.zh-CN.md\n- 确保 demo 在各种尺寸下都能正常展示\n- 对于复杂交互提供必要的操作说明\n\n## 文件组织\n\n- 每个组件演示包含 `.md`（说明文档）和 `.ts`（实际代码）两部分\n- 位置：hooks 目录下的 `src` 子目录，如 `packages/hooks/src/useHover`\n- 文件名应简洁地描述示例内容\n\n## MD 文档规范\n\n- 必须包含 `## zh-CN` 和 `## en-US` 两种语言说明\n- 内容简洁明了，突出组件特性和用法\n- 避免冗长段落，必要时使用列表或粗体\n- 标注注意事项和实验性功能\n\n## 代码质量\n\n- 实用且专注于单一功能\n- 关键处添加简洁注释\n- 使用有意义的数据和变量\n- 优先使用 ahooks 内置 hook 或者公共方法，减少外部依赖\n\n## 质量要求\n\n- 确保代码运行正常，无控制台错误\n- 适配常见浏览器\n- 避免过时 API，及时更新到新推荐用法\n"
  },
  {
    "path": ".cursor/rules/docs.mdc",
    "content": "---\ndescription: 规范项目文档和 Changelog\nglobs: [\"**/CHANGELOG*.md\", \"components/**/index.*.md\"]\nalwaysApply: false\n---\n\n# Changelog Emoji 规范\n\n- 🐞 Bug 修复\n- 💄 样式更新或 token 更新\n- 🆕 新增特性，新增属性\n- 🔥 极其值得关注的新增特性\n- 🇺🇸🇨🇳🇬🇧 国际化改动\n- 📖 📝 文档或网站改进\n- ✅ 新增或更新测试用例\n- 🛎 更新警告/提示信息\n- ⌨️ ♿ 可访问性增强\n- 🗑 废弃或移除\n- 🛠 重构或工具链优化\n- ⚡️ 性能提升\n\n# 文档规范\n\n- 提供中英文两个版本\n- 新属性需声明可用的版本号\n- 属性命名符合 API 命名规则\n- hook 文档包含：使用场景、基础用法、API 说明\n- 文档示例应简洁明了\n- 属性的描述应清晰易懂\n- 对复杂功能提供详细说明\n- 加入 TypeScript 定义\n- 提供常见问题解答\n- 更新文档时同步更新中英文版本\n\n## 其他要求\n\n- 新增属性时，建议用易于理解的语言描述用户可以感知的变化\n- 存在破坏性改动时，尽量给出原始的 PR 链接，社区提交的 PR 改动加上提交者的链接\n"
  },
  {
    "path": ".cursor/rules/git.mdc",
    "content": "---\ndescription:\nglobs:\nalwaysApply: true\n---\n# Git 规范\n\n## 开发流程\n\n1. 从保护分支（通常是 `master`）创建新的功能分支\n2. 在新分支上进行开发\n3. 提交 Pull Request 到目标分支\n4. 等待 Code Review 和 CI 通过\n5. 合并到目标分支\n\n## 分支命名规范\n\n- 功能开发：`feat/description-of-feature`\n  - 例如：`feat/add-dark-mode`\n  - 例如：`feat/improve-table-performance`\n- 问题修复：`fix/issue-number-or-description`\n  - 例如：`fix/button-style-issue`\n  - 例如：`fix/issue-1234`\n- 文档更新：`docs/what-is-changed`\n  - 例如：`docs/update-api-reference`\n  - 例如：`docs/fix-typos`\n- 代码重构：`refactor/what-is-changed`\n  - 例如：`refactor/button-component`\n  - 例如：`refactor/remove-deprecated-api`\n- 样式修改：`style/what-is-changed`\n  - 例如：`style/update-button-tokens`\n  - 例如：`style/improve-mobile-layout`\n- 测试相关：`test/what-is-changed`\n  - 例如：`test/add-button-test`\n  - 例如：`test/improve-coverage`\n- 构建相关：`build/what-is-changed`\n  - 例如：`build/upgrade-webpack`\n  - 例如：`build/fix-ts-config`\n- 持续集成：`ci/what-is-changed`\n  - 例如：`ci/add-e2e-test`\n  - 例如：`ci/fix-deploy-script`\n- 性能优化：`perf/what-is-changed`\n  - 例如：`perf/optimize-render`\n  - 例如：`perf/reduce-bundle-size`\n- 依赖升级：`deps/package-name-version`\n  - 例如：`deps/upgrade-react-19`\n  - 例如：`deps/update-dependencies`\n\n## 分支命名注意事项\n\n1. 使用小写字母\n2. 使用连字符（-）分隔单词\n3. 简短但具有描述性\n4. 避免使用下划线或其他特殊字符\n5. 如果与 Issue 关联，可以包含 Issue 编号\n\n## Pull Request 规范\n\n### PR 标题\n\n- PR 标题始终使用英文\n- 遵循格式：`类型: 简短描述`\n- 例如：`fix: fix button style issues in Safari browser`\n- 例如：`feat: add dark mode support`\n\n### PR 内容\n\n- PR 内容默认使用英文\n- 尽量简洁清晰地描述改动内容和目的\n- 可以视需要在英文描述后附上中文说明\n\n### PR 提交注意事项\n\n1. **审核流程**：\n\n   - PR 需要由至少一名维护者审核通过后才能合并\n   - 确保所有 CI 检查都通过\n   - 解决所有 Code Review 中提出的问题\n\n2. **PR 质量要求**：\n\n   - 确保代码符合项目代码风格\n   - 添加必要的测试用例\n   - 更新相关文档\n   - 大型改动需要更详细的说明和更多的审核者参与\n\n3. **工具标注**：\n   - 如果是用 Cursor 提交的代码，请在 PR body 末尾进行标注：`> Submitted by Cursor`\n\n## 新增内容\n\n- Pull Request 标题格式：[组件名]: 描述\n- 从 master 分支创建新分支\n- 分支命名规范：\n  - feature/xxx：新特性\n  - fix/xxx：Bug 修复\n  - docs/xxx：文档更新\n- PR 说明中选择改动类型：\n  - 🆕 新特性提交\n  - 🐞 Bug 修复\n  - 📝 文档改进\n  - 📽️ 演示代码改进\n  - 💄 样式/交互改进\n  - 🤖 TypeScript 更新\n  - 📦 包体积优化\n  - ⚡️ 性能优化\n  - 🌐 国际化改进\n- 提供改动背景和解决方案\n- 更新日志同时提供英文和中文版本\n"
  },
  {
    "path": ".cursor/rules/project.mdc",
    "content": "---\ndescription:\nglobs:\nalwaysApply: true\n---\n # 项目背景\n\n这是由蚂蚁团队开发的一个高质量、可靠的 React Hooks 库。\n\n- 易学易用\n- 支持 SSR\n- 对输入输出函数做了特殊处理，避免闭包问题\n- 包含大量提炼自业务的高级 Hooks\n- 包含丰富的基础 Hooks\n- 使用 TypeScript 构建，提供完整的类型定义文件\n\n# 编码规范\n\n- 使用 TypeScript 和 React 书写\n- 避免引入新依赖，严控打包体积\n- 兼容现代浏览器\n- 支持服务端渲染\n- 保持向下兼容，避免 breaking change\n- 合理使用 React.memo、useMemo 和 useCallback 优化性能\n"
  },
  {
    "path": ".cursor/rules/testing.mdc",
    "content": "---\ndescription:\nglobs: **/__tests__/**,**/*.test.tsx,**/*.test.ts\nalwaysApply: false\n---\n # 测试规范\n\n- 使用 vitest 和 @testing-library/react 编写单元测试\n- 测试覆盖率要求 100%\n- 测试文件放在 __tests__ 目录，命名格式为：index.spec.ts 或 xxx.spec.ts\n"
  },
  {
    "path": ".cursor/rules/typescript.mdc",
    "content": "# TypeScript 规范\n\n## 基本原则\n\n- 所有组件和函数必须提供准确的类型定义\n- 尽量避免使用 `any` 类型，尽可能精确地定义类型\n- 使用接口而非类型别名定义对象结构\n- 导出所有公共接口类型，方便用户使用\n- 严格遵循 TypeScript 类型设计原则，确保类型安全\n- 确保编译无任何类型错误或警告\n\n## hook 类型定义\n\n- 复杂的数据结构应拆分为多个接口定义\n- 所有函数类型应明确定义参数和返回值\n\n## 泛型使用\n\n- 适当使用泛型增强类型灵活性\n- 为泛型参数提供合理的默认类型和约束\n- 避免过度使用泛型导致类型复杂化\n- 在泛型参数上应用限制条件（constraints）确保类型安全\n- 为复杂泛型提供类型别名以提高可读性\n\n## 类型合并与扩展\n\n- 使用交叉类型（&）合并多个类型\n- 使用 Partial<T>、Pick<T, K>、Omit<T, K> 等工具类型修改现有类型\n- 扩展原生 DOM 元素属性时，继承相应的内置类型\n- 使用 type 定义联合类型和交叉类型\n- 优先使用自带的工具类型，避免重复定义\n\n## 枚举和常量\n\n- 使用字面量联合类型定义有限的选项集合\n- 为复杂的枚举值提供类型守卫函数\n- 避免使用 `enum`，优先使用联合类型和 `as const`\n- 对于关键常量，使用 `as const` 断言确保类型严格\n- 为联合类型中的每个值提供适当的注释\n\n## 类型推断与断言\n\n- 尽可能依赖 TypeScript 的类型推断\n- 只在必要时使用类型断言（as）\n- 使用类型守卫函数进行运行时类型检查\n- 尽量避免使用非空断言操作符（!）\n- 使用 `instanceof` 和 `typeof` 进行类型守卫\n- 为自定义类型创建类型谓词（type predicates）函数\n\n## JSDoc 注释\n\n- 为复杂的类型、函数、hook 添加 JSDoc 注释\n- 使用 `@deprecated` 标记已废弃的 API\n- 在注释中提供使用示例\n- 说明参数和返回值的含义与约束\n- 在 interface 和重要类型定义上添加文档注释\n\n## 类型兼容性\n\n- 确保类型定义兼容不同版本的 React\n- 避免使用实验性或不稳定的 TypeScript 特性\n- 为第三方库未提供的类型编写声明文件\n- 使用条件类型处理复杂的类型逻辑\n- 验证类型在不同 TypeScript 版本下的兼容性\n\n## 严格使用 TypeScript 类型\n\n- 导出组件类型和接口\n- 避免使用 any，优先使用 unknown\n- 组件 Props 使用 interface 定义\n- 使用明确的命名约定\n- 合理使用泛型提高复用性\n- 导出类型时使用 export type\n- 组件属性使用 JSDoc 注释说明用途\n"
  },
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[Makefile]\nindent_style = tab"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/pr_cn.md",
    "content": "<!--\n首先，感谢你的贡献！😄\n\n新特性请提交至 master 分支。\n在维护者审核通过后会合并。\n请确保填写以下 pull request 的信息，谢谢！~\n-->\n\n[[English Template / 英文模板](https://github.com/alibaba/hooks/blob/master/.github/PULL_REQUEST_TEMPLATE.md)]\n\n### 🤔 这个变动的性质是？\n\n- [ ] 新特性提交\n- [ ] 日常 bug 修复\n- [ ] 站点、文档改进\n- [ ] 演示代码改进\n- [ ] TypeScript 定义更新\n- [ ] 包体积优化\n- [ ] 性能优化\n- [ ] 功能增强\n- [ ] 国际化改进\n- [ ] 重构\n- [ ] 代码风格优化\n- [ ] 测试用例\n- [ ] 分支合并\n- [ ] 其他改动（是关于什么的改动？）\n\n### 🔗 相关 Issue\n\n<!--\n1. 描述相关需求的来源，如相关的 issue 讨论链接。\n-->\n\n### 💡 需求背景和解决方案\n\n<!--\n1. 要解决的具体问题。\n2. 列出最终的 API 实现和用法。\n3. 涉及UI/交互变动需要有截图或 GIF。\n-->\n\n### 📝 更新日志\n\n<!--\n从用户角度描述具体变化，以及可能的 breaking change 和其他风险。\n-->\n\n| 语言    | 更新描述 |\n| ------- | -------- |\n| 🇺🇸 英文 |          |\n| 🇨🇳 中文 |          |\n\n### ☑️ 请求合并前的自查清单\n\n⚠️ 请自检并全部**勾选全部选项**。⚠️\n\n- [ ] 文档已补充或无须补充\n- [ ] 代码演示已提供或无须提供\n- [ ] TypeScript 定义已补充或无须补充\n- [ ] Changelog 已提供或无须提供\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--\nFirst of all, thank you for your contribution! 😄\n\nNew feature please send a pull request to master branch.\nPull requests will be merged after one of the collaborators approve.\nPlease makes sure that these forms are filled before submitting your pull request, thank you!\n-->\n\n[[中文版模板 / Chinese template](https://github.com/alibaba/hooks/blob/master/.github/PULL_REQUEST_TEMPLATE/pr_cn.md)]\n\n### 🤔 This is a ...\n\n- [ ] New feature\n- [ ] Bug fix\n- [ ] Site / documentation update\n- [ ] Demo update\n- [ ] TypeScript definition update\n- [ ] Bundle size optimization\n- [ ] Performance optimization\n- [ ] Enhancement feature\n- [ ] Internationalization\n- [ ] Refactoring\n- [ ] Code style optimization\n- [ ] Test Case\n- [ ] Branch merge\n- [ ] Other (about what?)\n\n### 🔗 Related issue link\n\n<!--\n1. Describe the source of requirement, like related issue link.\n-->\n\n### 💡 Background and solution\n\n<!--\n1. Describe the problem and the scenario.\n2. GIF or snapshot should be provided if includes UI/interactive modification.\n3. How to fix the problem, and list final API implementation and usage sample if that is a new feature.\n-->\n\n### 📝 Changelog\n\n<!--\nDescribe changes from the user side, and list all potential break changes or other risks.\n--->\n\n| Language   | Changelog |\n| ---------- | --------- |\n| 🇺🇸 English |           |\n| 🇨🇳 Chinese |           |\n\n### ☑️ Self Check before Merge\n\n⚠️ Please check all items below before review. ⚠️\n\n- [ ] Doc is updated/provided or not needed\n- [ ] Demo is updated/provided or not needed\n- [ ] TypeScript definition is updated/provided or not needed\n- [ ] Changelog is provided or not needed\n"
  },
  {
    "path": ".github/workflows/comment-when-needs-more-info.yml",
    "content": "name: Comment When Needs More Info Label Added\n\non:\n  issues:\n    types: [labeled]\n\njobs:\n  create-comment:\n    runs-on: ubuntu-latest\n    if: github.event.label.name == 'needs more info'\n    steps:\n      - name: Create comment\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'create-comment'\n          token: ${{ secrets.GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n          body: |\n            Hi, ${{ github.event.issue.user.login }}.\n\n            It seems that this issue is a bit vague and lacks some necessary information. \n\n            看起来这条 issue 描述得有些模糊，缺少一些必要的信息。\n"
  },
  {
    "path": ".github/workflows/gitleaks.yml",
    "content": "name: gitleaks\n\non: [push, pull_request]\n\njobs:\n  gitleaks:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: wget\n        uses: wei/wget@v1\n        with:\n          args: -O .gitleaks.toml https://raw.githubusercontent.com/ycjcl868/gitleaks/master/.gitleaks.toml\n      - name: gitleaks-action\n        uses: gitleaks/gitleaks-action@v1.6.0\n"
  },
  {
    "path": ".github/workflows/issue-close-require.yml",
    "content": "name: Issue Close Require\n\non:\n  schedule:\n    - cron: '0 0 * * *'\n\njobs:\n  close-issues:\n    runs-on: ubuntu-latest\n    steps:\n      - name: need reproduce\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'close-issues'\n          labels: '🤔 Need Reproduce'\n          inactive-day: 3\n\n      - name: needs more info\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'close-issues'\n          labels: 'needs more info'\n          inactive-day: 3\n          body: |\n            Since the issue was labeled with `needs more info`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.\n            由于该 issue 被标记为需要更多信息，却 3 天未收到回应。现关闭 issue，若有任何问题，可评论回复。\n"
  },
  {
    "path": ".github/workflows/issue-reply.yml",
    "content": "name: Issue Reply\n\non:\n  issues:\n    types: [labeled]\n\njobs:\n  reply-helper:\n    runs-on: ubuntu-latest\n    steps:\n      - name: help wanted\n        if: github.event.label.name == 'help wanted'\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'create-comment'\n          issue-number: ${{ github.event.issue.number }}\n          body: |\n            Hello @${{ github.event.issue.user.login }}. We totally like your proposal/feedback, welcome to [send us a Pull Request](https://help.github.com/en/articles/creating-a-pull-request) for it. Please send your Pull Request to proper branch (feature branch for the new feature, master for bugfix and other changes), fill the [Pull Request Template](https://github.com/alibaba/hooks/blob/master/.github/PULL_REQUEST_TEMPLATE.md) here, provide changelog/TypeScript/documentation/test cases if needed and make sure CI passed, we will review it soon. We appreciate your effort in advance and looking forward to your contribution!\n\n            你好 @${{ github.event.issue.user.login }}，我们完全同意你的提议/反馈，欢迎直接在此仓库 [创建一个 Pull Request](https://help.github.com/en/articles/creating-a-pull-request) 来解决这个问题。请将 Pull Request 发到正确的分支（新特性发到 feature 分支，其他发到 master 分支），务必填写 Pull Request 内的[预设模板](https://github.com/alibaba/hooks/blob/master/.github/PULL_REQUEST_TEMPLATE.md)，提供改动所需相应的 changelog、TypeScript 定义、测试用例、文档等，并确保 CI 通过，我们会尽快进行 Review，提前感谢和期待您的贡献。\n\n            ![giphy](https://user-images.githubusercontent.com/507615/62342668-4735dc00-b51a-11e9-92a7-d46fbb1cc0c7.gif)\n\n      - name: 🤔 Need Reproduce\n        if: github.event.label.name == '🤔 Need Reproduce'\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'create-comment'\n          issue-number: ${{ github.event.issue.number }}\n          body: |\n            Hello @${{ github.event.issue.user.login }}. Please provide a online reproduction by forking this link https://codesandbox.io/s/ok2fe or a minimal GitHub repository. Issues labeled by `Need Reproduce` will be closed if no activities in 3 days.\n\n            你好 @${{ github.event.issue.user.login }}, 我们需要你提供一个在线的重现实例以便于我们帮你排查问题。你可以通过点击 [此处](https://codesandbox.io/s/ok2fe) 创建一个 codesandbox 或者提供一个最小化的 GitHub 仓库。3 天内未跟进此 issue 将会被自动关闭。\n\n            ![](https://gw.alipayobjects.com/zos/antfincdn/y9kwg7DVCd/reproduce.gif)\n"
  },
  {
    "path": ".github/workflows/pkg.pr.new.yml",
    "content": "name: Publish Any Commit\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    types: [opened, synchronize, reopened]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    if: github.repository == 'alibaba/hooks'\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - uses: pnpm/action-setup@v4\n        name: Install pnpm\n        with:\n          run_install: false\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: 'pnpm'\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Build\n        run: pnpm build\n\n      # https://github.com/stackblitz-labs/pkg.pr.new#readme\n      - run: pnpx pkg-pr-new publish './packages/*' --no-template --compact\n"
  },
  {
    "path": ".github/workflows/static.yml",
    "content": "name: Deploy static content to Pages\n\non:\n  # Runs on pushes targeting the default branch\n  push:\n    branches: [\"master\"]\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\npermissions:\n  contents: write\n  pages: write\n  id-token: write\n\n# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.\n# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.\nconcurrency:\n  group: \"pages\"\n  cancel-in-progress: false\n\njobs:\n  deploy:\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n    env:\n      NODE_OPTIONS: --openssl-legacy-provider\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22\n\n      - name: Setup pnpm\n        run: |\n            npm install --global corepack@latest\n            corepack enable\n            corepack prepare pnpm@latest --activate\n            echo \"$(pnpm bin --global)\" >> $GITHUB_PATH\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Cache pnpm dependencies\n        uses: actions/cache@v4\n        with:\n          path: |\n            ~/.pnpm-store\n            node_modules\n          key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-\n\n      - name: Build documentation\n        run: npm run build:doc\n\n      - name: Setup Pages\n        uses: actions/configure-pages@v5\n\n      - name: Deploy to GitHub Pages\n        uses: peaceiris/actions-gh-pages@v4\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          publish_dir: ./dist  # 构建后的静态文件目录\n          force_orphan: true\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test CI\n\non: [push, pull_request]\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - run: pnpm install\n      - run: pnpm run tsc\n  test:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        mode: ['normal', 'strict']\n        node-version: [20, 22]\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Get pnpm store directory\n        id: pnpm-cache\n        run: |\n          echo \"pnpm_cache_dir=$(pnpm store path)\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Setup pnpm cache\n        uses: actions/cache@v4\n        with:\n          path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n\n      - name: pnpm run install, build\n        run: |\n          pnpm run init\n\n      - name: test with react normal mode\n        if: ${{ matrix.mode == 'normal' }}\n        run: |\n          pnpm run test\n\n      - name: test with react strict mode\n        if: ${{ matrix.mode == 'strict' }}\n        run: |\n          pnpm run test:strict\n"
  },
  {
    "path": ".gitignore",
    "content": "dist\nes\nlib\n.docz\nnode_modules\n.history\n.idea\n.vscode\ncoverage\n.doc\n.DS_Store\n.umi\n.umi-production\npage\nlerna-debug.log\ntsconfig.tsbuildinfo\npackages/hooks/README.md\nyarn-error.log\npackage-lock.json\nmetadata.json\n.eslintcache\n"
  },
  {
    "path": ".gitleaks.toml",
    "content": "title = \"gitleaks config\"\n[[rules]]\n\tdescription = \"AWS Manager ID\"\n\tregex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''\n\ttags = [\"key\", \"AWS\"]\n[[rules]]\n\tdescription = \"AWS Secret Key\"\n\tregex = '''(?i)aws(.{0,20})?(?-i)['\\\"][0-9a-zA-Z\\/+]{40}['\\\"]'''\n\ttags = [\"key\", \"AWS\"]\n[[rules]]\n\tdescription = \"AWS MWS key\"\n\tregex = '''amzn\\.mws\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'''\n\ttags = [\"key\", \"AWS\", \"MWS\"]\n[[rules]]\n\tdescription = \"Facebook Secret Key\"\n\tregex = '''(?i)(facebook|fb)(.{0,20})?(?-i)['\\\"][0-9a-f]{32}['\\\"]'''\n\ttags = [\"key\", \"Facebook\"]\n[[rules]]\n\tdescription = \"Facebook Client ID\"\n\tregex = '''(?i)(facebook|fb)(.{0,20})?['\\\"][0-9]{13,17}['\\\"]'''\n\ttags = [\"key\", \"Facebook\"]\n[[rules]]\n\tdescription = \"Twitter Secret Key\"\n\tregex = '''(?i)twitter(.{0,20})?['\\\"][0-9a-z]{35,44}['\\\"]'''\n\ttags = [\"key\", \"Twitter\"]\n[[rules]]\n\tdescription = \"Twitter Client ID\"\n\tregex = '''(?i)twitter(.{0,20})?['\\\"][0-9a-z]{18,25}['\\\"]'''\n\ttags = [\"client\", \"Twitter\"]\n[[rules]]\n\tdescription = \"Github\"\n\tregex = '''(?i)github(.{0,20})?(?-i)['\\\"][0-9a-zA-Z]{35,40}['\\\"]'''\n\ttags = [\"key\", \"Github\"]\n[[rules]]\n\tdescription = \"Github Token\"\n\tregex = '''[0-9a-zA-Z]{35,40}'''\n\ttags = [\"key\", \"Github Token\"]\n[[rules]]\n\tdescription = \"Alibaba\"\n\tregex = '''((alibaba|antfin|aliyun|alipay)(-inc|\\.net)|intranetproxy\\.alipay)'''\n\ttags = [\"key\", \"Alibaba\"]\n[[rules]]\n\tdescription = \"antfin\"\n\tregex = '''(?i)antfin(.{0,20})?(?-i)['\\\"][0-9a-zA-Z]{35,40}['\\\"]'''\n\ttags = [\"key\", \"Antfin\"]\n[[rules]]\n\tdescription = \"LinkedIn Client ID\"\n\tregex = '''(?i)linkedin(.{0,20})?(?-i)['\\\"][0-9a-z]{12}['\\\"]'''\n\ttags = [\"client\", \"LinkedIn\"]\n[[rules]]\n\tdescription = \"LinkedIn Secret Key\"\n\tregex = '''(?i)linkedin(.{0,20})?['\\\"][0-9a-z]{16}['\\\"]'''\n\ttags = [\"secret\", \"LinkedIn\"]\n[[rules]]\n\tdescription = \"Slack\"\n\tregex = '''xox[baprs]-([0-9a-zA-Z]{10,48})?'''\n\ttags = [\"key\", \"Slack\"]\n[[rules]]\n\tdescription = \"Asymmetric Private Key\"\n\tregex = '''-----BEGIN ((EC|PGP|DSA|RSA|OPENSSH) )?PRIVATE KEY( BLOCK)?-----'''\n\ttags = [\"key\", \"AsymmetricPrivateKey\"]\n[[rules]]\n\tdescription = \"Public Key\"\n\tregex = '''ssh-rsa'''\n\ttags = [\"keys\", \"public key\"]\n[[rules]]\n\tdescription = \"Gitlab Key\"\n\tregex = '''privateToken|private-token'''\n\ttags = [\"keys\", \"Gitlab\"]\n[[rules]]\n\tdescription = \"Generic Credential\"\n\tregex = '''(?i)(api_key|apikey|secret)(.{0,20})?['|\"][0-9a-zA-Z]{16,45}['|\"]'''\n\ttags = [\"key\", \"API\", \"generic\"]\n[[rules]]\n\tdescription = \"Google API key\"\n\tregex = '''AIza[0-9A-Za-z\\\\-_]{35}'''\n\ttags = [\"key\", \"Google\"]\n[[rules]]\n\tdescription = \"Heroku API key\"\n\tregex = '''(?i)heroku(.{0,20})?['\"][0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}['\"]'''\n\ttags = [\"key\", \"Heroku\"]\n[[rules]]\n\tdescription = \"MailChimp API key\"\n\tregex = '''(?i)(mailchimp|mc)(.{0,20})?['\"][0-9a-f]{32}-us[0-9]{1,2}['\"]'''\n\ttags = [\"key\", \"Mailchimp\"]\n[[rules]]\n\tdescription = \"Mailgun API key\"\n\tregex = '''(?i)(mailgun|mg)(.{0,20})?['\"][0-9a-z]{32}['\"]'''\n\ttags = [\"key\", \"Mailgun\"]\n[[rules]]\n\tdescription = \"PayPal Braintree access token\"\n\tregex = '''access_token\\$production\\$[0-9a-z]{16}\\$[0-9a-f]{32}'''\n\ttags = [\"key\", \"Paypal\"]\n[[rules]]\n\tdescription = \"Picatic API key\"\n\tregex = '''sk_live_[0-9a-z]{32}'''\n\ttags = [\"key\", \"Picatic\"]\n[[rules]]\n\tdescription = \"SendGrid API Key\"\n\tregex = '''SG\\.[\\w_]{16,32}\\.[\\w_]{16,64}'''\n\ttags = [\"key\", \"SendGrid\"]\n[[rules]]\n\tdescription = \"Slack Webhook\"\n\tregex = '''https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8}/[a-zA-Z0-9_]{24}'''\n\ttags = [\"key\", \"slack\"]\n[[rules]]\n\tdescription = \"Stripe API key\"\n\tregex = '''(?i)stripe(.{0,20})?['\\\"][sk|rk]_live_[0-9a-zA-Z]{24}'''\n\ttags = [\"key\", \"Stripe\"]\n[[rules]]\n\tdescription = \"Square access token\"\n\tregex = '''sq0atp-[0-9A-Za-z\\-_]{22}'''\n\ttags = [\"key\", \"square\"]\n[[rules]]\n\tdescription = \"Square OAuth secret\"\n\tregex = '''sq0csp-[0-9A-Za-z\\\\-_]{43}'''\n\ttags = [\"key\", \"square\"]\n[[rules]]\n\tdescription = \"Twilio API key\"\n\tregex = '''(?i)twilio(.{0,20})?['\\\"][0-9a-f]{32}['\\\"]'''\n\ttags = [\"key\", \"twilio\"]\n[whitelist]\n\tdescription = \"Whitelisted files\"\n\tfile = '''(^\\.?gitleaks.toml$|(.*?)(jpg|gif|doc|pdf|bin)$|^package-lock.json$|yarn.lock|pnpm-lock.yaml|node_modules)'''"
  },
  {
    "path": ".husky/commit-msg",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpx commitlint --edit $1\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpm run pretty"
  },
  {
    "path": ".npmrc",
    "content": "shamefully-hoist=true"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n  - 'lts/*'\ninstall:\n  - pnpm install\n  - pnpm install -g surge\nscript:\n  - pnpm run build:doc\n  - surge ./dist ahooks-$(git rev-parse --short HEAD).surge.sh\n  - pnpm run coveralls # generate static files\ncache:\n  directories:\n    - 'node_modules'\n"
  },
  {
    "path": "CONTRIBUTING.MD",
    "content": "# Contributing\n\nThe following is a set of guidelines for contributing to `ahooks`. Please spend several minutes reading these guidelines before creating an issue or pull request.\n\n## Open Development\n\nAll work on ahooks happens directly on [GitHub](https://github.com/alibaba/hooks). Pull requests sent by both core team members and external contributors will go through the same review process.\n\n## New Features\n\nIf you want to add a new Hook, we recommend that you first create an issue that describes the scenario and usage of the Hook, see [[RFC] useLockFn](https://github.com/alibaba/hooks/issues/562).\n\nThen you can initialize a new Hook based on an existing Hook.\n\n## Pull Request\n\nWe are monitoring for pull requests. We will review your pull request and either merge it, request changes to it, or close it with an explanation.\n\nBefore submitting a pull request, please make sure the following is done:\n\n1. Create your branch from the master.\n\n2. If you've fixed a bug or added code that should be tested, add tests!\n\n3. Ensure the test suite passes `pnpm run test`.\n\n## Development Workflow\n\nAfter cloning `ahooks`, run `pnpm run init` to fetch its dependencies. Then, you can run several commands:\n\n1. `pnpm start` runs `ahooks` website locally.\n\n2. `pnpm run test` runs the complete test suite.\n\n3. `pnpm run build` to build.\n"
  },
  {
    "path": "CONTRIBUTING.zh-CN.MD",
    "content": "# 贡献指南\n\n这篇指南会指导你如何为 `ahooks` 贡献一份自己的力量，请在你要提 issue 或者 pull request 之前花几分钟来阅读一遍这篇指南。\n\n## 透明的开发\n\n我们所有的工作都会放在 [GitHub](https://github.com/alibaba/hooks) 上。不管是核心团队的成员还是外部贡献者的 pull request 都需要经过同样流程的 review。\n\n## 新增功能\n\n如果你想新增一个 Hooks，我们建议你先建立一个 issue，说明该 Hooks 的应用场景及用法，参考 [[RFC] useLockFn](https://github.com/alibaba/hooks/issues/562)。\n\n然后你可以基于已有 Hook 来初始化一个新的 Hook。\n\n## Pull Request\n\n我们会关注所有的 pull request，会 review 以及合并你的代码，也有可能要求你做一些修改或者告诉你我们为什么不能接受这样的修改。\n\n在你发送 Pull Request 之前，请确认你是按照下面的步骤来做的：\n\n1. 基于 master 分支做修改。\n\n2. 如果你修复了一个 bug 或者新增了一个功能，请确保写了相应的测试，这很重要。\n\n3. 确认所有的测试是通过的 `pnpm run test`。\n\n## 开发流程\n\n在你 clone 代码并且使用 `pnpm run init` 安装完依赖后，你还可以运行下面几个常用的命令：\n\n1. `pnpm start` 在本地运行 `ahooks` 网站。\n\n2. `pnpm run test` 运行测试。\n\n3. `pnpm run build` 构建编译。\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019-present ahooks\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <a href=\"https://ahooks.js.org\">\n    <img width=\"200\" src=\"https://ahooks.js.org/logo.svg\">\n  </a>\n</p>\n\n<div align=\"center\">\n\nA high-quality & reliable React Hooks library.\n\n[![NPM version][image-1]][1]\n[![NPM downloads][image-2]][2]\n[![npm](https://img.shields.io/npm/dw/ahooks-v2?label=downloads%28v2%29)](https://www.npmjs.com/package/ahooks-v2)\n[![npm](https://img.shields.io/github/issues/alibaba/hooks)](https://github.com/alibaba/hooks/issues)\n[![Coverage Status](https://coveralls.io/repos/github/alibaba/hooks/badge.svg?branch=master)](https://coveralls.io/github/alibaba/hooks?branch=master)\n![gzip size](https://img.badgesize.io/https:/unpkg.com/ahooks/dist/ahooks.js?label=gzip%20size&compression=gzip)\n[![Percentage of issues still open](http://isitmaintained.com/badge/open/alibaba/hooks.svg)](http://isitmaintained.com/project/alibaba/hooks \"Percentage of issues still open\")\n[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/alibaba/hooks.svg)](http://isitmaintained.com/project/alibaba/hooks \"Average time to resolve an issue\")\n![GitHub](https://img.shields.io/github/license/alibaba/hooks)\n\nEnglish | [简体中文](https://github.com/alibaba/hooks/blob/master/README.zh-CN.md)\n\n</div>\n\n## 📚 Documentation\n\n- [English](https://ahooks.js.org/)\n- [中文](https://ahooks.js.org/zh-CN/)\n\n## ✨ Features\n\n- Easy to learn and use\n- Supports SSR\n- Special treatment for functions, avoid closure problems\n- Contains a large number of advanced Hooks that are refined from business scenarios\n- Contains a comprehensive collection of basic Hooks\n- Written in TypeScript with predictable static types\n\n## 📦 Install\n\n```bash\n$ npm install --save ahooks\n# or\n$ yarn add ahooks\n# or\n$ pnpm add ahooks\n# or\n$ bun add ahooks\n```\n\n## 🔨 Usage\n\n```ts\nimport { useRequest } from \"ahooks\";\n```\n\n## 💻 Online Demo\n\n[![Edit demo for ahooks](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/demo-for-ahooks-forked-fg79k?file=/src/App.js)\n\n## 🤝 Contributing\n\n```bash\n$ git clone git@github.com:alibaba/hooks.git\n$ cd hooks\n$ pnpm run init\n$ pnpm start\n```\n\nOpen your browser and visit http://127.0.0.1:8000\n\nWe welcome all contributions, please read our [CONTRIBUTING.MD](https://github.com/alibaba/hooks/blob/master/CONTRIBUTING.MD) first, let's build a better hooks library together.\n\nThanks to all the contributors:\n\n<a href=\"https://github.com/alibaba/hooks/graphs/contributors\">\n  <img src=\"https://opencollective.com/ahooks/contributors.svg?width=960&button=false\" alt=\"contributors\" />\n</a>\n\n## 👥 Discuss\n\n<img alt=\"ahooks discussion group 1\" src=\"https://github.com/user-attachments/assets/0ba7a370-2a69-442f-b746-9eb16bbbc46c\" width=\"200\" style='display:inline' />\n<img alt=\"ahooks discussion group 2\" src=\"https://github.com/user-attachments/assets/a08693d3-bfcc-4aca-b2b0-2d9c23012858\" width=\"200\" style='display:inline' />\n<img alt=\"ahooks discussion group 3\" src=\"https://github.com/user-attachments/assets/15a505a7-06d1-4e72-ab02-6fad968323f1\" width=\"200\" style='display:inline' />\n\n[1]: https://www.npmjs.com/package/ahooks\n[2]: https://npmjs.org/package/ahooks\n[image-1]: https://img.shields.io/npm/v/ahooks.svg?style=flat\n[image-2]: https://img.shields.io/npm/dw/ahooks.svg?style=flat\n"
  },
  {
    "path": "README.zh-CN.md",
    "content": "<p align=\"center\">\n  <a href=\"https://ahooks.js.org\">\n    <img width=\"200\" src=\"https://ahooks.js.org/logo.svg\">\n  </a>\n</p>\n\n<div align=\"center\">\n\n一套高质量可靠的 React Hooks 库\n\n[![NPM version][image-1]][1]\n[![NPM downloads][image-2]][2]\n[![npm](https://img.shields.io/npm/dw/ahooks-v2?label=downloads%28v2%29)](https://www.npmjs.com/package/ahooks-v2)\n[![Coverage Status](https://coveralls.io/repos/github/alibaba/hooks/badge.svg?branch=master)](https://coveralls.io/github/alibaba/hooks?branch=master)\n[![npm](https://img.shields.io/github/issues/alibaba/hooks)](https://github.com/alibaba/hooks/issues)\n![gzip size](https://img.badgesize.io/https:/unpkg.com/ahooks/dist/ahooks.js?label=gzip%20size&compression=gzip)\n[![Percentage of issues still open](http://isitmaintained.com/badge/open/alibaba/hooks.svg)](http://isitmaintained.com/project/alibaba/hooks \"Percentage of issues still open\")\n[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/alibaba/hooks.svg)](http://isitmaintained.com/project/alibaba/hooks \"Average time to resolve an issue\")\n![GitHub](https://img.shields.io/github/license/alibaba/hooks)\n\n[English](https://github.com/alibaba/hooks/blob/master/README.md) | 简体中文\n\n</div>\n\n## 📚 文档\n\n- [English](https://ahooks.js.org/)\n- [中文](https://ahooks.js.org/zh-CN/)\n\n## ✨ 特性\n\n- 易学易用\n- 支持 SSR\n- 对输入输出函数做了特殊处理，避免闭包问题\n- 包含大量提炼自业务的高级 Hooks\n- 包含丰富的基础 Hooks\n- 使用 TypeScript 构建，提供完整的类型定义文件\n\n## 📦 安装\n\n```bash\n$ npm install --save ahooks\n# or\n$ yarn add ahooks\n# or\n$ pnpm add ahooks\n# or\n$ bun add ahooks\n```\n\n## 🔨 使用\n\n```js\nimport { useRequest } from \"ahooks\";\n```\n\n## 💻 在线体验\n\n[![Edit demo for ahooks](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/demo-for-ahooks-forked-fg79k?file=/src/App.js)\n\n## 🤝 参与共建\n\n```bash\n$ git clone git@github.com:alibaba/hooks.git\n$ cd hooks\n$ pnpm run init\n$ pnpm start\n```\n\n打开浏览器访问 http://127.0.0.1:8000\n\n我们欢迎所有人参与共建，请参考[CONTRIBUTING.MD](https://github.com/alibaba/hooks/blob/master/CONTRIBUTING.zh-CN.MD)\n\n感谢所有贡献者：\n\n<a href=\"https://github.com/alibaba/hooks/graphs/contributors\">\n  <img src=\"https://opencollective.com/ahooks/contributors.svg?width=960&button=false\" alt=\"contributors\" />\n</a>\n\n## 👥 交流讨论\n\n<img src=\"https://github.com/user-attachments/assets/0ba7a370-2a69-442f-b746-9eb16bbbc46c\" width=\"200\" style='display:inline' />\n<img src=\"https://github.com/user-attachments/assets/a08693d3-bfcc-4aca-b2b0-2d9c23012858\" width=\"200\" style='display:inline' />\n<img src=\"https://github.com/user-attachments/assets/15a505a7-06d1-4e72-ab02-6fad968323f1\" width=\"200\" style='display:inline' />\n\n[1]: https://www.npmjs.com/package/ahooks\n[2]: https://npmjs.org/package/ahooks\n[image-1]: https://img.shields.io/npm/v/ahooks.svg?style=flat\n[image-2]: https://img.shields.io/npm/dw/ahooks.svg?style=flat\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Reporting a Vulnerability\n\nPlease report vulnerabilities to brickspert.fjl@antfin.com or guangbo.hgb@alibaba-inc.com\n"
  },
  {
    "path": "biome.json",
    "content": "{\n  \"$schema\": \"./node_modules/@biomejs/biome/configuration_schema.json\",\n  \"files\": {\n    \"ignoreUnknown\": true\n  },\n  \"vcs\": {\n    \"enabled\": true,\n    \"clientKind\": \"git\",\n    \"useIgnoreFile\": true\n  },\n  \"linter\": {\n    \"rules\": {\n      \"style\": {\n        \"noNonNullAssertion\": \"off\"\n      },\n      \"correctness\": {\n        \"useHookAtTopLevel\": \"error\"\n      },\n      \"suspicious\": {\n        \"noExplicitAny\": \"off\"\n      }\n    }\n  },\n  \"formatter\": {\n    \"lineWidth\": 100,\n    \"indentStyle\": \"space\"\n  },\n  \"javascript\": {\n    \"parser\": {\n      \"unsafeParameterDecoratorsEnabled\": true\n    },\n    \"formatter\": {\n      \"quoteStyle\": \"single\"\n    }\n  },\n  \"css\": {\n    \"parser\": {\n      \"cssModules\": true\n    },\n    \"formatter\": {\n      \"enabled\": true\n    },\n    \"linter\": {\n      \"enabled\": true\n    }\n  }\n}\n"
  },
  {
    "path": "config/config.ts",
    "content": "import { menus } from './hooks';\n\nconst packages = require('../packages/hooks/package.json');\n\nexport default {\n  // ssr: {},\n  exportStatic: {},\n  nodeModulesTransform: {\n    type: 'none',\n    exclude: [],\n  },\n  // https://github.com/alibaba/hooks/issues/2155\n  extraBabelIncludes: ['filter-obj'],\n  extraBabelPlugins: [\n    [\n      'babel-plugin-import',\n      {\n        libraryName: 'antd',\n        libraryDirectory: 'es',\n        style: true,\n      },\n      'antd',\n    ],\n    [\n      'babel-plugin-import',\n      {\n        libraryName: '@alifd/next',\n        style: false,\n      },\n      'fusion',\n    ],\n  ],\n  mode: 'site',\n  title: 'ahooks 3.0',\n  favicon: '/simple-logo.svg',\n  logo: '/logo.svg',\n  dynamicImport: {},\n  manifest: {},\n  hash: true,\n  publicPath: '/',\n  alias: {\n    ahooks: process.cwd() + '/packages/hooks/src/index.ts',\n    '@ahooksjs/use-url-state': process.cwd() + '/packages/use-url-state/src/index.ts',\n  },\n  resolve: {\n    includes: ['docs', 'packages/hooks/src', 'packages/use-url-state'],\n  },\n  links: [\n    {\n      rel: 'stylesheet',\n      href: 'https://unpkg.com/@alifd/theme-design-pro@0.6.2/dist/next-noreset.min.css',\n    },\n    { rel: 'stylesheet', href: '/style.css' },\n  ],\n  navs: {\n    'zh-CN': [\n      { title: '指南', path: '/zh-CN/guide' },\n      { title: 'Hooks', path: '/zh-CN/hooks' },\n      {\n        title: '历史版本',\n        children: [\n          {\n            title: 'v2.x',\n            path: 'https://ahooks-v2.js.org/',\n          },\n          {\n            title: 'v1.x',\n            path: 'http://hooks.umijs.org/',\n          },\n        ],\n      },\n      { title: '更新日志', path: 'https://github.com/alibaba/hooks/releases' },\n      { title: '备用镜像', path: 'https://alibaba.github.io/hooks/' },\n      { title: 'GitHub', path: 'https://github.com/alibaba/hooks' },\n    ],\n    'en-US': [\n      { title: 'Guide', path: '/guide' },\n      { title: 'Hooks', path: '/hooks' },\n      {\n        title: 'Legacy Versions',\n        children: [\n          {\n            title: 'v2.x',\n            path: 'https://ahooks-v2.js.org/',\n          },\n          {\n            title: 'v1.x',\n            path: 'http://hooks.umijs.org/',\n          },\n        ],\n      },\n      { title: 'Releases', path: 'https://github.com/alibaba/hooks/releases' },\n      { title: '国内镜像', path: 'https://alibaba.github.io/hooks/' },\n      { title: 'GitHub', path: 'https://github.com/alibaba/hooks' },\n    ],\n  },\n  menus: {\n    '/': [\n      {\n        title: 'Home',\n        path: 'index',\n      },\n    ],\n    '/zh-CN': [\n      {\n        title: '首页',\n        path: 'index',\n      },\n    ],\n    '/guide': [\n      {\n        title: 'Intro',\n        path: '/guide',\n      },\n      {\n        title: 'v2 to v3',\n        path: '/guide/upgrade',\n      },\n      {\n        title: 'Hooks of dom specification',\n        path: '/guide/dom',\n      },\n      {\n        title: 'Blog',\n        children: [\n          {\n            title: 'ahooks function specification',\n            path: '/guide/blog/function',\n          },\n          {\n            title: 'React Hooks & SSR',\n            path: '/guide/blog/ssr',\n          },\n          {\n            title: 'React Hooks & react-refresh（HMR）',\n            path: '/guide/blog/hmr',\n          },\n          {\n            title: 'React Hooks & strict mode',\n            path: '/guide/blog/strict',\n          },\n        ],\n      },\n    ],\n    '/zh-CN/guide': [\n      {\n        title: '介绍',\n        path: '/guide',\n      },\n      {\n        title: 'v2 to v3',\n        path: '/guide/upgrade',\n      },\n      {\n        title: 'DOM 类 Hooks 使用规范',\n        path: '/guide/dom',\n      },\n      {\n        title: 'Blog',\n        children: [\n          {\n            title: 'ahooks 输入输出函数处理规范',\n            path: '/zh-CN/guide/blog/function',\n          },\n          {\n            title: 'React Hooks & SSR',\n            path: '/zh-CN/guide/blog/ssr',\n          },\n          {\n            title: 'React Hooks & react-refresh（HMR）',\n            path: '/zh-CN/guide/blog/hmr',\n          },\n          {\n            title: 'React Hooks & strict mode',\n            path: '/zh-CN/guide/blog/strict',\n          },\n        ],\n      },\n    ],\n    '/hooks': menus,\n    '/zh-CN/hooks': menus,\n  },\n  scripts: [\n    'https://s4.cnzz.com/z_stat.php?id=1278992092&web_id=1278992092',\n    `\n  const insertVersion = function() {\n    const logo = document.querySelector('.__dumi-default-navbar-logo');\n    if (!logo) return;\n    const dom = document.createElement('span');\n    dom.id = 'logo-version';\n    dom.innerHTML = '${packages.version}';\n    logo.parentNode.insertBefore(dom, logo.nextSibling);\n  };\n  const observer = new MutationObserver((mutationsList, observer) => {\n    for (const mutation of mutationsList) {\n      if (mutation.type === 'childList') {\n        const logoVersion = document.querySelector('#logo-version');\n        if (logoVersion) {\n          observer.disconnect();\n        } else {\n          insertVersion();\n        }\n      }\n    }\n  });\n  observer.observe(document.body, { childList: true, subtree: true });\n  `,\n  ],\n};\n"
  },
  {
    "path": "config/hooks.ts",
    "content": "export const menus = [\n  {\n    title: 'useRequest',\n    children: [\n      'useRequest/doc/index',\n      'useRequest/doc/basic',\n      'useRequest/doc/loadingDelay',\n      'useRequest/doc/polling',\n      'useRequest/doc/ready',\n      'useRequest/doc/refreshDeps',\n      'useRequest/doc/refreshOnWindowFocus',\n      'useRequest/doc/debounce',\n      'useRequest/doc/throttle',\n      'useRequest/doc/cache',\n      'useRequest/doc/retry',\n    ],\n  },\n  {\n    title: 'Scene',\n    children: [\n      'useAntdTable',\n      'useFusionTable',\n      'useInfiniteScroll',\n      'usePagination',\n      'useDynamicList',\n      'useVirtualList',\n      'useHistoryTravel',\n      'useNetwork',\n      'useSelections',\n      'useCountDown',\n      'useCounter',\n      'useTextSelection',\n      'useWebSocket',\n      'useTheme',\n    ],\n  },\n  {\n    title: 'LifeCycle',\n    children: ['useMount', 'useUnmount', 'useUnmountedRef'],\n  },\n  {\n    title: 'State',\n    children: [\n      'useSetState',\n      'useBoolean',\n      'useToggle',\n      'use-url-state',\n      'useCookieState',\n      'useLocalStorageState',\n      'useSessionStorageState',\n      'useDebounce',\n      'useThrottle',\n      'useMap',\n      'useSet',\n      'usePrevious',\n      'useRafState',\n      'useSafeState',\n      'useGetState',\n      'useResetState',\n    ],\n  },\n  {\n    title: 'Effect',\n    children: [\n      'useUpdateEffect',\n      'useUpdateLayoutEffect',\n      'useAsyncEffect',\n      'useDebounceEffect',\n      'useDebounceFn',\n      'useThrottleFn',\n      'useThrottleEffect',\n      'useDeepCompareEffect',\n      'useDeepCompareLayoutEffect',\n      'useInterval',\n      'useRafInterval',\n      'useTimeout',\n      'useRafTimeout',\n      'useLockFn',\n      'useUpdate',\n    ],\n  },\n  {\n    title: 'Dom',\n    children: [\n      'useEventListener',\n      'useClickAway',\n      'useDocumentVisibility',\n      'useDrop',\n      'useEventTarget',\n      'useExternal',\n      'useTitle',\n      'useFavicon',\n      'useFullscreen',\n      'useHover',\n      'useMutationObserver',\n      'useInViewport',\n      'useKeyPress',\n      'useLongPress',\n      'useMouse',\n      'useResponsive',\n      'useScroll',\n      'useSize',\n      'useFocusWithin',\n    ],\n  },\n  {\n    title: 'Advanced',\n    children: [\n      'useControllableValue',\n      'useCreation',\n      'useEventEmitter',\n      'useIsomorphicLayoutEffect',\n      'useLatest',\n      'useMemoizedFn',\n      'useReactive',\n    ],\n  },\n  {\n    title: 'Dev',\n    children: ['useTrackedEffect', 'useWhyDidYouUpdate'],\n  },\n];\n"
  },
  {
    "path": "docs/guide/blog/function.en-US.md",
    "content": "# ahooks function specification\n\nahooks tries its best to help everyone avoid the closure problem by specially processing the input and output functions.\n\n**1. All the output functions of ahooks, the references are stable**\n\n```ts\nconst [state, setState] = useState();\n```\n\nAs we all know, the reference of the `setState` function returned by `React.useState` is fixed, and there is no need to consider weird problems when using it, and there is no need to put `setState` in the dependencies of other Hooks.\n\nAll functions returned by ahooks Hooks have the same characteristics as `setState`, the reference will not change, just feel free to use it.\n\n**2. For all user input functions, always use the latest one**\n\nFor the received function, ahooks will do a special process to ensure that the function called each time is always the latest.\n\n```ts\nconst [state, setState] = useState();\n\nuseInterval(() => {\n  console.log(state);\n}, 1000);\n```\n\nFor example, in the above example, the function called by `useInterval` at any time is always the latest, that is, the state is always the latest.\n\n## Principle\n\nFor the input function, we use `useRef` to make a record to ensure that the latest function can be accessed anywhere.\n\n```js\nconst fnRef = useRef(fn);\nfnRef.current = fn;\n```\n\nFor example, the useUnmount code is as follows:\n\n```js\nconst useUnmount = (fn) => {\n  const fnRef = useRef(fn);\n  fnRef.current = fn;\n\n  useEffect(\n    () => () => {\n      fnRef.current();\n    },\n    [],\n  );\n};\n```\n\nIn the above code, because we use ref for memorizing the latest function to solve the closure problem.\n\nFor the output function, we use the [useMemoizedFn](/zh-CN/hooks/use-memoized-fn) wrapped to ensure that the reference address will never change.\n\nFor a simple example, given a `useToggle` Hook, the code is like this:\n\n```js\nconst useToggle = (left, right) => {\n  const [state, setState] = useState(left);\n\n  const toggle = useCallback(() => {\n    setState((s) => (s === left ? right : left));\n  }, [left, right]);\n\n  return [state, toggle];\n};\n```\n\nThe `toggle` function returned in this demo will change according to the changes of `left/right`, which is uncomfortable for users to use.\n\nThen we replace `useCallback` with `useMemoizedFn` to realize that the `toggle` reference will never change.\n\n```js\nconst useToggle = (left, right) => {\n  const [state, setState] = useState(left);\n\n  const toggle = useMemoizedFn(() => {\n    setState((s) => (s === left ? right : left));\n  });\n\n  return [state, toggle];\n};\n```\n"
  },
  {
    "path": "docs/guide/blog/function.zh-CN.md",
    "content": "# ahooks 函数处理规范\n\nahooks 通过对输入输出函数做特殊处理，尽力帮助大家避免闭包问题。\n\n**1. ahooks 所有的输出函数，地址都是不会变化的**\n\n```ts\nconst [state, setState] = React.useState();\n```\n\n众所周知，`React.useState` 返回的 `setState` 函数地址是固定的，使用时不需要考虑奇奇怪怪的问题，不需要把 `setState` 放到各种依赖中。\n\nahooks 所有 Hooks 返回的函数，都拥有和 `setState` 一样的特性，地址不会变化，放心大胆的使用即可。\n\n**2. 所有用户输入的函数，永远使用最新的一份**\n\n对于接收的函数，ahooks 会做一次特殊处理，保证每次调用的函数永远是最新的。\n\n```ts\nconst [state, setState] = useState();\n\nuseInterval(() => {\n  console.log(state);\n}, 1000);\n```\n\n比如以上示例，`useInterval` 任何时候调用的函数永远是最新的，也就是 state 永远是最新的。\n\n## 实现原理\n\n针对输入函数，我们通过 `useRef` 做一次记录，以保证在任何地方都能访问到最新的函数。\n\n```js\nconst fnRef = useRef(fn);\nfnRef.current = fn;\n```\n\n比如 useUnmount 代码如下：\n\n```js\nconst useUnmount = (fn) => {\n  const fnRef = useRef(fn);\n  fnRef.current = fn;\n\n  useEffect(\n    () => () => {\n      fnRef.current();\n    },\n    [],\n  );\n};\n```\n\n在上面的代码中，由于我们通过 ref 来记忆最新的函数，解决闭包问题。\n\n针对输出函数，我们通过 ahooks 的 [useMemoizedFn](/zh-CN/hooks/use-memoized-fn) 包裹，保证地址永远不会变化。\n\n举一个比较简单的例子，假如我们有一个 `useToggle` Hook，代码是这样的\n\n```js\nconst useToggle = (left, right) => {\n  const [state, setState] = useState(left);\n\n  const toggle = useCallback(() => {\n    setState((s) => (s === left ? right : left));\n  }, [left, right]);\n\n  return [state, toggle];\n};\n```\n\n这个 demo 中返回的 `toggle` 函数，会根据 `left/right` 的变化而变化的，用户用起来很难受。\n\n然后我们将 `useCallback` 替换成 `useMemoizedFn`，即可实现 `toggle` 地址永远不变化。\n\n```js\nconst useToggle = (left, right) => {\n  const [state, setState] = useState(left);\n\n  const toggle = useMemoizedFn(() => {\n    setState((s) => (s === left ? right : left));\n  });\n\n  return [state, toggle];\n};\n```\n"
  },
  {
    "path": "docs/guide/blog/hmr.en-US.md",
    "content": "# React Hooks & react-refresh（HMR）\n\n## What is react-refresh?\n\n[react-refresh-webpack-plugin](https://github.com/pmmmwh/react-refresh-webpack-plugin) is a hot module replacement (HMR) plugin provided by React.\n\n> A Webpack plugin to enable \"Fast Refresh\" (also previously known as Hot Reloading) for React components.\n\nIn the development, react-refresh can keep state in component, and only change the edited part. In [umi](https://umijs.org/zh-CN/docs/fast-refresh), can enable this feature by config `fastRefresh: {}`.\n\n![fast-refresh.gif](https://camo.githubusercontent.com/244b53f735f2a78cfbce79a3914600840cdedac545e5f309d32ac7be4fdb2517/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f6769662f3138393335302f313632303631353937363932382d33633832353564642d396165342d343933342d613833322d3965643934636565353762632e67696623636c69656e7449643d7563376235663533362d656661652d342666726f6d3d64726f702669643d753234363633316564266d617267696e3d2535426f626a6563742532304f626a656374253544266e616d653d666173742d726566726573682e676966266f726967696e4865696768743d363136266f726967696e57696474683d31303030266f726967696e616c547970653d62696e6172792673697a653d35313534393234267374617475733d646f6e65267374796c653d6e6f6e65267461736b49643d7565316263613762312d393035362d343431392d613438382d6231393365366236643936)\n\nThis gif shows the development experience of using the react-refresh. After edit some code, the username and password that have been filled in remain unchanged, only the edited part has been changed.\n\n## Simple Principles of react-refresh\n\nFor the Class component, react-refresh are always refresh (remount), existing state will be reset. For function components, react-refresh retains the existing state. Therefore, react-refresh provides a better experience for function components.\n\nThis article mainly explains the weird behavior of React Hooks in react-refresh mode. Now let us look at the working mechanism of react-refresh on function components.\n\n- To maintain the state during hot replacement, the value of `useState` and `useRef` will not update.\n- During hot replacement, [To avoid some problems](<(https://github.com/facebook/react/issues/21019#issuecomment-800650091)>), `useEffect`、`useCallback`、`useMemoRun` will re-executed.\n\n> When we update the code, we need to \"clean up\" the effects that hold onto past values (e.g. passed functions), and \"setup\" the new ones with updated values. Otherwise, the values used by your effect would be stale and \"disagree\" with value used in your rendering, which makes Fast Refresh much less useful and hurts the ability to have it work with chains of custom Hooks.\n\n![Kapture 2021-05-10 at 11.37.54.gif](https://camo.githubusercontent.com/d9452c7cb9035fd422d9be908d1815ad25f0ca496d938fc3962d317c6d29fc61/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f6769662f3138393335302f313632303631383030393232392d63656261323438342d656430612d343336392d393731612d3636353931383933313238642e67696623636c69656e7449643d7563376235663533362d656661652d342666726f6d3d64726f702669643d753737313339373665266d617267696e3d2535426f626a6563742532304f626a656374253544266e616d653d4b617074757265253230323032312d30352d3130253230617425323031312e33372e35342e676966266f726967696e4865696768743d383736266f726967696e57696474683d31323534266f726967696e616c547970653d62696e6172792673697a653d31373038383131267374617475733d646f6e65267374796c653d6e6f6e65267461736b49643d7561343663386239322d656234342d343862372d383634352d3738323232623438646464)\n\nAs shown in the gif, after the text is modified, `state` remains unchanged and `useEffect` is executed again.\n\n## Problem caused by react-refresh\n\nUnder the above working mechanism, there will be many problems. Next, I will give a few specific examples.\n\n### First problem\n\n```js\nimport React, { useEffect, useState } from 'react';\n\nexport default () => {\n  const [count, setState] = useState(0);\n\n  useEffect(() => {\n    setState((s) => s + 1);\n  }, []);\n\n  return <div>{count}</div>;\n};\n```\n\nThe above code is very simple. In normal mode, the maximum value of `count` is `1`. Because `useEffect` will only be executed once during initialization.\n\nBut in the react-refresh mode, the `state` does not change every time it is hot updated, but the re-execution of `useEffect` will cause the value of `count` to keep increasing.\n\n![Kapture 2021-05-10 at 12.09.47.gif](https://camo.githubusercontent.com/82528f255af3a88133d66824de55dd1f6e665030caf0bae81291951d5fe75943/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f6769662f3138393335302f313632303631393831313739312d34383161323862302d396262642d343938302d626635322d3731313561633336363262352e67696623636c69656e7449643d7563376235663533362d656661652d342666726f6d3d64726f702669643d753263313732313030266d617267696e3d2535426f626a6563742532304f626a656374253544266e616d653d4b617074757265253230323032312d30352d3130253230617425323031322e30392e34372e676966266f726967696e4865696768743d383736266f726967696e57696474683d31323534266f726967696e616c547970653d62696e6172792673697a653d31333532383932267374617475733d646f6e65267374796c653d6e6f6e65267461736b49643d7533346432653861302d333430642d346638632d383039302d6232346638303433623266)\n\nAs shown in the gif, `count` increases with each hot replacement.\n\n### Second problem\n\nIf you used [ahooks v2](https://github.com/alibaba/hooks/blob/release/v2.x/packages/hooks/src/useUpdateEffect/index.ts) or [react-use](https://github.com/streamich/react-use/blob/master/docs/useUpdateEffect.md) `useUpdateEffect` will also have unexpected behavior in HMR.\n\n```javascript\nimport React, { useEffect } from 'react';\nimport useUpdateEffect from './useUpdateEffect';\n\nexport default () => {\n  useEffect(() => {\n    console.log('useEffect');\n  }, []);\n\n  useUpdateEffect(() => {\n    console.log('useUpdateEffect');\n  }, []);\n\n  return <div>hello world</div>;\n};\n```\n\nCompared with `useEffect`, `useUpdateEffect` ignores the first execution and only executes when the deps changes. In the normal mode of the above code, `useUpdateEffect` will never be executed, because deps is an empty array and will never change.\nBut in react-refresh mode, during HMR, `useUpdateEffect` and `useEffect` are executed at the same time.\n\n![Kapture 2021-05-10 at 12.26.19.gif](https://camo.githubusercontent.com/18000e2859234c5ca4d7613985dab82cba0a654cca53a9df5bc63dfcd126cce7/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f6769662f3138393335302f313632303632303739373138392d36613561366434302d616637372d343339642d616462632d3230666430343664636663302e67696623636c69656e7449643d7563376235663533362d656661652d342666726f6d3d64726f702669643d753065323737343631266d617267696e3d2535426f626a6563742532304f626a656374253544266e616d653d4b617074757265253230323032312d30352d3130253230617425323031322e32362e31392e676966266f726967696e4865696768743d383736266f726967696e57696474683d31323534266f726967696e616c547970653d62696e6172792673697a653d373937383135267374617475733d646f6e65267374796c653d6e6f6e65267461736b49643d7564613336343363622d386435312d346437322d626461322d3362333431353762313530)\n\nThe reason for this problem is that `useUpdateEffect` uses `ref` to record whether it is currently executed for the first time, see the code below.\n\n```javascript\nimport { useEffect, useRef } from 'react';\n\nconst useUpdateEffect: typeof useEffect = (effect, deps) => {\n  const isMounted = useRef(false);\n\n  useEffect(() => {\n    if (!isMounted.current) {\n      isMounted.current = true;\n    } else {\n      return effect();\n    }\n  }, deps);\n};\n\nexport default useUpdateEffect;\n```\n\nThe key of the above code is `isMounted`.\n\n- During initialization, after the `useEffect` is executed, the `isMounted` is changed to `true`\n- After the HMR, when the `useEffect` is re-executing, because the `isMounted` is already `true`, so the whole effect is executed again.\n\n### Third problem\n\nThe first time discovered this problem is the `useRequest` of ahooks, after HMR, the `loading` would always be `true`. After an inspection, the reason is use the `isUnmount` ref to mark whether the component is unmount.\n\n```javascript\nimport React, { useEffect, useState } from 'react';\n\nfunction getUsername() {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve('test');\n    }, 1000);\n  });\n}\n\nexport default function IndexPage() {\n  const isUnmount = React.useRef(false);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    setLoading(true);\n    getUsername().then(() => {\n      if (isUnmount.current === false) {\n        setLoading(false);\n      }\n    });\n    return () => {\n      isUnmount.current = true;\n    };\n  }, []);\n\n  return loading ? <div>loading</div> : <div>hello world</div>;\n}\n```\n\nAs the code above, during the hot replacement, `isUnmount.current` becomes `true`, causing the code to think that the component has been unmounted during the second execution.\n\n## How to solve these problems\n\n### First solution\n\nThe first solution is to solve it from the code, that is, when we write code, we can always remember the weird behavior in react-refresh mode.\n\nFor example, with `useUpdateEffect`, we can initialize the `isMounted` ref during initialization or hot replacement. as follows:\n\n```diff\nimport { useEffect, useRef } from 'react';\n\nconst useUpdateEffect: typeof useEffect = (effect, deps) => {\n  const isMounted = useRef(false);\n\n+  useEffect(() => {\n+  \tisMounted.current = false;\n+  }, []);\n\n  useEffect(() => {\n    if (!isMounted.current) {\n      isMounted.current = true;\n    } else {\n      return effect();\n    }\n  }, deps);\n};\n\nexport default useUpdateEffect;\n```\n\nThis solution is effective for both questions 2 and 3 above.\n\n### Second solution\n\nAccording to [Official Document](https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/docs/API.md#reset), we can solve this problem by adding the following comment in the file .\n\n```javascript\n/* @refresh reset */\n```\n\nAfter adding this question, every hot replacement will remount, that is, the component will be re-executed. `useState` and `useRef` will also be reset, so the above problem will not occur.\n\n## Official attitude\n\nThere are already many unspoken rules for React Hooks. When using react-refresh, there are still unspoken rules to pay attention to. But the official reply stated that this is expected behavior, see the [issue](https://github.com/facebook/react/issues/21019).\n\n> Effects are not exactly \"mount\"/\"unmount\" — they're more like \"show\"/\"hide\".\n"
  },
  {
    "path": "docs/guide/blog/hmr.zh-CN.md",
    "content": "# React Hooks & react-refresh（HMR）\n\n## 什么是 react-refresh\n\n[react-refresh-webpack-plugin](https://github.com/pmmmwh/react-refresh-webpack-plugin) 是 React 官方提供的一个 模块热替换（HMR）插件。\n\n> A Webpack plugin to enable \"Fast Refresh\" (also previously known as Hot Reloading) for React components.\n\n在开发环境编辑代码时，react-refresh 可以保持组件当前状态，仅仅变更编辑的部分。在 [umi](https://umijs.org/zh-CN/docs/fast-refresh) 中可以通过 `fastRefresh: {}`快速开启该功能。\n\n![fast-refresh.gif](https://camo.githubusercontent.com/244b53f735f2a78cfbce79a3914600840cdedac545e5f309d32ac7be4fdb2517/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f6769662f3138393335302f313632303631353937363932382d33633832353564642d396165342d343933342d613833322d3965643934636565353762632e67696623636c69656e7449643d7563376235663533362d656661652d342666726f6d3d64726f702669643d753234363633316564266d617267696e3d2535426f626a6563742532304f626a656374253544266e616d653d666173742d726566726573682e676966266f726967696e4865696768743d363136266f726967696e57696474683d31303030266f726967696e616c547970653d62696e6172792673697a653d35313534393234267374617475733d646f6e65267374796c653d6e6f6e65267461736b49643d7565316263613762312d393035362d343431392d613438382d6231393365366236643936)\n\n这张 gif 动图展示的是使用 react-refresh 特性的开发体验，可以看出，修改组件代码后，已经填写的用户名和密码保持不变，仅仅只有编辑的部分变更了。\n\n## react-refresh 的简单原理\n\n对于 Class 类组件，react-refresh 会一律重新刷新（remount），已有的 state 会被重置。而对于函数组件，react-refresh 则会保留已有的 state。所以 react-refresh 对函数类组件体验会更好。\n本篇文章主要讲解 React Hooks 在 react-refresh 模式下的怪异行为，现在我来看下 react-refresh 对函数组件的工作机制。\n\n- 在热更新时为了保持状态，`useState` 和 `useRef` 的值不会更新。\n- 在热更新时，[为了解决某些问题](https://github.com/facebook/react/issues/21019#issuecomment-800650091)，`useEffect`、`useCallback`、`useMemo` 等会重新执行。\n\n> When we update the code, we need to \"clean up\" the effects that hold onto past values (e.g. passed functions), and \"setup\" the new ones with updated values. Otherwise, the values used by your effect would be stale and \"disagree\" with value used in your rendering, which makes Fast Refresh much less useful and hurts the ability to have it work with chains of custom Hooks.\n\n![Kapture 2021-05-10 at 11.37.54.gif](https://camo.githubusercontent.com/d9452c7cb9035fd422d9be908d1815ad25f0ca496d938fc3962d317c6d29fc61/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f6769662f3138393335302f313632303631383030393232392d63656261323438342d656430612d343336392d393731612d3636353931383933313238642e67696623636c69656e7449643d7563376235663533362d656661652d342666726f6d3d64726f702669643d753737313339373665266d617267696e3d2535426f626a6563742532304f626a656374253544266e616d653d4b617074757265253230323032312d30352d3130253230617425323031312e33372e35342e676966266f726967696e4865696768743d383736266f726967696e57696474683d31323534266f726967696e616c547970653d62696e6172792673697a653d31373038383131267374617475733d646f6e65267374796c653d6e6f6e65267461736b49643d7561343663386239322d656234342d343862372d383634352d3738323232623438646464)\n\n如上图所示，在文本修改之后，`state` 保持不变，`useEffect` 被重新执行了。\n\n## react-refresh 工作机制导致的问题\n\n在上述工作机制下，会带来很多问题，接下来我会举几个具体的例子。\n\n### 第一个问题\n\n```js\nimport React, { useEffect, useState } from 'react';\n\nexport default () => {\n  const [count, setState] = useState(0);\n\n  useEffect(() => {\n    setState((s) => s + 1);\n  }, []);\n\n  return <div>{count}</div>;\n};\n```\n\n上面的代码很简单，在正常模式下，`count`值最大为 `1`。因为 `useEffect` 只会在初始化的时候执行一次。\n但在 react-refresh 模式下，每次热更新的时候，`state` 不变，但 `useEffect` 重新执行，就会导致 `count` 的值一直在递增。\n\n![Kapture 2021-05-10 at 12.09.47.gif](https://camo.githubusercontent.com/82528f255af3a88133d66824de55dd1f6e665030caf0bae81291951d5fe75943/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f6769662f3138393335302f313632303631393831313739312d34383161323862302d396262642d343938302d626635322d3731313561633336363262352e67696623636c69656e7449643d7563376235663533362d656661652d342666726f6d3d64726f702669643d753263313732313030266d617267696e3d2535426f626a6563742532304f626a656374253544266e616d653d4b617074757265253230323032312d30352d3130253230617425323031322e30392e34372e676966266f726967696e4865696768743d383736266f726967696e57696474683d31323534266f726967696e616c547970653d62696e6172792673697a653d31333532383932267374617475733d646f6e65267374796c653d6e6f6e65267461736b49643d7533346432653861302d333430642d346638632d383039302d6232346638303433623266)\n\n如上图所示，`count` 随着每一次热更新在递增。\n\n### 第二个问题\n\n如果你使用了 [ahooks v2](https://github.com/alibaba/hooks/blob/release/v2.x/packages/hooks/src/useUpdateEffect/index.ts) 或者 [react-use](https://github.com/streamich/react-use/blob/master/docs/useUpdateEffect.md) 的 `useUpdateEffect`，在热更新模式下也会有不符合预期的行为。\n\n```javascript\nimport React, { useEffect } from 'react';\nimport useUpdateEffect from './useUpdateEffect';\n\nexport default () => {\n  useEffect(() => {\n    console.log('执行了 useEffect');\n  }, []);\n\n  useUpdateEffect(() => {\n    console.log('执行了 useUpdateEffect');\n  }, []);\n\n  return <div>hello world</div>;\n};\n```\n\n`useUpdateEffect` 与 `useEffect`相比，它会忽略第一次执行，只有在 deps 变化时才会执行。以上代码的在正常模式下，`useUpdateEffect` 是永远不会执行的，因为 deps 是空数组，永远不会变化。\n但在 react-refresh 模式下，热更新时，`useUpdateEffect` 和 `useEffect` 同时执行了。\n\n![Kapture 2021-05-10 at 12.26.19.gif](https://camo.githubusercontent.com/18000e2859234c5ca4d7613985dab82cba0a654cca53a9df5bc63dfcd126cce7/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f6769662f3138393335302f313632303632303739373138392d36613561366434302d616637372d343339642d616462632d3230666430343664636663302e67696623636c69656e7449643d7563376235663533362d656661652d342666726f6d3d64726f702669643d753065323737343631266d617267696e3d2535426f626a6563742532304f626a656374253544266e616d653d4b617074757265253230323032312d30352d3130253230617425323031322e32362e31392e676966266f726967696e4865696768743d383736266f726967696e57696474683d31323534266f726967696e616c547970653d62696e6172792673697a653d373937383135267374617475733d646f6e65267374796c653d6e6f6e65267461736b49643d7564613336343363622d386435312d346437322d626461322d3362333431353762313530)\n\n造成这个问题的原因，就是 `useUpdateEffect` 用 `ref` 来记录了当前是不是第一次执行，见下面的代码。\n\n```javascript\nimport { useEffect, useRef } from 'react';\n\nconst useUpdateEffect: typeof useEffect = (effect, deps) => {\n  const isMounted = useRef(false);\n\n  useEffect(() => {\n    if (!isMounted.current) {\n      isMounted.current = true;\n    } else {\n      return effect();\n    }\n  }, deps);\n};\n\nexport default useUpdateEffect;\n```\n\n上面代码的关键在 `isMounted`\n\n- 初始化时，`useEffect` 执行，标记 `isMounted` 为 `true`\n- 热更新后，`useEffect` 重新执行了，此时 `isMounted` 为 `true`，就往下执行了\n\n### 第三个问题\n\n最初发现这个问题，是 ahooks 的 `useRequest` 在热更新后，`loading` 会一直为 `true`。经过分析，原因就是使用 `isUnmount` ref 来标记组件是否卸载。\n\n```javascript\nimport React, { useEffect, useState } from 'react';\n\nfunction getUsername() {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve('test');\n    }, 1000);\n  });\n}\n\nexport default function IndexPage() {\n  const isUnmount = React.useRef(false);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    setLoading(true);\n    getUsername().then(() => {\n      if (isUnmount.current === false) {\n        setLoading(false);\n      }\n    });\n    return () => {\n      isUnmount.current = true;\n    };\n  }, []);\n\n  return loading ? <div>loading</div> : <div>hello world</div>;\n}\n```\n\n如上代码所示，在热更新时，`isUnmount.current` 变为了 `true`，导致二次执行时，代码以为组件已经卸载了，不再响应异步操作。\n\n## 如何解决这些问题\n\n### 方案一\n\n第一个解决方案是从代码层面解决，也就是要求我们在写代码的时候，时时能想起来 react-refresh 模式下的怪异行为。\n比如 `useUpdateEffect` 我们就可以在初始化或者热替换时，将 `isMounted` ref 初始化掉。如下：\n\n```diff\nimport { useEffect, useRef } from 'react';\n\nconst useUpdateEffect: typeof useEffect = (effect, deps) => {\n  const isMounted = useRef(false);\n\n+  useEffect(() => {\n+  \tisMounted.current = false;\n+  }, []);\n\n  useEffect(() => {\n    if (!isMounted.current) {\n      isMounted.current = true;\n    } else {\n      return effect();\n    }\n  }, deps);\n};\n\nexport default useUpdateEffect;\n```\n\n这个方案对上面的问题二和三都是有效的。\n\n### 方案二\n\n根据[官方文档](https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/docs/API.md#reset)，我们可以通过在文件中添加以下注释来解决这个问题。\n\n```javascript\n/* @refresh reset */\n```\n\n添加这个问题后，每次热更新，都会 remount，也就是组件重新执行。`useState` 和 `useRef` 也会重置掉，也就不会出现上面的问题了。\n\n## 官方态度\n\n本来 React Hooks 已经有蛮多潜规则了，在使用 react-refresh 时，还有潜规则要注意。但官方回复说这是预期行为，见该 [issue](https://github.com/facebook/react/issues/21019)。\n\n> Effects are not exactly \"mount\"/\"unmount\" — they're more like \"show\"/\"hide\".\n"
  },
  {
    "path": "docs/guide/blog/ssr.en-US.md",
    "content": "# React Hooks & SSR\n\nServer-Side Rendering refers to the page processing technology where the HTML structure of the page is spliced on the server side. Generally used to solve SEO problems and speed up the first screen.\n\nSince SSR executes JS code in a non-browser environment, there will be many problems. This article mainly introduces the common problems and solutions of React Hooks in SSR mode.\n\n## Problem 1: DOM/BOM is missing\n\nSSR is to run React code in a node environment, while global properties such as window, document, and navigator are not available at this time. If you use these properties directly, you will get errors like `window is not defined, document is not defined, navigator is not defined`, etc.\n\nA common misuse is that global properties, such as document, are used directly during the execution of Hooks.\n\n```js\nimport React, { useState } from 'react';\n\nexport default () => {\n  const [state, setState] = useState(document.visibilityState);\n  return state;\n};\n```\n\n### Solution\n\n1. Put the code of accessing the DOM/BOM in useEffect/useLayoutEffect (the server will not execute it) to avoid errors when the server executes, for example:\n\n```js\nimport React, { useState, useEffect } from 'react';\n\nexport default () => {\n  const [state, setState] = useState();\n\n  useEffect(() => {\n    setState(document.visibilityState);\n  }, []);\n\n  return state;\n};\n```\n\n2. Differentiate the environments by `isBrowser`\n\n```js\nimport React, { useState } from 'react';\n\nfunction isBrowser() {\n  return !!(typeof window !== 'undefined' && window.document && window.document.createElement);\n}\n\nexport default () => {\n  const [state, setState] = useState(isBrowser() && document.visibilityState);\n\n  return state;\n};\n```\n\n## Problem 2: useLayoutEffect Warning\n\nIf `useLayoutEffect` is used, the following warning will appear in SSR mode\n\n> ⚠️ Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://fb.me/react-uselayouteffect-ssr for common fixes.\n\n### Solution\n\n1. Use useEffect instead of useLayoutEffect (nonsense)\n2. Dynamically specify whether to use useEffect or useLayoutEffect according to the environment. This is a hack solution from the community, currently in [react-redux](https://github.com/reduxjs/react-redux/blob/d16262582b2eeb62c05313fca3eb59dc0b395955/src/components/connectAdvanced.js#L40), [react-use](https://github.com/streamich/react-use/blob/master/src/useIsomorphicLayoutEffect.ts), [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd/blob/master/src/view/use-isomorphic-layout-effect.js).\n\n```js\nimport { useLayoutEffect, useEffect } from 'react';\nconst useIsomorphicLayoutEffect = isBrowser() ? useLayoutEffect : useEffect;\nexport default useIsomorphicLayoutEffect;\n```\n\n## Summary: Need to pay attention when writing Hooks\n\n1. Do not use DOM/BOM properties directly in non-useEffect/useLayoutEffect\n2. When using DOM/BOM properties other than useEffect/useLayoutEffect, use `isBrowser` to determine whether to execute in the browser environment\n3. If a Hook needs to receive DOM/BOM properties, it needs to support passing the properties via a function type parameter. Take the useEventListener of ahooks as an example, it must support the function type to specify the target option.\n\n```diff\nimport React, { useState } from 'react';\nimport { useEventListener } from 'ahooks';\n\nexport default () => {\n  const [value, setValue] = useState(0);\n\n  const clickHandler = () => {\n    setValue(value + 1);\n  };\n\n  useEventListener(\n    'click',\n    clickHandler,\n    {\n-       target: document.getElementById('click-btn')\n+       target: () => document.getElementById('click-btn')\n    }\n  );\n\n  return (\n    <button id=\"click-btn\" type=\"button\">\n      You click {value} times\n    </button>\n  );\n};\n```\n\n4. Use `useIsomorphicLayoutEffect` instead of `useLayoutEffect`\n\n## Reference\n\n- [fix: useDocumentVisiblility support ssr](https://github.com/alibaba/hooks/pull/935/files)\n- [UmiJS 服务端渲染](https://umijs.org/docs/ssr#window-is-not-defined-document-is-not-defined-navigator-is-not-defined)\n- [useLayoutEffect and SSR](https://medium.com/@alexandereardon/uselayouteffect-and-ssr-192986cdcf7a)\n"
  },
  {
    "path": "docs/guide/blog/ssr.zh-CN.md",
    "content": "# React Hooks & SSR\n\n服务端渲染（Server-Side Rendering），是指由服务侧完成页面的 HTML 结构拼接的页面处理技术。一般用于解决 SEO 问题和首屏加载速度问题。\n\n由于 SSR 是在非浏览器环境执行 JS 代码，所以会出现很多问题。本文主要介绍 React Hooks 在 SSR 模式下常见问题及解决方案。\n\n> 更多关于 SSR 的介绍可以看 UmiJS 的文档《[服务端渲染（SSR）](https://umijs.org/zh-CN/docs/ssr#服务端渲染（ssr）)》。\n\n## 问题一：DOM/BOM 缺失\n\nSSR 是在 node 环境下运行 React 代码，而此时 window、document、navigator 等全局属性没有。如果直接使用了这些属性，就会报错 `window is not defined, document is not defined, navigator is not defined` 等。\n\n常见的错误用法是在 Hooks 执行过程中，直接使用了 document 等全局属性。\n\n```js\nimport React, { useState } from 'react';\n\nexport default () => {\n  const [state, setState] = useState(document.visibilityState);\n  return state;\n};\n```\n\n### 解决方案\n\n1. 将访问 DOM/BOM 的方法放在 useEffect/useLayoutEffect 中（服务端不会执行），避免服务端执行时报错，例如：\n\n```js\nimport React, { useState, useEffect } from 'react';\n\nexport default () => {\n  const [state, setState] = useState();\n\n  useEffect(() => {\n    setState(document.visibilityState);\n  }, []);\n\n  return state;\n};\n```\n\n2. 通过 `isBrowser` 来做环境判断\n\n```js\nimport React, { useState } from 'react';\n\nfunction isBrowser() {\n  return !!(typeof window !== 'undefined' && window.document && window.document.createElement);\n}\n\nexport default () => {\n  const [state, setState] = useState(isBrowser() && document.visibilityState);\n\n  return state;\n};\n```\n\n## 问题二 useLayoutEffect Warning\n\n如果使用了 `useLayoutEffect`，在 SSR 模式下，会出现以下警告\n\n> ⚠️ Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://fb.me/react-uselayouteffect-ssr for common fixes.\n\n### 解决方案\n\n1. 使用 useEffect 代替 useLayoutEffect（废话）\n2. 根据环境动态的指定是使用 useEffect 还是 useLayoutEffect。这是来自社区的一种 hack 解决方案，目前在 [react-redux](https://github.com/reduxjs/react-redux/blob/d16262582b2eeb62c05313fca3eb59dc0b395955/src/components/connectAdvanced.js#L40)、[react-use](https://github.com/streamich/react-use/blob/master/src/useIsomorphicLayoutEffect.ts)、[react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd/blob/master/src/view/use-isomorphic-layout-effect.js) 均使用的这种方案。\n\n```js\nimport { useLayoutEffect, useEffect } from 'react';\nconst useIsomorphicLayoutEffect = isBrowser() ? useLayoutEffect : useEffect;\nexport default useIsomorphicLayoutEffect;\n```\n\n## 总结：写 Hooks 时需要注意\n\n1. 不要在非 useEffect/useLayoutEffect 中，直接使用 DOM/BOM 属性\n2. 在非 useEffect/useLayoutEffect 使用 DOM/BOM 属性时，使用 `isBrowser` 判断是否在浏览器环境执行\n3. 如果某个 Hook 需要接收 DOM/BOM 属性，需要支持函数形式传参。以 ahooks 的 useEventListener 举例，必须支持函数形式来指定 target 属性。\n\n```diff\nimport React, { useState } from 'react';\nimport { useEventListener } from 'ahooks';\n\nexport default () => {\n  const [value, setValue] = useState(0);\n\n  const clickHandler = () => {\n    setValue(value + 1);\n  };\n\n  useEventListener(\n    'click',\n    clickHandler,\n    {\n-       target: document.getElementById('click-btn')\n+       target: () => document.getElementById('click-btn')\n    }\n  );\n\n  return (\n    <button id=\"click-btn\" type=\"button\">\n      You click {value} times\n    </button>\n  );\n};\n```\n\n4. 使用 `useIsomorphicLayoutEffect` 来代替 `useLayoutEffect`\n\n## 参考资料\n\n- [fix: useDocumentVisiblility support ssr](https://github.com/alibaba/hooks/pull/935/files)\n- [UmiJS 服务端渲染](https://umijs.org/zh-CN/docs/ssr#window-is-not-defined-document-is-not-defined-navigator-is-not-defined)\n- [useLayoutEffect and SSR](https://medium.com/@alexandereardon/uselayouteffect-and-ssr-192986cdcf7a)\n"
  },
  {
    "path": "docs/guide/blog/strict.en-US.md",
    "content": "# React Hooks & strict mode\n\n## What is strict mode\n\nIn React, there are many historical APIs or writing methods that will be obsolete in future versions and are now marked as deprecated. such as `componentWillMount`, in normal mode, you can use it normally. But in strict mode, a warning will be thrown:\n\n![image.png](https://user-images.githubusercontent.com/12526493/140928679-cafd5b58-2937-41a9-87e8-f68aa6d978d9.png)\n\nSo **strict mode is for future development, all APIs or writing methods that are not recommended will throw warnings (only effective in development mode).**\n\nWe can use `React.StrictMode` to enable strict mode.\n\n```javascript\nimport React from 'react';\n\nfunction ExampleApplication() {\n  return (\n    <div>\n      <Header />\n      <React.StrictMode>\n        <div>\n          <ComponentOne />\n          <ComponentTwo />\n        </div>\n      </React.StrictMode>\n      <Footer />\n    </div>\n  );\n}\n```\n\nFor more documents, please refer to \"[Strict Mode](https://reactjs.org/docs/strict-mode.html)\"\n\n## Points to note in React Hooks\n\nOne of the most important capabilities of strict mode is \"[Detecting Unexpected Side Effects](https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects)\", in the upcoming concurrent mode, the component is divided into two phases:\n\n- **Render phase**: Generate DOM tree, will execute constructor, componentWillMount, componentWillReceiveProps, componentWillUpdate, getDerivedStateFromProps, shouldComponentUpdate, render, **useState, useMemo, useCallback** and other life cycles\n- **Commit stage**: Apply DOM changes, trigger componentDidMount, componentDidUpdate, **useEffect** and other life cycles\n\nGenerally, the render phase is time-consuming, and the commit phase is executed quickly. Therefore, in the upcoming concurrent mode, the render phase may be suspended and re-executed. That is, the life cycle of the rendering phase may be executed multiple times.\n\n```javascript\nconstructor(){\n  services.getUserInfo().then(() => {\n    .....\n  });\n}\n```\n\nAs above, if we initiate a network request in the constructor, it may be executed multiple times. So **do not perform operations with side effects during the render phase.**\n\nBut if you perform side-effect operations during the rendering phase, React will not be able to perceive it. **But in strict mode, React will intentionally repeat the render phase method, making it easier for us to find such bugs in the development phase** (not all the life cycles of the rendering phase will be re-executed, see [Official Documentation](https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects)).\n\n```javascript\nconst useTest = () => {\n  const [state, setState] = useState(() => {\n    console.log('get state');\n    return 'state';\n  });\n\n  const memoState = useMemo(() => {\n    console.log('get memo state');\n    return 'state';\n  }, []);\n\n  console.log('render');\n\n  return state;\n};\n```\n\nIn the above code, the first parameter of `useState`, `useMemo` and the Hook function body are all executed twice.\n\n[Demo](https://codesandbox.io/s/xvv55893mp?file=/src/index.js)\n\nPlease remember the conclusion: **In strict mode, the first parameter of `useState`, `useMemo`, `useReducer` and the Hook function body will be executed twice. Do not perform operations with side effects here.**\n"
  },
  {
    "path": "docs/guide/blog/strict.zh-CN.md",
    "content": "# React Hooks & strict mode\n\n## 什么是严格模式\n\n在 React 中，有很多历史的 API 或写法，在未来版本中会被废弃，现在被标记为不建议使用。既然是不建议使用，那还是可以用的，比如 `componentWillMount`，在普通模式下，你可以正常使用。但在严格模式下，就会抛出警告：\n\n![image.png](https://user-images.githubusercontent.com/12526493/140928679-cafd5b58-2937-41a9-87e8-f68aa6d978d9.png)\n\n所以**严格模式就是面向未来开发，所有不建议的 API 或写法，都会抛出警告（只在开发模式生效）。**\n\n一般我们可以通过 `React.StrictMode` 来局部启用严格模式。\n\n```javascript\nimport React from 'react';\n\nfunction ExampleApplication() {\n  return (\n    <div>\n      <Header />\n      <React.StrictMode>\n        <div>\n          <ComponentOne />\n          <ComponentTwo />\n        </div>\n      </React.StrictMode>\n      <Footer />\n    </div>\n  );\n}\n```\n\n更多文档参考《[严格模式](https://zh-hans.reactjs.org/docs/strict-mode.html)》\n\n## 在 React Hooks 中需要注意的点\n\n严格模式很重要的一个能力是《[检测意外的副作用](https://zh-hans.reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects)》，在未来的 concurrent 模式中，组件被分为两个阶段：\n\n- **渲染（render）阶段**：生成 DOM 树，会执行 constructor、componentWillMount、componentWillReceiveProps、componentWillUpdate、getDerivedStateFromProps、shouldComponentUpdate、render、**useState、useMemo、useCallback** 等生命周期\n- **提交（commit）阶段**：操作 DOM，触发 componentDidMount、componentDidUpdate、**useEffect** 等生命周期\n\n一般渲染阶段会比较耗时，提交阶段执行很快。所以在未来的 concurrent 模式中，渲染阶段可能会被暂停、重新执行。也就是渲染阶段的生命周期，可能会被多次执行。\n\n```javascript\nconstructor(){\n  services.getUserInfo().then(() => {\n    .....\n  });\n}\n```\n\n如上，我们在 constructor 中发起网络请求，就可能被执行多次。所以**不要在渲染阶段执行带有副作用的操作。**\n\n但假如你在渲染阶段执行了副作用操作，React 也是无法感知的。**但是 React 在严格模式下，会故意重复执行渲染阶段的方法，使得我们在开发阶段能更容易发现这类 bug**（并不是所有渲染阶段的生命周期都会被重新执行，具体见[官方文档](https://zh-hans.reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects)）。\n\n```javascript\nconst useTest = () => {\n  const [state, setState] = useState(() => {\n    console.log('get state');\n    return 'state';\n  });\n\n  const memoState = useMemo(() => {\n    console.log('get memo state');\n    return 'state';\n  }, []);\n\n  console.log('render');\n\n  return state;\n};\n```\n\n在上面的代码中 `useState`、`useMemo` 的第一个参数、Hook 函数体均执行了两次。\n\n[在线体验](https://codesandbox.io/s/xvv55893mp?file=/src/index.js)\n\n请记住结论：**在严格模式下，`useState`、`useMemo`、`useReducer` 的第一个参数、Hook 函数体都会被执行两次，不要在这里执行带有副作用的操作。**\n"
  },
  {
    "path": "docs/guide/dom.en-US.md",
    "content": "## Hooks of DOM specification\n\nMost of the DOM Hooks will receive the `target` parameter, which indicates the element to be processed.\n\n`target` supports three types `React.MutableRefObject`, `HTMLElement`, `() => HTMLElement`.\n\n1. Support `React.MutableRefObject`\n\n```ts\nexport default () => {\n  const ref = useRef(null);\n  const isHovering = useHover(ref);\n  return <div ref={ref}>{isHovering ? 'hover' : 'leaveHover'}</div>;\n};\n```\n\n2. Support `HTMLElement`\n\n```ts\nexport default () => {\n  const isHovering = useHover(document.getElementById('test'));\n  return <div id=\"test\">{isHovering ? 'hover' : 'leaveHover'}</div>;\n};\n```\n\n3. Support `() => HTMLElement`, generally applicable in SSR scenarios\n\n```ts\nexport default () => {\n  const isHovering = useHover(() => document.getElementById('test'));\n  return <div id=\"test\">{isHovering ? 'hover' : 'leaveHover'}</div>;\n};\n```\n\nIn addition, **the `target` of DOM Hooks supports dynamic changes**. for example:\n\n```ts\nexport default () => {\n  const [boolean, { toggle }] = useBoolean();\n\n  const ref = useRef(null);\n  const ref2 = useRef(null);\n\n  const isHovering = useHover(boolean ? ref : ref2);\n  return (\n    <>\n      <div ref={ref}>{isHovering ? 'hover' : 'leaveHover'}</div>\n      <div ref={ref2}>{isHovering ? 'hover' : 'leaveHover'}</div>\n    </>\n  );\n};\n```\n"
  },
  {
    "path": "docs/guide/dom.zh-CN.md",
    "content": "## DOM 类 Hooks 使用规范\n\nahooks 大部分 DOM 类 Hooks 都会接收 `target` 参数，表示要处理的元素。\n\n`target` 支持三种类型 `React.MutableRefObject`、`HTMLElement`、`() => HTMLElement`。\n\n1. 支持 `React.MutableRefObject`\n\n```ts\nexport default () => {\n  const ref = useRef(null);\n  const isHovering = useHover(ref);\n  return <div ref={ref}>{isHovering ? 'hover' : 'leaveHover'}</div>;\n};\n```\n\n2. 支持 `HTMLElement`\n\n```ts\nexport default () => {\n  const isHovering = useHover(document.getElementById('test'));\n  return <div id=\"test\">{isHovering ? 'hover' : 'leaveHover'}</div>;\n};\n```\n\n3. 支持 `() => HTMLElement`，一般适用在 SSR 场景\n\n```ts\nexport default () => {\n  const isHovering = useHover(() => document.getElementById('test'));\n  return <div id=\"test\">{isHovering ? 'hover' : 'leaveHover'}</div>;\n};\n```\n\n另外，**DOM 类 Hooks 的 `target` 是支持动态变化的**。比如：\n\n```ts\nexport default () => {\n  const [boolean, { toggle }] = useBoolean();\n\n  const ref = useRef(null);\n  const ref2 = useRef(null);\n\n  const isHovering = useHover(boolean ? ref : ref2);\n  return (\n    <>\n      <div ref={ref}>{isHovering ? 'hover' : 'leaveHover'}</div>\n      <div ref={ref2}>{isHovering ? 'hover' : 'leaveHover'}</div>\n    </>\n  );\n};\n```\n"
  },
  {
    "path": "docs/guide/index.en-US.md",
    "content": "## Intro\n\nahooks, pronounced [eɪ hʊks], is a high-quality and reliable React Hooks library. In the current React project development process, a set of easy-to-use React Hooks library is indispensable, hope ahooks can be your choice.\n\n## Features\n\n- Easy to learn and use\n- Supports SSR\n- Special treatment for functions, avoid closure problems\n- Contains a large number of advanced Hooks that are refined from business scenarios\n- Contains a comprehensive collection of basic Hooks\n- Written in TypeScript with predictable static types\n\n## Install\n\n```bash\n$ npm install --save ahooks\n# or\n$ yarn add ahooks\n# or\n$ pnpm add ahooks\n# or\n$ bun add ahooks\n```\n\n## Usage\n\n```ts\nimport { useRequest } from 'ahooks';\n```\n\n## Online Demo\n\n[![Edit demo for ahooks](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/demo-for-ahooks-forked-fg79k?file=/src/App.js)\n"
  },
  {
    "path": "docs/guide/index.zh-CN.md",
    "content": "# 介绍\n\nahooks，发音 [eɪ hʊks]，是一套高质量可靠的 React Hooks 库。在当前 React 项目研发过程中，一套好用的 React Hooks 库是必不可少的，希望 ahooks 能成为您的选择。\n\n## 特性\n\n- 易学易用\n- 支持 SSR\n- 对输入输出函数做了特殊处理，且避免闭包问题\n- 包含大量提炼自业务的高级 Hooks\n- 包含丰富的基础 Hooks\n- 使用 TypeScript 构建，提供完整的类型定义文件\n\n## 安装\n\n```bash\n$ npm install --save ahooks\n# or\n$ yarn add ahooks\n# or\n$ pnpm add ahooks\n# or\n$ bun add ahooks\n```\n\n## 使用\n\n```ts\nimport { useRequest } from 'ahooks';\n```\n\n## 💻 在线体验\n\n[![Edit demo for ahooks](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/demo-for-ahooks-forked-fg79k?file=/src/App.js)\n"
  },
  {
    "path": "docs/guide/upgrade.en-US.md",
    "content": "## v2 to v3\n\nCompared with the ahooks v2 version, the changes in the ahooks v3 version mainly include:\n\n- New `useRequest`\n- Support SSR\n- Special treatment for input and output functions to avoid closure problems\n- Hooks of DOM support dynamic target\n- Solved the problem in Strict Mode\n- Solved the problem in react-refresh (HMR) mode\n- Fixed known issues\n- Added more Hooks\n\n## Upgrade suggestion\n\nWe have released the `ahooks-v2` package, you can install v2 and v3 dependencies at the same time to transition upgrades.\n\n```bash\nnpm install ahooks-v2 --save\nnpm install ahooks --save\n```\n\n## New useRequest\n\nuseRequest has been rewritten:\n\n- Organized the source code through a plug-in pattern, the core code is extremely simple, and can be easily extended for more advanced features.\n- Provides step-by-step documentation.\n- Fixed the way of exception handling, provides `run` and `runAsync` two trigger functions.\n- The `options` parameter supports dynamic changes.\n- Deleted `pagination`, `loadMore`, `formatResult` options to avoid the overload of TypeScript, it is more convenient for encapsulating more advanced Hooks based on `useRequest`.\n\n### Detailed changes\n\n- Deleted `UseRequestProvider`, it is recommended to encapsulate advanced Hooks based on `useRequest` instead.\n- Removed `pagination` related options, it is recommended to use `usePagination` or `useAntdTable` to achieve paging ability.\n- Removed `loadMore` related options, it is recommended to use `useInfiniteScroll` to achieve unlimited loading ability.\n- Removed `fetchKey`, that is, deleted concurrent request.\n- Removed `formatResult`, `initialData`, and `throwOnError`.\n- The request library is no longer integrated by default, and `service` no longer supports string or object.\n- Added `runAsync` and `refreshAsync`, the original `run` no longer returns Promise.\n- Added error retry ability.\n- Added `onBefore` and `onFinally` life cycles.\n- Added cache clear ability.\n- All options support dynamic changes.\n- In debounce/throttle mode, `runAsync` can return current Promise.\n- Debounce/throttle mode supports more options.\n- Only successful request data will be cached.\n- Upgraded `ready` behavior\n\n[How is useRequest compatible with deleted capabilities?](#how-is-userequest-compatible-with-deleted-capabilities)\n\n## Support SSR\n\nahooks v3 fully supports SSR, and related documents can be found in \"[React Hooks & SSR](/guide/blog/ssr)\".\n\n## Hooks of DOM support dynamic target\n\nHooks of DOM support dynamic target, and related documents can be found in \"[Hooks of DOM specification](/guide/dom)\".\n\n## Avoid closure problems\n\nInside ahooks, we have made special treatment for the functions input by the user and the functions returned, to avoid the closure problem as much as possible.\n\n**The reference address of all output functions of ahooks will not change.**\n\n```ts\nconst [state, setState] = React.useState();\n```\n\nAs we all know, the `setState` reference address returned by `React.useState` will not change.\n\nAll functions returned in ahooks have the same characteristics as `setState`, and the reference address will not change.\n\n```ts\nconst [state, { toggle }] = useToggle();\n```\n\nFor example, the reference address of `toggle` function returned by `useToggle` is always stable.\n\n**All input functions of ahooks always use the latest one.**\n\nFor the received function, ahooks will do a special process to ensure that the function called each time is always the latest.\n\n```ts\nconst [state, setState] = useState();\n\nuseInterval(() => {\n  console.log(state);\n}, 1000);\n```\n\nFor example, in the above example, the function called by `useInterval` is always the latest.\n\nRelated documents can be found in \"[ahooks function specification](/guide/blog/function)\".\n\n## Support strict mode\n\nv3 fixed some problems in strict mode. Refer to \"[React Hooks & strict mode](/guide/blog/strict)\"\n\n## Support react-refresh (HMR) mode\n\nv3 fixed some problems in react-refresh (HMR) mode. Refer to \"[React Hooks & react-refresh (HMR)](/guide/blog/hmr)\"\n\n## More changes\n\n### New Hooks\n\n- [useRafState](/hooks/use-raf-state)\n- [useSetState](/hooks/use-set-state)\n- [useAsyncEffect](/hooks/use-async-effect)\n- [useDeepCompareEffect](/hooks/use-deep-compare-effect)\n- [useIsomorphicLayoutEffect](/hooks/use-isomorphic-layout-effect)\n- [useLatest](/hooks/use-latest)\n- [usePagination](/hooks/use-pagination)\n- [useLongPress](/hooks/use-long-press)\n- [useInfiniteScroll](/hooks/use-infinite-scroll)\n\n### Breaking Changes\n\n- useBoolean\n\n  - `toggle` no longer accepts parameters\n  - Added `set`\n\n- useToggle\n\n  - `toggle` no longer accepts parameters\n  - Added `set`\n\n- useSet\n\n  - Removed `has` method, use `state.has` instead\n\n- useCookieState\n\n  - `setState(null)` is no longer supported to delete cookies, please use `setState(undefined)` or `setState()` instead\n\n- useCountDown\n\n  - Deleted the return value of `setTargetDate`, you can dynamically change `options.targetDate` to achieve the same effect\n\n- useLocalStorageState / useSessionStorageState\n\n  - The second parameter changed from `defaultValue` to `Options`, use `options.defaultValue` instead\n  - Added `options.serializer` and `options.deserializer` to support custom sequence method\n\n- useDynamicList\n\n  - `sortForm` was renamed to `sortList`\n\n- useDrag & useDrop\n\n  - API is redesigned and needs to be upgraded according to the new document\n\n- useExternal\n\n  - API has undergone major adjustments, please refer to the documentation\n  - No longer supports image type resources\n  - The resource becomes globally unique and will not be loaded repeatedly. At the same time, if there are multiple references, the resource will be deleted only after all references are unloaded\n\n- useFullscreen\n\n  - API has been renamed, please refer to the documentation\n\n- useVirtualList\n\n  - API is redesigned and needs to be upgraded according to the new document\n  - Added a `data` parameter to the function type `options.itemHeight` parameter\n\n- useInViewport\n\n  - API has been upgraded, please refer to the documentation\n  - Added visible ratio ability\n\n- useScroll\n\n  - The return value type is changed from `{ left?: number, top?: number }` to `{ left: number, top: number } | undefined`\n\n- useSize\n\n  - The return value type is changed from `{ width?: number, height?: number }` to `{ width: number, height: number } | undefined`\n\n- useKeyPress\n\n  - All aliases have been modified, please refer to the documentation\n\n- useAntdTable\n\n  - Removed `options.formatResult`\n  - More changes are the same as useRequest\n\n- useFusionTable\n\n  - Removed `options.formatResult`\n  - More changes are the same as useRequest\n\n- usePersistFn was renamed to useMemoizedFn\n\n- Deprecated the useControlledValue naming left over from 1.0, please use useControllableValue instead\n\n### Optimization\n\n- useUrlState\n\n  - Supported React Router v6\n\n- useControllableValue\n\n  - Optimized logic to avoid unnecessary re-render\n\n- More other optimizations\n\n## FAQ\n\n### How is useRequest compatible with deleted capabilities?\n\nThe new version of useRequest only provides the underlying capabilities of Promise management, and more advanced capabilities can be supported by encapsulating advanced Hooks based on useRequest.\n\n1. `options.formatResult` is deleted, and the service is expected to return the data in the final format. for example:\n\n```ts\nconst { data } = useRequest(async () => {\n  const result = await getData();\n  return result.data;\n});\n```\n\n2. The original concurrent mode of `options.fetchKey` is deleted. It is expected that each request action and UI will be encapsulated as a component instead of placing all requests in the parent.\n\n3. `options.initialData` is deleted, you can do this\n\n```ts\nconst { data = initialData } = useRequest(getData);\n```\n\n4. The request library is no longer integrated by default, and `service` no longer supports string or object. It is expected to be supported by encapsulating advanced Hooks based on useReqeust. for example:\n\n```ts\nconst useCustomHooks = (pathname, options) => {\n  return useRequest(() => {\n    return axios(pathname);\n  }, options);\n};\n```\n"
  },
  {
    "path": "docs/guide/upgrade.zh-CN.md",
    "content": "## v2 to v3\n\n相较于 ahooks v2 版本，ahooks v3 版本的变更主要包括：\n\n- 全新的 `useRequest`\n- 全面支持 SSR\n- 对输入输出函数做特殊处理，避免闭包问题\n- DOM 类 Hooks 支持 target 动态变化\n- 解决了在严格模式（Strict Mode）下的问题\n- 解决了在 react-refresh（HMR）模式下的问题\n- 修复了已知问题\n- 新增了更多的 Hooks\n\n## 升级建议\n\n我们发布了 `ahooks-v2` 包，你可以同时安装 v2 和 v3 依赖，以过渡升级。\n\n```bash\nnpm install ahooks-v2 --save\nnpm install ahooks --save\n```\n\n## 全新的 useRequest\n\nuseRequest 完全进行了重写：\n\n- 通过插件式组织代码，核心代码极其简单，可以很方便的扩展出更高级的能力。\n- 提供了循序渐进的文档。\n- 彻底修复了异常处理方式，提供了 `run` 和 `runAsync` 两种触发函数。\n- `options` 参数支持动态变化。\n- 删除了 `pagination`、`loadMore`、`formatResult` 属性，避免了 `useRequest` TypeScript 重载，可以更方便的基于 `useRequest` 封装更高级的 Hooks。\n\n### 详细变更\n\n- 删除了 `UseRequestProvider`，建议自行基于 `useRequest` 封装高级 Hooks 来代替。\n- 删除了 `pagination` 相关属性，建议使用 `usePagination` 或 `useAntdTable` 来实现分页能力。\n- 删除了 `loadMore` 相关属性，建议使用 `useInfiniteScroll` 来实现无限加载能力。\n- 删除了 `fetchKey`，也就是删除了并行能力。\n- 删除了 `formatResult`、`initialData`、`throwOnError`。\n- 不再默认集成请求库，`service` 不再支持字符或对象。\n- 新增了 `runAsync` 和 `refreshAsync`，原来的 `run` 不再返回 Promise。\n- 新增了错误重试能力。\n- 新增了 `onBefore`、`onFinally` 生命周期。\n- 新增了缓存清理能力。\n- 所有参数支持动态变化。\n- 防抖/节流模式下，`runAsync` 可以返回正常 Promise。\n- 防抖/节流支持更多参数。\n- 只有成功的请求数据才会缓存。\n- `ready` 行为升级\n\n[被删除的参数如何兼容？](#userequest-被删除的能力如何兼容)\n\n## SSR 支持\n\nahooks v3 全面支持 SSR，相关文档可见《[React Hooks & SSR](/zh-CN/guide/blog/ssr)》。\n\n## DOM 类 Hooks 支持 target 动态变化\n\nDOM 类 Hooks 支持 target 动态变化，相关文档可见《[DOM 类 Hooks 使用规范](/zh-CN/guide/dom)》\n\n## 避免闭包问题\n\nahooks v3 通过对输入输出函数做特殊处理，尽力帮助大家避免闭包问题。\n\n**所有的输出函数，地址是不会变化的。**\n\n```ts\nconst [state, setState] = React.useState();\n```\n\n大家熟知的`React.useState`返回的 `setState` 函数，地址是不会变化的。\n\nv3 所有 Hooks 返回的函数，也有和 `setState` 一样的特性，地址不会变化。\n\n```ts\nconst [state, { toggle }] = useToggle();\n```\n\n比如 `useToggle` 返回的 `toggle` 函数，地址就是永远固定的。\n\n**所有的输入函数，永远使用最新的一份。**\n\n对于接收的函数，v3 会做一次特殊处理，保证每次调用的函数永远是最新的。\n\n```ts\nconst [state, setState] = useState();\n\nuseInterval(() => {\n  console.log(state);\n}, 1000);\n```\n\n比如以上示例，`useInterval` 调用的函数永远是最新的。\n\n相关文档可见《[ahooks 输入输出函数处理规范](/zh-CN/guide/blog/function)》。\n\n## 支持严格模式\n\nv3 修复了在严格模式下的一些问题。参考《[React Hooks & strict mode](/zh-CN/guide/blog/strict)》\n\n## 支持 react-refresh（HMR）模式\n\nv3 修复了在 react-refresh（HMR）模式下的一些问题。参考《[React Hooks & react-refresh（HMR）](/zh-CN/guide/blog/hmr)》\n\n## 更多变更\n\n### 新增 Hooks\n\n- [useRafState](/zh-CN/hooks/use-raf-state)\n- [useSetState](/zh-CN/hooks/use-set-state)\n- [useAsyncEffect](/zh-CN/hooks/use-async-effect)\n- [useDeepCompareEffect](/zh-CN/hooks/use-deep-compare-effect)\n- [useIsomorphicLayoutEffect](/zh-CN/hooks/use-isomorphic-layout-effect)\n- [useLatest](/zh-CN/hooks/use-latest)\n- [usePagination](/zh-CN/hooks/use-pagination)\n- [useLongPress](/zh-CN/hooks/use-long-press)\n- [useInfiniteScroll](/zh-CN/hooks/use-infinite-scroll)\n\n### Breaking Changes\n\n- useBoolean\n\n  - `toggle` 不再接收参数\n  - 增加了 `set`\n\n- useToggle\n\n  - `toggle` 不再接收参数\n  - 增加了 `set`\n\n- useSet\n\n  - 删除了 `has` 方法，使用 `state.has` 代替\n\n- useCookieState\n\n  - 不再支持 `setState(null)` 删除 Cookie，请使用 `setState(undefined)` 或 `setState()` 替代\n\n- useCountDown\n\n  - 删除了 `setTargetDate` 返回值，可以动态改变 `options.targetDate` 实现相同效果\n\n- useLocalStorageState / useSessionStorageState\n\n  - 第二个参数从 `defaultValue` 变为了 `Options`，使用 `options.defaultValue` 代替\n  - 增加了 `options.serializer` 和 `options.deserializer`，支持自定义序列法方法\n\n- useDynamicList\n\n  - `sortForm` 改名为 `sortList`\n\n- useDrag & useDrop\n\n  - API 重新设计，需要对照新的文档做升级\n\n- useExternal\n\n  - API 进行了比较大的调整，请查阅文档\n  - 不再支持图片类型资源\n  - 资源在全局变成唯一的，不会重复加载，同时如果有多处引用，只有等全部引用卸载之后，才会删除该资源\n\n- useFullscreen\n\n  - API 进行了重命名，请查阅文档\n\n- useVirtualList\n\n  - API 重新设计，需要对照新的文档做升级\n  - `options.itemHeight` 函数型参数增加了 `data` 参数\n\n- useInViewport\n\n  - API 进行了升级，请查阅文档\n  - 增加了可见比例能力\n\n- useScroll\n\n  - 返回值类型从 `{ left?: number, top?: number }` 改为 `{ left: number, top: number } | undefined`\n\n- useSize\n\n  - 返回值类型从 `{ width?: number, height?: number }` 改为 `{ width: number, height: number } | undefined`\n\n- useKeyPress\n\n  - 修改了所有别名，请查阅文档\n\n- useAntdTable\n\n  - 删除了 `options.formatResult`\n  - 更多变更同 useRequest\n\n- useFusionTable\n\n  - 删除了 `options.formatResult`\n  - 更多变更同 useRequest\n\n- usePersistFn 更名为 useMemoizedFn\n\n- 废弃了 1.0 遗留的 useControlledValue 命名，请使用 useControllableValue 代替\n\n### 优化\n\n- useUrlState\n\n  - 支持了 React Router v6\n\n- useControllableValue\n\n  - 优化了代码逻辑，避免了不必要的 re-render\n\n- 更多其它优化\n\n## FAQ\n\n### useRequest 被删除的能力如何兼容？\n\n新版 useRequest 只做 Promise 管理的底层能力，更多高级能力可以基于 useRequest 封装高级 Hooks 来支持。\n\n1. 原 `options.formatResult` 删除，期望 service 返回最终格式的数据。比如：\n\n```ts\nconst { data } = useRequest(async () => {\n  const result = await getData();\n  return result.data;\n});\n```\n\n2. 原 `options.fetchKey` 并行模式删除，期望将每个请求动作和 UI 封装为一个组件，而不是把所有请求都放到父级。\n\n3. 原 `options.initialData` 删除，可以这样做\n\n```ts\nconst { data = initialData } = useRequest(getData);\n```\n\n4. 不再默认集成请求库，`service` 不再支持字符或对象。期望基于 useReqeust 封装高级 Hooks 来支持。比如：\n\n```ts\nconst useCustomHooks = (pathname, options) => {\n  return useRequest(() => {\n    return axios(pathname);\n  }, options);\n};\n```\n"
  },
  {
    "path": "docs/index.en-US.md",
    "content": "---\ntitle: ahooks - React Hooks Library\nhero:\n  image: /logo.svg\n  desc: A high-quality & reliable React Hooks library\n  actions:\n    - text: Guide\n      link: /guide\n    - text: Hooks List\n      link: /hooks\nfooter: Open-source MIT Licensed | Copyright © 2019-present<br />Powered by [dumi](https://d.umijs.org)\n---\n\n[![NPM version][image-1]][1]\n[![NPM downloads][image-2]][2]\n[![npm](https://img.shields.io/npm/dw/ahooks-v2?label=downloads%28v2%29)](https://www.npmjs.com/package/ahooks-v2)\n[![npm](https://img.shields.io/github/issues/alibaba/hooks)](https://github.com/alibaba/hooks/issues)\n[![Coverage Status](https://coveralls.io/repos/github/alibaba/hooks/badge.svg?branch=master)](https://coveralls.io/github/alibaba/hooks?branch=master)\n![gzip size](https://img.badgesize.io/https:/unpkg.com/ahooks/dist/ahooks.js?label=gzip%20size&compression=gzip)\n[![Percentage of issues still open](http://isitmaintained.com/badge/open/alibaba/hooks.svg)](http://isitmaintained.com/project/alibaba/hooks \"Percentage of issues still open\")\n[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/alibaba/hooks.svg)](http://isitmaintained.com/project/alibaba/hooks \"Average time to resolve an issue\")\n![GitHub](https://img.shields.io/github/license/alibaba/hooks)\n\n## ✨ Features\n\n- Easy to learn and use\n- Supports SSR\n- Special treatment for functions, avoid closure problems\n- Contains a large number of advanced Hooks that are refined from business scenarios\n- Contains a comprehensive collection of basic Hooks\n- Written in TypeScript with predictable static types\n\n\n## 📦 Install\n\n```bash\n$ npm install --save ahooks\n# or\n$ yarn add ahooks\n# or\n$ pnpm add ahooks\n# or\n$ bun add ahooks\n```\n\n## 🔨 Usage\n\n```ts\nimport { useRequest } from \"ahooks\";\n```\n\n## 💻 Online Demo\n\n[![Edit demo for ahooks](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/demo-for-ahooks-forked-fg79k?file=/src/App.js)\n\n## 🤝 Contributing\n\n```bash\n$ git clone git@github.com:alibaba/hooks.git\n$ cd hooks\n$ pnpm run init\n$ pnpm start\n```\n\nOpen your browser and visit http://127.0.0.1:8000\n\nWe welcome all contributions, please read our [CONTRIBUTING.MD](https://github.com/alibaba/hooks/blob/master/CONTRIBUTING.MD) first, let's build a better hooks library together.\n\nThanks to all the contributors:\n\n<a href=\"https://github.com/alibaba/hooks/graphs/contributors\">\n  <img src=\"https://opencollective.com/ahooks/contributors.svg?width=960&button=false\" alt=\"contributors\" />\n</a>\n\n## 👥 Discuss\n\n<img alt=\"ahooks discussion group 1\" src=\"https://github.com/user-attachments/assets/0ba7a370-2a69-442f-b746-9eb16bbbc46c\" width=\"200\" style='display:inline' />\n<img alt=\"ahooks discussion group 2\" src=\"https://github.com/user-attachments/assets/a08693d3-bfcc-4aca-b2b0-2d9c23012858\" width=\"200\" style='display:inline' />\n<img alt=\"ahooks discussion group 3\" src=\"https://github.com/user-attachments/assets/15a505a7-06d1-4e72-ab02-6fad968323f1\" width=\"200\" style='display:inline' />\n\n[1]: https://www.npmjs.com/package/ahooks\n[2]: https://npmjs.org/package/ahooks\n[image-1]: https://img.shields.io/npm/v/ahooks.svg?style=flat\n[image-2]: https://img.shields.io/npm/dm/ahooks.svg?style=flat\n"
  },
  {
    "path": "docs/index.zh-CN.md",
    "content": "---\ntitle: ahooks - React Hooks Library\nhero:\n  image: /logo.svg\n  desc: 一套高质量可靠的 React Hooks 库\n  actions:\n    - text: 指南\n      link: /zh-CN/guide\n    - text: Hooks 列表\n      link: /zh-CN/hooks\nfooter: Open-source MIT Licensed | Copyright © 2019-present<br />Powered by [dumi](https://d.umijs.org)\n---\n\n[![NPM version][image-1]][1]\n[![NPM downloads][image-2]][2]\n[![npm](https://img.shields.io/npm/dw/ahooks-v2?label=downloads%28v2%29)](https://www.npmjs.com/package/ahooks-v2)\n[![npm](https://img.shields.io/github/issues/alibaba/hooks)](https://github.com/alibaba/hooks/issues)\n[![Coverage Status](https://coveralls.io/repos/github/alibaba/hooks/badge.svg?branch=master)](https://coveralls.io/github/alibaba/hooks?branch=master)\n![gzip size](https://img.badgesize.io/https:/unpkg.com/ahooks/dist/ahooks.js?label=gzip%20size&compression=gzip)\n[![Percentage of issues still open](http://isitmaintained.com/badge/open/alibaba/hooks.svg)](http://isitmaintained.com/project/alibaba/hooks \"Percentage of issues still open\")\n[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/alibaba/hooks.svg)](http://isitmaintained.com/project/alibaba/hooks \"Average time to resolve an issue\")\n![GitHub](https://img.shields.io/github/license/alibaba/hooks)\n\n## ✨ 特性\n\n- 易学易用\n- 支持 SSR\n- 对输入输出函数做了特殊处理，避免闭包问题\n- 包含大量提炼自业务的高级 Hooks\n- 包含丰富的基础 Hooks\n- 使用 TypeScript 构建，提供完整的类型定义文件\n\n## 📦 安装\n\n```bash\n$ npm install --save ahooks\n# or\n$ yarn add ahooks\n# or\n$ pnpm add ahooks\n# or\n$ bun add ahooks\n```\n\n## 🔨 使用\n\n```ts\nimport { useRequest } from \"ahooks\";\n```\n\n## 💻 在线体验\n\n[![Edit demo for ahooks](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/demo-for-ahooks-forked-fg79k?file=/src/App.js)\n\n## 🤝 参与共建\n\n```bash\n$ git clone git@github.com:alibaba/hooks.git\n$ cd hooks\n$ pnpm run init\n$ pnpm start\n```\n\n打开浏览器访问 http://127.0.0.1:8000\n\n我们欢迎所有人参与共建，请参考[CONTRIBUTING.MD](https://github.com/alibaba/hooks/blob/master/CONTRIBUTING.zh-CN.MD)\n\n感谢所有贡献者：\n\n<a href=\"https://github.com/alibaba/hooks/graphs/contributors\">\n  <img src=\"https://opencollective.com/ahooks/contributors.svg?width=960&button=false\" alt=\"contributors\" />\n</a>\n\n## 👥 交流讨论\n\n<img alt=\"ahooks 交流群1\" src=\"https://github.com/user-attachments/assets/0ba7a370-2a69-442f-b746-9eb16bbbc46c\" width=\"200\" style='display:inline' />\n<img alt=\"ahooks 交流群2\" src=\"https://github.com/user-attachments/assets/a08693d3-bfcc-4aca-b2b0-2d9c23012858\" width=\"200\" style='display:inline' />\n<img alt=\"ahooks 交流群3\" src=\"https://github.com/user-attachments/assets/15a505a7-06d1-4e72-ab02-6fad968323f1\" width=\"200\" style='display:inline' />\n\n[1]: https://www.npmjs.com/package/ahooks\n[2]: https://npmjs.org/package/ahooks\n[image-1]: https://img.shields.io/npm/v/ahooks.svg?style=flat\n[image-2]: https://img.shields.io/npm/dm/ahooks.svg?style=flat\n"
  },
  {
    "path": "example/.gitkeep",
    "content": "import React from 'react';\r\nimport { useBoolean } from 'ahooks';\r\n\r\nexport default function Demo() {\r\n  const [state, { toggle, setTrue, setFalse }] = useBoolean(false);\r\n\r\n  return (\r\n    <div>\r\n      <p>Current state: {state ? 'ON' : 'OFF'}</p>\r\n      <button onClick={toggle}>Toggle</button>\r\n      <button onClick={setTrue}>Set True</button>\r\n      <button onClick={setFalse}>Set False</button>\r\n    </div>\r\n  );\r\n}\r\n"
  },
  {
    "path": "gulpfile.js",
    "content": "const gulp = require('gulp');\nconst babel = require('gulp-babel');\nconst ts = require('gulp-typescript');\nconst del = require('del');\n\ngulp.task('clean', async () => {\n  await del('lib/**');\n  await del('es/**');\n  await del('dist/**');\n});\n\ngulp.task('cjs', () =>\n  gulp\n    .src(['./es/**/*.js'])\n    .pipe(\n      babel({\n        configFile: '../../.babelrc',\n      }),\n    )\n    .pipe(gulp.dest('lib/')),\n);\n\ngulp.task('es', async () => {\n  const { execSync } = require('child_process');\n\n  // 使用 tsc 直接编译\n  console.log('Running TypeScript compilation...');\n  execSync('npx tsc --project tsconfig.pro.json --outDir es --module esnext', { stdio: 'inherit' });\n  console.log('TypeScript compilation completed');\n\n  // 然后运行 babel 转换\n  console.log('Running Babel transformation...');\n  return gulp\n    .src(['es/**/*.js'])\n    .pipe(\n      babel({\n        configFile: './.babelrc',\n      }),\n    )\n    .pipe(gulp.dest('es/'));\n});\n\ngulp.task('declaration', () => {\n  const tsProject = ts.createProject('tsconfig.pro.json', {\n    declaration: true,\n    emitDeclarationOnly: true,\n  });\n  return tsProject.src().pipe(tsProject()).pipe(gulp.dest('es/')).pipe(gulp.dest('lib/'));\n});\n\ngulp.task('copyReadme', async () => {\n  await gulp.src('../../README.md').pipe(gulp.dest('../../packages/hooks'));\n});\n\nexports.default = gulp.series('clean', 'es', 'cjs', 'declaration', 'copyReadme');\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"ahooks\",\n  \"private\": true,\n  \"packageManager\": \"pnpm@10.12.4\",\n  \"engines\": {\n    \"pnpm\": \">=7 <=10\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/alibaba/hooks.git\"\n  },\n  \"scripts\": {\n    \"init\": \"pnpm install && pnpm run build\",\n    \"start\": \"pnpm run dev\",\n    \"dev\": \"cross-env NODE_OPTIONS=--openssl-legacy-provider dumi dev\",\n    \"clean-dist\": \"rimraf 'packages/*/{lib,es,node_modules,dist}'\",\n    \"clean\": \"pnpm run clean-dist && rimraf node_modules\",\n    \"build\": \"pnpm -r --filter=./packages/* run build\",\n    \"test\": \"pnpm --filter=./packages/* test\",\n    \"test:strict\": \"cross-env REACT_MODE=strict pnpm --filter=./packages/* test:cov\",\n    \"coveralls\": \"vitest run --coverage | coveralls\",\n    \"lint\": \"biome lint --fix\",\n    \"pretty\": \"biome format --fix --no-errors-on-unmatched\",\n    \"build:doc\": \"cross-env NODE_OPTIONS=--openssl-legacy-provider dumi build\",\n    \"build:doc-github\": \"node scripts/build-with-relative-paths.js\",\n    \"pub:doc-surge\": \"surge ./dist --domain ahooks.js.org\",\n    \"pub:doc-gitee\": \"cd ./dist && rm -rf .git && touch .spa && touch .nojekyll && git init && git remote add origin git@gitee.com:ahooks/ahooks.git && git add -A && git commit -m \\\"publish docs\\\" && git push origin main -f && echo https://gitee.com/ahooks/ahooks/pages\",\n    \"pub:doc\": \"pnpm run build:doc && pnpm run pub:doc-surge && pnpm run build:doc-github\",\n    \"pub\": \"pnpm run build && pnpm -r --filter=./packages/* publish\",\n    \"pub:beta\": \"pnpm run build && pnpm -r --filter=./packages/* publish --tag beta\",\n    \"preinstall\": \"npx only-allow pnpm\",\n    \"prepare\": \"husky install\",\n    \"commit\": \"git add -A && czg\",\n    \"tsc\": \"pnpm --filter=./packages/* tsc\"\n  },\n  \"devDependencies\": {\n    \"@alifd/next\": \"^1.27.32\",\n    \"@ant-design/icons\": \"^5.6.1\",\n    \"@babel/cli\": \"^7.10.1\",\n    \"@babel/core\": \"^7.10.2\",\n    \"@babel/plugin-transform-runtime\": \"^7.19.6\",\n    \"@biomejs/biome\": \"^2.0.6\",\n    \"@commitlint/cli\": \"^17.1.2\",\n    \"@commitlint/config-conventional\": \"^17.1.0\",\n    \"@testing-library/react\": \"^16.3.0\",\n    \"@types/lodash\": \"^4.17.20\",\n    \"@types/mockjs\": \"^1.0.7\",\n    \"@types/react\": \"^19.1.8\",\n    \"@types/react-dom\": \"^19.1.6\",\n    \"@types/react-router\": \"^5.1.19\",\n    \"@umijs/fabric\": \"^2.1.0\",\n    \"@vitest/coverage-istanbul\": \"^3.2.4\",\n    \"antd\": \"^5.26.3\",\n    \"babel-plugin-import\": \"^1.12.0\",\n    \"coveralls\": \"^3.1.1\",\n    \"cross-env\": \"^7.0.3\",\n    \"czg\": \"^1.12.0\",\n    \"del\": \"^5.1.0\",\n    \"dumi\": \"^1.1.54\",\n    \"fast-glob\": \"^3.2.11\",\n    \"fs-extra\": \"^10.0.1\",\n    \"gray-matter\": \"^4.0.3\",\n    \"gulp\": \"^4.0.2\",\n    \"gulp-babel\": \"^8.0.0\",\n    \"gulp-typescript\": \"^6.0.0-alpha.1\",\n    \"husky\": \"^8.0.0\",\n    \"jsdom\": \"^26.1.0\",\n    \"mockjs\": \"^1.1.0\",\n    \"react\": \"^19.1.0\",\n    \"react-dom\": \"^19.1.0\",\n    \"react-drag-listview\": \"^0.1.6\",\n    \"react-json-view\": \"^1.21.3\",\n    \"react-router\": \"^6.4.2\",\n    \"react-shadow\": \"^20.6.0\",\n    \"rimraf\": \"^3.0.2\",\n    \"surge\": \"^0.21.3\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\",\n    \"vitest-websocket-mock\": \"^0.5.0\",\n    \"webpack\": \"^5.99.9\",\n    \"webpack-cli\": \"^6.0.1\",\n    \"webpack-merge\": \"^6.0.1\"\n  },\n  \"commitlint\": {\n    \"extends\": [\n      \"@commitlint/config-conventional\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/hooks/gulpfile.js",
    "content": "const commonConfig = require('../../gulpfile');\nconst gulp = require('gulp');\nconst fs = require('fs');\nconst fse = require('fs-extra');\nconst fg = require('fast-glob');\nconst gm = require('gray-matter');\n\nfunction camelToKebab(str) {\n  return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();\n}\n\nasync function genDesc(mdPath) {\n  if (!fs.existsSync(mdPath)) {\n    return;\n  }\n  const mdFile = fs.readFileSync(mdPath, 'utf8');\n  const { content } = gm(mdFile);\n  let description =\n    (content.replace(/\\r\\n/g, '\\n').match(/# \\w+[\\s\\n]+(.+?)(?:, |\\. |\\n|\\.\\n)/m) || [])[1] || '';\n\n  description = description.trim();\n  description = description.charAt(0).toLowerCase() + description.slice(1);\n  return description;\n}\n\nasync function genMetaData() {\n  const metadata = {\n    functions: [],\n  };\n  const hooks = fg\n    .sync('src/use*', {\n      onlyDirectories: true,\n    })\n    .map((hook) => hook.replace('src/', ''))\n    .sort();\n  await Promise.allSettled(\n    hooks.map(async (hook) => {\n      const description = await genDesc(`src/${hook}/index.en-US.md`);\n      return {\n        name: hook,\n        docs: `https://ahooks.js.org/hooks/${camelToKebab(hook)}`,\n        description,\n      };\n    }),\n  ).then((res) => {\n    metadata.functions = res.map((item) => {\n      if (item.status === 'fulfilled') {\n        return item.value;\n      }\n      return null;\n    });\n  });\n  return metadata;\n}\n\ngulp.task('metadata', async function () {\n  const metadata = await genMetaData();\n  await fse.writeJson('metadata.json', metadata, { spaces: 2 });\n});\n\nexports.default = gulp.series(commonConfig.default, 'metadata');\n"
  },
  {
    "path": "packages/hooks/package.json",
    "content": "{\n  \"name\": \"ahooks\",\n  \"version\": \"3.9.6\",\n  \"description\": \"react hooks library\",\n  \"keywords\": [\n    \"ahooks\",\n    \"umi hooks\",\n    \"react hooks\"\n  ],\n  \"main\": \"./lib/index.js\",\n  \"module\": \"./es/index.js\",\n  \"types\": \"./lib/index.d.ts\",\n  \"unpkg\": \"dist/ahooks.js\",\n  \"sideEffects\": false,\n  \"authors\": {\n    \"name\": \"brickspert\",\n    \"email\": \"brickspert.fjl@alipay.com\"\n  },\n  \"publishConfig\": {\n    \"registry\": \"https://registry.npmjs.org/\"\n  },\n  \"repository\": \"https://github.com/alibaba/hooks\",\n  \"homepage\": \"https://github.com/alibaba/hooks\",\n  \"scripts\": {\n    \"build\": \"gulp && webpack-cli\",\n    \"test\": \"vitest run --color\",\n    \"test:cov\": \"vitest run --color --coverage\",\n    \"tsc\": \"tsc --noEmit\"\n  },\n  \"files\": [\n    \"dist\",\n    \"lib\",\n    \"es\",\n    \"metadata.json\",\n    \"package.json\",\n    \"README.md\"\n  ],\n  \"dependencies\": {\n    \"@types/js-cookie\": \"^3.0.6\",\n    \"@babel/runtime\": \"^7.21.0\",\n    \"dayjs\": \"^1.9.1\",\n    \"intersection-observer\": \"^0.12.0\",\n    \"js-cookie\": \"^3.0.5\",\n    \"lodash\": \"^4.17.21\",\n    \"react-fast-compare\": \"^3.2.2\",\n    \"resize-observer-polyfill\": \"^1.5.1\",\n    \"screenfull\": \"^5.0.0\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\",\n    \"react-dom\": \"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\"\n  },\n  \"license\": \"MIT\",\n  \"gitHead\": \"11f6ad571bd365c95ecb9409ca3050cbbfc9b34a\"\n}\n"
  },
  {
    "path": "packages/hooks/src/createDeepCompareEffect/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { useEffect, useLayoutEffect, useState } from 'react';\nimport { describe, expect, test } from 'vitest';\nimport { createDeepCompareEffect } from '../index';\n\ndescribe('createDeepCompareEffect', () => {\n  test('should work for useEffect', async () => {\n    const useDeepCompareEffect = createDeepCompareEffect(useEffect);\n\n    const hook = renderHook(() => {\n      const [x, setX] = useState(0);\n      const [y, setY] = useState({ foo: 'foo', bar: ['baz'] });\n      useDeepCompareEffect(() => {\n        setX((prevX) => prevX + 1);\n      }, [y]);\n      return { x, setY };\n    });\n\n    expect(hook.result.current.x).toBe(1);\n\n    await act(async () => {\n      hook.result.current.setY({ foo: 'foo', bar: ['baz'] });\n    });\n\n    expect(hook.result.current.x).toBe(1);\n\n    await act(async () => {\n      hook.result.current.setY({ foo: 'foo', bar: ['bazz'] });\n    });\n\n    expect(hook.result.current.x).toBe(2);\n  });\n\n  test('should work for useLayoutEffect', async () => {\n    const useDeepCompareLayoutEffect = createDeepCompareEffect(useLayoutEffect);\n\n    const hook = renderHook(() => {\n      const [x, setX] = useState(0);\n      const [y, setY] = useState({ foo: 'foo', bar: ['baz'] });\n      useDeepCompareLayoutEffect(() => {\n        setX((prevX) => prevX + 1);\n      }, [y]);\n      return { x, setY };\n    });\n\n    expect(hook.result.current.x).toBe(1);\n\n    await act(async () => {\n      hook.result.current.setY({ foo: 'foo', bar: ['baz'] });\n    });\n\n    expect(hook.result.current.x).toBe(1);\n\n    await act(async () => {\n      hook.result.current.setY({ foo: 'foo', bar: ['bazz'] });\n    });\n\n    expect(hook.result.current.x).toBe(2);\n  });\n\n  test('deps is undefined should rerender in useEffect', async () => {\n    const useDeepCompareLayoutEffect = createDeepCompareEffect(useEffect);\n    let count = 0;\n    const hook = renderHook(() => {\n      useDeepCompareLayoutEffect(() => {\n        count++;\n      });\n    });\n\n    expect(count).toBe(1);\n    hook.rerender();\n    expect(count).toBe(2);\n    hook.rerender();\n    expect(count).toBe(3);\n  });\n\n  test('deps is undefined should rerender in useLayoutEffect', async () => {\n    const useDeepCompareLayoutEffect = createDeepCompareEffect(useLayoutEffect);\n    let count = 0;\n    const hook = renderHook(() => {\n      useDeepCompareLayoutEffect(() => {\n        count++;\n      });\n    });\n\n    expect(count).toBe(1);\n    hook.rerender();\n    expect(count).toBe(2);\n    hook.rerender();\n    expect(count).toBe(3);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/createDeepCompareEffect/index.ts",
    "content": "import { useRef } from 'react';\nimport type { DependencyList, useEffect, useLayoutEffect } from 'react';\nimport { depsEqual } from '../utils/depsEqual';\n\ntype EffectHookType = typeof useEffect | typeof useLayoutEffect;\n\ntype CreateUpdateEffect = (hook: EffectHookType) => EffectHookType;\n\nexport const createDeepCompareEffect: CreateUpdateEffect = (hook) => (effect, deps) => {\n  const ref = useRef<DependencyList>(undefined);\n  const signalRef = useRef<number>(0);\n  if (deps === undefined || !depsEqual(deps, ref.current)) {\n    signalRef.current += 1;\n  }\n  ref.current = deps;\n  hook(effect, [signalRef.current]);\n};\n"
  },
  {
    "path": "packages/hooks/src/createUpdateEffect/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { useEffect, useLayoutEffect } from 'react';\nimport { describe, expect, test } from 'vitest';\nimport { createUpdateEffect } from '../index';\n\ndescribe('createUpdateEffect', () => {\n  test('should work for useEffect', () => {\n    const useUpdateEffect = createUpdateEffect(useEffect);\n\n    let mountedState = 1;\n    const hook = renderHook(() =>\n      useUpdateEffect(() => {\n        mountedState = 2;\n      }),\n    );\n    expect(mountedState).toBe(1);\n    hook.rerender();\n    expect(mountedState).toBe(2);\n  });\n\n  test('should work for useLayoutEffect', () => {\n    const useUpdateLayoutEffect = createUpdateEffect(useLayoutEffect);\n\n    let mountedState = 1;\n    const hook = renderHook(() =>\n      useUpdateLayoutEffect(() => {\n        mountedState = 2;\n      }),\n    );\n    expect(mountedState).toBe(1);\n    hook.rerender();\n    expect(mountedState).toBe(2);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/createUpdateEffect/index.ts",
    "content": "import { useRef } from 'react';\nimport type { useEffect, useLayoutEffect } from 'react';\n\ntype EffectHookType = typeof useEffect | typeof useLayoutEffect;\n\nexport const createUpdateEffect: (hook: EffectHookType) => EffectHookType =\n  (hook) => (effect, deps) => {\n    const isMounted = useRef(false);\n\n    // for react-refresh\n    hook(() => {\n      return () => {\n        isMounted.current = false;\n      };\n    }, []);\n\n    hook(() => {\n      if (!isMounted.current) {\n        isMounted.current = true;\n      } else {\n        return effect();\n      }\n    }, deps);\n  };\n\nexport default createUpdateEffect;\n"
  },
  {
    "path": "packages/hooks/src/createUseStorageState/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport type { Options } from '../index';\nimport { createUseStorageState } from '../index';\n\nclass TestStorage implements Storage {\n  [name: string]: any;\n\n  length = 0;\n\n  _values = new Map<string, string>();\n\n  clear(): void {\n    this._values.clear();\n    this.length = 0;\n  }\n\n  getItem(key: string): string | null {\n    return this._values.get(key) || null;\n  }\n\n  key(index: number): string | null {\n    if (index >= this._values.size) {\n      return null;\n    }\n\n    return Array.from(this._values.keys())[index];\n  }\n\n  removeItem(key: string): void {\n    if (this._values.delete(key)) {\n      this.length -= 1;\n    }\n  }\n\n  setItem(key: string, value: string): void {\n    if (!this._values.has(key)) {\n      this.length += 1;\n    }\n\n    this._values.set(key, value);\n  }\n}\n\ninterface StorageStateProps<T> extends Pick<Options<T>, 'defaultValue'> {\n  key: string;\n}\n\ndescribe('useStorageState', () => {\n  const setUp = <T>(props: StorageStateProps<T>) => {\n    const storage = new TestStorage();\n    const useStorageState = createUseStorageState(() => storage);\n\n    return renderHook(\n      ({ key, defaultValue }: StorageStateProps<T>) => {\n        const [state, setState] = useStorageState(key, { defaultValue });\n\n        return { state, setState };\n      },\n      {\n        initialProps: props,\n      },\n    );\n  };\n\n  test('should get defaultValue for a given key', () => {\n    const hook = setUp({ key: 'key1', defaultValue: 'value1' });\n    expect(hook.result.current.state).toBe('value1');\n\n    hook.rerender({ key: 'key2', defaultValue: 'value2' });\n    expect(hook.result.current.state).toBe('value2');\n  });\n\n  test('should get default and set value for a given key', () => {\n    const hook = setUp({ key: 'key', defaultValue: 'defaultValue' });\n    expect(hook.result.current.state).toBe('defaultValue');\n    act(() => {\n      hook.result.current.setState('setValue');\n    });\n    expect(hook.result.current.state).toBe('setValue');\n    hook.rerender({ key: 'key' });\n    expect(hook.result.current.state).toBe('setValue');\n  });\n\n  test('should remove value for a given key', () => {\n    const hook = setUp({ key: 'key' });\n    act(() => {\n      hook.result.current.setState('value');\n    });\n    expect(hook.result.current.state).toBe('value');\n    act(() => {\n      hook.result.current.setState(undefined);\n    });\n    expect(hook.result.current.state).toBeUndefined();\n\n    act(() => hook.result.current.setState('value'));\n    expect(hook.result.current.state).toBe('value');\n    act(() => hook.result.current.setState(undefined));\n    expect(hook.result.current.state).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/createUseStorageState/index.ts",
    "content": "import { useRef, useState } from 'react';\nimport useEventListener from '../useEventListener';\nimport useMemoizedFn from '../useMemoizedFn';\nimport useUpdateEffect from '../useUpdateEffect';\nimport { isFunction, isUndef } from '../utils';\n\nexport const SYNC_STORAGE_EVENT_NAME = 'AHOOKS_SYNC_STORAGE_EVENT_NAME';\n\nexport type SetState<S> = S | ((prevState?: S) => S);\n\nexport interface Options<T> {\n  defaultValue?: T | (() => T);\n  listenStorageChange?: boolean;\n  serializer?: (value: T) => string;\n  deserializer?: (value: string) => T;\n  onError?: (error: unknown) => void;\n}\n\nexport const createUseStorageState = (getStorage: () => Storage | undefined) => {\n  const useStorageState = <T>(key: string, options: Options<T> = {}) => {\n    let storage: Storage | undefined;\n\n    const { listenStorageChange = false } = options;\n\n    const serializer = isFunction(options.serializer) ? options.serializer : JSON.stringify;\n\n    const deserializer = isFunction(options.deserializer) ? options.deserializer : JSON.parse;\n\n    const onError = isFunction(options.onError) ? options.onError : console.error;\n\n    // https://github.com/alibaba/hooks/issues/800\n    try {\n      storage = getStorage();\n    } catch (err) {\n      onError(err);\n    }\n\n    const getStoredValue = () => {\n      try {\n        const raw = storage?.getItem(key);\n        if (raw) {\n          return deserializer(raw);\n        }\n      } catch (e) {\n        onError(e);\n      }\n      if (isFunction(options.defaultValue)) {\n        return options.defaultValue();\n      }\n      return options.defaultValue;\n    };\n\n    const [state, setState] = useState<T>(getStoredValue);\n\n    const stateRef = useRef<T>(state);\n    stateRef.current = state;\n\n    useUpdateEffect(() => {\n      const nextState = getStoredValue();\n      if (Object.is(nextState, stateRef.current)) {\n        return; // 新旧状态相同，不更新 state，避免 setState 带来不必要的 re-render\n      }\n      stateRef.current = nextState;\n      setState(nextState);\n    }, [key]);\n\n    const updateState = (value: SetState<T>) => {\n      const previousState = stateRef.current;\n      const currentState = isFunction(value) ? value(previousState) : value;\n\n      if (Object.is(currentState, previousState)) {\n        return; // 新旧状态相同，不更新 state，避免 setState 带来不必要的 re-render\n      }\n\n      if (!listenStorageChange) {\n        stateRef.current = currentState;\n        setState(currentState);\n      }\n\n      try {\n        let newValue: string | null;\n        const oldValue = storage?.getItem(key);\n\n        if (isUndef(currentState)) {\n          newValue = null;\n          storage?.removeItem(key);\n        } else {\n          newValue = serializer(currentState);\n          storage?.setItem(key, newValue);\n        }\n\n        dispatchEvent(\n          // send custom event to communicate within same page\n          // importantly this should not be a StorageEvent since those cannot\n          // be constructed with a non-built-in storage area\n          new CustomEvent(SYNC_STORAGE_EVENT_NAME, {\n            detail: {\n              key,\n              newValue,\n              oldValue,\n              storageArea: storage,\n            },\n          }),\n        );\n      } catch (e) {\n        onError(e);\n      }\n    };\n\n    const syncState = (event: StorageEvent) => {\n      if (event.key !== key || event.storageArea !== storage) {\n        return;\n      }\n\n      const nextState = getStoredValue();\n\n      if (Object.is(nextState, stateRef.current)) {\n        return; // 新旧状态相同，不更新 state，避免 setState 带来不必要的 re-render\n      }\n\n      stateRef.current = nextState;\n      setState(nextState);\n    };\n\n    const syncStateFromCustomEvent = (event: CustomEvent<StorageEvent>) => {\n      syncState(event.detail);\n    };\n\n    // from another document\n    useEventListener('storage', syncState, {\n      enable: listenStorageChange,\n    });\n\n    // from the same document but different hooks\n    useEventListener(SYNC_STORAGE_EVENT_NAME, syncStateFromCustomEvent, {\n      enable: listenStorageChange,\n    });\n\n    return [state, useMemoizedFn(updateState)] as const;\n  };\n\n  return useStorageState;\n};\n"
  },
  {
    "path": "packages/hooks/src/global.d.ts",
    "content": "declare module '*.jpg';\n\ninterface Window {\n  TEST_SCRIPT?: any;\n}\n"
  },
  {
    "path": "packages/hooks/src/index.spec.ts",
    "content": "import { describe, expect, test } from 'vitest';\nimport * as ahooks from '.';\n\ndescribe('ahooks', () => {\n  test('exports modules should be defined', () => {\n    Object.entries(ahooks).forEach(([key, value]) => {\n      expect(value).toBeDefined();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/index.ts",
    "content": "import { createUpdateEffect } from './createUpdateEffect';\nimport useAntdTable from './useAntdTable';\nimport useAsyncEffect from './useAsyncEffect';\nimport useBoolean from './useBoolean';\nimport useClickAway from './useClickAway';\nimport useControllableValue from './useControllableValue';\nimport useCookieState from './useCookieState';\nimport useCountDown from './useCountDown';\nimport useCounter from './useCounter';\nimport useCreation from './useCreation';\nimport useDebounce from './useDebounce';\nimport useDebounceEffect from './useDebounceEffect';\nimport useDebounceFn from './useDebounceFn';\nimport useDeepCompareEffect from './useDeepCompareEffect';\nimport useDeepCompareLayoutEffect from './useDeepCompareLayoutEffect';\nimport useDocumentVisibility from './useDocumentVisibility';\nimport useDrag from './useDrag';\nimport useDrop from './useDrop';\nimport useDynamicList from './useDynamicList';\nimport useEventEmitter from './useEventEmitter';\nimport useEventListener from './useEventListener';\nimport useEventTarget from './useEventTarget';\nimport useExternal from './useExternal';\nimport useFavicon from './useFavicon';\nimport useFocusWithin from './useFocusWithin';\nimport useFullscreen from './useFullscreen';\nimport useFusionTable from './useFusionTable';\nimport useGetState from './useGetState';\nimport useHistoryTravel from './useHistoryTravel';\nimport useHover from './useHover';\nimport useInfiniteScroll from './useInfiniteScroll';\nimport useInterval from './useInterval';\nimport useInViewport from './useInViewport';\nimport useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';\nimport useKeyPress from './useKeyPress';\nimport useLatest from './useLatest';\nimport useLocalStorageState from './useLocalStorageState';\nimport useLockFn from './useLockFn';\nimport useLongPress from './useLongPress';\nimport useMap from './useMap';\nimport useMemoizedFn from './useMemoizedFn';\nimport useMount from './useMount';\nimport useMouse from './useMouse';\nimport useNetwork from './useNetwork';\nimport usePagination from './usePagination';\nimport usePrevious from './usePrevious';\nimport useRafInterval from './useRafInterval';\nimport useRafState from './useRafState';\nimport useRafTimeout from './useRafTimeout';\nimport useReactive from './useReactive';\nimport useRequest, { clearCache } from './useRequest';\nimport useResetState from './useResetState';\nimport useResponsive, { configResponsive } from './useResponsive';\nimport useSafeState from './useSafeState';\nimport useScroll from './useScroll';\nimport useSelections from './useSelections';\nimport useSessionStorageState from './useSessionStorageState';\nimport useSet from './useSet';\nimport useSetState from './useSetState';\nimport useSize from './useSize';\nimport useTextSelection from './useTextSelection';\nimport useThrottle from './useThrottle';\nimport useThrottleEffect from './useThrottleEffect';\nimport useThrottleFn from './useThrottleFn';\nimport useTimeout from './useTimeout';\nimport useTitle from './useTitle';\nimport useToggle from './useToggle';\nimport useTrackedEffect from './useTrackedEffect';\nimport useUnmount from './useUnmount';\nimport useUnmountedRef from './useUnmountedRef';\nimport useUpdate from './useUpdate';\nimport useUpdateEffect from './useUpdateEffect';\nimport useUpdateLayoutEffect from './useUpdateLayoutEffect';\nimport useVirtualList from './useVirtualList';\nimport useWebSocket from './useWebSocket';\nimport useWhyDidYouUpdate from './useWhyDidYouUpdate';\nimport useMutationObserver from './useMutationObserver';\nimport useTheme from './useTheme';\n\nexport {\n  useRequest,\n  useControllableValue,\n  useDynamicList,\n  useVirtualList,\n  useResponsive,\n  useEventEmitter,\n  useLocalStorageState,\n  useSessionStorageState,\n  useSize,\n  configResponsive,\n  useUpdateEffect,\n  useUpdateLayoutEffect,\n  useBoolean,\n  useToggle,\n  useDocumentVisibility,\n  useSelections,\n  useThrottle,\n  useThrottleFn,\n  useThrottleEffect,\n  useDebounce,\n  useDebounceFn,\n  useDebounceEffect,\n  usePrevious,\n  useMouse,\n  useScroll,\n  useClickAway,\n  useFullscreen,\n  useInViewport,\n  useKeyPress,\n  useEventListener,\n  useHover,\n  useUnmount,\n  useSet,\n  useMemoizedFn,\n  useMap,\n  useCreation,\n  useDrag,\n  useDrop,\n  useMount,\n  useCounter,\n  useUpdate,\n  useTextSelection,\n  useEventTarget,\n  useHistoryTravel,\n  useCookieState,\n  useSetState,\n  useInterval,\n  useWhyDidYouUpdate,\n  useTitle,\n  useNetwork,\n  useTimeout,\n  useReactive,\n  useFavicon,\n  useCountDown,\n  useWebSocket,\n  useLockFn,\n  useUnmountedRef,\n  useExternal,\n  useSafeState,\n  useLatest,\n  useIsomorphicLayoutEffect,\n  useDeepCompareEffect,\n  useDeepCompareLayoutEffect,\n  useAsyncEffect,\n  useLongPress,\n  useRafState,\n  useTrackedEffect,\n  usePagination,\n  useAntdTable,\n  useFusionTable,\n  useInfiniteScroll,\n  useGetState,\n  clearCache,\n  useFocusWithin,\n  createUpdateEffect,\n  useRafInterval,\n  useRafTimeout,\n  useResetState,\n  useMutationObserver,\n  useTheme,\n};\n"
  },
  {
    "path": "packages/hooks/src/useAntdTable/__tests__/index.spec.ts",
    "content": "import type { RenderHookResult } from '@testing-library/react';\nimport { act, renderHook, waitFor } from '@testing-library/react';\nimport { Form } from 'antd';\nimport { useEffect } from 'react';\nimport { describe, expect, test } from 'vitest';\nimport { sleep } from '../../utils/testingHelpers';\nimport useAntdTable from '../index';\n\ninterface Query {\n  current: number;\n  pageSize: number;\n\n  [key: string]: any;\n}\n\ndescribe('useAntdTable', () => {\n  let queryArgs: any;\n  const asyncFn = (query: Query, formData: any = {}) => {\n    queryArgs = { ...query, ...formData };\n    return Promise.resolve({\n      total: 20,\n      list: [],\n    });\n  };\n\n  let searchType = 'simple';\n\n  const form = {\n    getInternalHooks: () => {},\n    initialValue: {\n      name: 'default name',\n    },\n    fieldsValue: {\n      name: 'default name',\n    },\n    getFieldsValue() {\n      if (searchType === 'simple') {\n        return {\n          name: this.fieldsValue.name,\n        };\n      }\n      return this.fieldsValue;\n    },\n    setFieldsValue(values: object) {\n      this.fieldsValue = {\n        ...this.fieldsValue,\n        ...values,\n      };\n    },\n    resetFields() {\n      this.fieldsValue = { ...this.initialValue };\n    },\n    validateFields(fields: any[]) {\n      const targetFields: Record<string | number, any> = {};\n      fields.forEach((field: string | number) => {\n        targetFields[field] = (this.fieldsValue as any)[field];\n      });\n      return Promise.resolve(targetFields);\n    },\n  };\n\n  const changeSearchType = (type: any) => {\n    searchType = type;\n  };\n\n  const setUp = (\n    service: Parameters<typeof useAntdTable>[0],\n    options: Parameters<typeof useAntdTable>[1],\n  ) => renderHook((o) => useAntdTable(service, o || options));\n\n  let hook: RenderHookResult<any, any>;\n\n  test('should fetch after first render', async () => {\n    queryArgs = undefined;\n    form.resetFields();\n    changeSearchType('simple');\n\n    act(() => {\n      hook = setUp(asyncFn, {});\n    });\n\n    expect(hook.result.current.tableProps.loading).toBe(false);\n    expect(hook.result.current.tableProps.pagination.current).toBe(1);\n    expect(hook.result.current.tableProps.pagination.pageSize).toBe(10);\n    await waitFor(() => expect(hook.result.current.tableProps.pagination.total).toBe(20));\n  });\n\n  test('should defaultParams work', async () => {\n    queryArgs = undefined;\n    form.resetFields();\n    changeSearchType('advance');\n    act(() => {\n      hook = setUp(asyncFn, {\n        form,\n        defaultParams: [\n          {\n            current: 2,\n            pageSize: 10,\n          },\n          { name: 'hello', phone: '123' },\n        ],\n        defaultType: 'advance',\n      });\n    });\n    const { search } = hook.result.current;\n    expect(hook.result.current.tableProps.loading).toBe(false);\n    await waitFor(() => expect(queryArgs.current).toBe(2));\n    expect(queryArgs.pageSize).toBe(10);\n    expect(queryArgs.name).toBe('hello');\n    expect(queryArgs.phone).toBe('123');\n    expect(search.type).toBe('advance');\n  });\n\n  test('should stop the query when validate fields failed', async () => {\n    queryArgs = undefined;\n    form.resetFields();\n    changeSearchType('advance');\n    act(() => {\n      hook = setUp(asyncFn, {\n        form: { ...form, validateFields: () => Promise.reject() },\n        defaultParams: [\n          {\n            current: 2,\n            pageSize: 10,\n          },\n          { name: 'hello', phone: '123' },\n        ],\n        defaultType: 'advance',\n      });\n    });\n\n    await sleep(1);\n    expect(queryArgs).toBeUndefined();\n  });\n\n  test('should ready work', async () => {\n    queryArgs = undefined;\n    form.resetFields();\n    changeSearchType('advance');\n\n    act(() => {\n      hook = setUp(asyncFn, {\n        ready: false,\n        form,\n        defaultParams: [\n          {\n            current: 2,\n            pageSize: 10,\n          },\n          { name: 'hello', phone: '123' },\n        ],\n        defaultType: 'advance',\n      });\n    });\n    await sleep(1);\n    expect(queryArgs).toBeUndefined();\n\n    hook.rerender({\n      ready: true,\n      form,\n      defaultParams: [\n        {\n          current: 2,\n          pageSize: 10,\n        },\n        { name: 'hello', phone: '456' },\n      ],\n      defaultType: 'advance',\n    });\n\n    const { search } = hook.result.current;\n    expect(hook.result.current.tableProps.loading).toBe(false);\n    await waitFor(() => expect(queryArgs.current).toBe(2));\n    expect(queryArgs.pageSize).toBe(10);\n    expect(queryArgs.name).toBe('hello');\n    expect(queryArgs.phone).toBe('456');\n    expect(search.type).toBe('advance');\n  });\n\n  test('should antd v3 work', async () => {\n    queryArgs = undefined;\n    form.resetFields();\n    changeSearchType('simple');\n\n    const v3Form = {\n      ...form,\n      getInternalHooks: undefined,\n      validateFields: function (fields: any[], callback: (arg0: undefined, arg1: {}) => void) {\n        const targetFields: Record<string | number, any> = {};\n        fields.forEach((field: string | number) => {\n          targetFields[field] = (this.fieldsValue as any)[field];\n        });\n        callback(undefined, targetFields);\n      },\n      getFieldInstance(key: string) {\n        // 根据不同的 type 返回不同的 fieldsValues\n        if (searchType === 'simple') {\n          return ['name'].includes(key) as any;\n        }\n        return ['name', 'email', 'phone'].includes(key) as any;\n      },\n    };\n\n    act(() => {\n      hook = setUp(asyncFn, { form: v3Form });\n    });\n    const { search } = hook.result.current;\n    expect(hook.result.current.tableProps.loading).toBe(false);\n    await waitFor(() => expect(queryArgs.current).toBe(1));\n    expect(queryArgs.pageSize).toBe(10);\n    expect(queryArgs.name).toBe('default name');\n    expect(search.type).toBe('simple');\n\n    // /* 切换 分页 */\n    act(() => {\n      hook.result.current.tableProps.onChange({\n        current: 2,\n        pageSize: 5,\n      });\n    });\n    await waitFor(() => expect(queryArgs.current).toBe(2));\n    expect(queryArgs.pageSize).toBe(5);\n    expect(queryArgs.name).toBe('default name');\n\n    /* 改变 name，提交表单 */\n    v3Form.fieldsValue.name = 'change name';\n    act(() => {\n      search.submit();\n    });\n    await waitFor(() => expect(queryArgs.current).toBe(1));\n    expect(queryArgs.current).toBe(1);\n    // expect(queryArgs.pageSize).toBe(5);\n    expect(queryArgs.name).toBe('change name');\n  });\n\n  test('should reset pageSize in defaultParams', async () => {\n    queryArgs = undefined;\n    form.resetFields();\n    act(() => {\n      hook = setUp(asyncFn, {\n        form,\n        defaultParams: [\n          {\n            current: 1,\n            pageSize: 10,\n          },\n        ],\n      });\n    });\n\n    const { search, tableProps } = hook.result.current;\n    expect(tableProps.loading).toBe(false);\n    await waitFor(() => expect(queryArgs.current).toBe(1));\n    expect(queryArgs.pageSize).toBe(10);\n\n    // change params\n    act(() => {\n      tableProps.onChange({\n        current: 2,\n        pageSize: 5,\n      });\n    });\n\n    await waitFor(() => {\n      expect(queryArgs.current).toBe(2);\n      expect(queryArgs.pageSize).toBe(5);\n    });\n\n    // reset params\n    act(() => {\n      search.reset();\n    });\n\n    await waitFor(() => {\n      expect(queryArgs.current).toBe(1);\n      expect(queryArgs.pageSize).toBe(10);\n    });\n  });\n\n  test('should reset pageSize in defaultPageSize', async () => {\n    queryArgs = undefined;\n    form.resetFields();\n    act(() => {\n      hook = setUp(asyncFn, {\n        form,\n        defaultParams: {\n          current: 1,\n          pageSize: 10,\n        } as any,\n        defaultPageSize: 20,\n      });\n    });\n\n    const { search, tableProps } = hook.result.current;\n    expect(tableProps.loading).toBe(false);\n    await waitFor(() => expect(queryArgs.current).toBe(1));\n    expect(queryArgs.pageSize).toBe(20);\n\n    // change params\n    act(() => {\n      tableProps.onChange({\n        current: 2,\n        pageSize: 5,\n      });\n    });\n\n    await waitFor(() => {\n      expect(queryArgs.current).toBe(2);\n      expect(queryArgs.pageSize).toBe(5);\n    });\n\n    // reset params\n    act(() => {\n      search.reset();\n    });\n\n    await waitFor(() => {\n      expect(queryArgs.current).toBe(1);\n      expect(queryArgs.pageSize).toBe(20);\n    });\n  });\n\n  test('search submit use default params', async () => {\n    queryArgs = undefined;\n    form.resetFields();\n    act(() => {\n      hook = setUp(asyncFn, {\n        form,\n        defaultParams: [\n          {\n            current: 2,\n            pageSize: 100,\n          },\n        ],\n      });\n    });\n\n    const { search } = hook.result.current;\n\n    act(() => {\n      search.submit();\n    });\n\n    await waitFor(() => {\n      expect(queryArgs.current).toBe(2);\n      expect(queryArgs.pageSize).toBe(100);\n    });\n  });\n\n  test('should defaultParams work with manual is  true', async () => {\n    queryArgs = undefined;\n    form.resetFields();\n    changeSearchType('advance');\n\n    act(() => {\n      renderHook((o) => {\n        const [myForm] = Form.useForm();\n\n        useAntdTable(\n          asyncFn,\n          o || {\n            form: myForm,\n            defaultParams: [\n              {\n                current: 2,\n                pageSize: 10,\n              },\n              { name: 'hello', phone: '123' },\n            ],\n            defaultType: 'advance',\n          },\n        );\n\n        useEffect(() => {\n          // defaultParams works\n          expect(myForm.getFieldValue('name')).toBe('hello');\n          expect(queryArgs).toBe(undefined);\n        }, []);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useAntdTable/demo/cache.tsx",
    "content": "import { useState } from 'react';\nimport { Button, Col, Form, Input, Row, Table, Select } from 'antd';\nimport { useAntdTable, clearCache } from 'ahooks';\nimport ReactJson from 'react-json-view';\n\nconst { Option } = Select;\n\ninterface Item {\n  name: {\n    last: string;\n  };\n  email: string;\n  phone: string;\n  gender: 'male' | 'female';\n}\n\ninterface Result {\n  total: number;\n  list: Item[];\n}\n\nconst getTableData = (\n  {\n    current,\n    pageSize,\n    sorter,\n    filters,\n    extra,\n  }: {\n    current: number;\n    pageSize: number;\n    sorter: any;\n    filters: any;\n    extra: any;\n  },\n  formData: Record<string, any>,\n): Promise<Result> => {\n  console.log(sorter, filters, extra);\n  let query = `page=${current}&size=${pageSize}`;\n  Object.entries(formData).forEach(([key, value]) => {\n    if (value) {\n      query += `&${key}=${value}`;\n    }\n  });\n\n  return fetch(`https://randomuser.me/api?results=55&${query}`)\n    .then((res) => res.json())\n    .then((res) => ({\n      total: res.info.results,\n      list: res.results,\n    }));\n};\n\nconst UserList = () => {\n  const [form] = Form.useForm();\n\n  const { tableProps, search, params } = useAntdTable(getTableData, {\n    defaultPageSize: 5,\n    form,\n    cacheKey: 'useAntdTableCache',\n  });\n\n  const { sorter = {}, filters = {} } = params[0] || ({} as any);\n\n  const { type, changeType, submit, reset } = search;\n\n  const columns = [\n    {\n      title: 'name',\n      dataIndex: ['name', 'last'],\n    },\n    {\n      title: 'email',\n      dataIndex: 'email',\n    },\n    {\n      title: 'phone',\n      dataIndex: 'phone',\n      sorter: true,\n      sortOrder: sorter.field === 'phone' && sorter.order,\n    },\n    {\n      title: 'gender',\n      dataIndex: 'gender',\n      filters: [\n        { text: 'male', value: 'male' },\n        { text: 'female', value: 'female' },\n      ],\n      filteredValue: filters.gender,\n    },\n  ];\n\n  const advanceSearchForm = (\n    <div>\n      <Form form={form}>\n        <Row gutter={24}>\n          <Col span={8}>\n            <Form.Item label=\"name\" name=\"name\">\n              <Input placeholder=\"name\" />\n            </Form.Item>\n          </Col>\n          <Col span={8}>\n            <Form.Item label=\"email\" name=\"email\">\n              <Input placeholder=\"email\" />\n            </Form.Item>\n          </Col>\n          <Col span={8}>\n            <Form.Item label=\"phone\" name=\"phone\">\n              <Input placeholder=\"phone\" />\n            </Form.Item>\n          </Col>\n        </Row>\n        <Row gutter={24} justify=\"end\" style={{ marginBottom: 24 }}>\n          <Button type=\"primary\" onClick={submit}>\n            Search\n          </Button>\n          <Button onClick={reset} style={{ marginLeft: 16 }}>\n            Reset\n          </Button>\n          <Button type=\"link\" onClick={changeType}>\n            Simple Search\n          </Button>\n        </Row>\n      </Form>\n    </div>\n  );\n\n  const searchForm = (\n    <div style={{ marginBottom: 16 }}>\n      <Form form={form} style={{ display: 'flex', justifyContent: 'flex-end' }}>\n        <Form.Item name=\"gender\" initialValue=\"male\">\n          <Select style={{ width: 120, marginRight: 16 }} onChange={submit}>\n            <Option value=\"\">all</Option>\n            <Option value=\"male\">male</Option>\n            <Option value=\"female\">female</Option>\n          </Select>\n        </Form.Item>\n        <Form.Item name=\"name\">\n          <Input.Search placeholder=\"enter name\" style={{ width: 240 }} onSearch={submit} />\n        </Form.Item>\n        <Button type=\"link\" onClick={changeType}>\n          Advanced Search\n        </Button>\n      </Form>\n    </div>\n  );\n\n  return (\n    <div>\n      {type === 'simple' ? searchForm : advanceSearchForm}\n      <Table columns={columns} rowKey=\"email\" style={{ overflow: 'auto' }} {...tableProps} />\n\n      <div style={{ background: '#f5f5f5', padding: 8 }}>\n        <p>Current Table:</p>\n        <ReactJson src={params[0]!} collapsed={2} />\n        <p>Current Form:</p>\n        <ReactJson src={params[0]!} collapsed={2} />\n      </div>\n    </div>\n  );\n};\n\nconst Demo = () => {\n  const [show, setShow] = useState(true);\n\n  return (\n    <div>\n      <Button\n        danger\n        onClick={() => {\n          setShow(!show);\n        }}\n        style={{ marginBottom: 16 }}\n      >\n        {show ? 'Click to destroy' : 'Click recovery'}\n      </Button>\n      <Button\n        danger\n        onClick={() => {\n          clearCache('useAntdTableCache');\n        }}\n        style={{ marginBottom: 16, marginLeft: 8 }}\n      >\n        Click to clearCache\n      </Button>\n      {show && <UserList />}\n    </div>\n  );\n};\n\nexport default Demo;\n"
  },
  {
    "path": "packages/hooks/src/useAntdTable/demo/form.tsx",
    "content": "import { Button, Col, Form, Input, Row, Table, Select } from 'antd';\nimport { useAntdTable } from 'ahooks';\nimport ReactJson from 'react-json-view';\n\nconst { Option } = Select;\n\ninterface Item {\n  name: {\n    last: string;\n  };\n  email: string;\n  phone: string;\n  gender: 'male' | 'female';\n}\n\ninterface Result {\n  total: number;\n  list: Item[];\n}\n\nconst getTableData = (\n  {\n    current,\n    pageSize,\n  }: {\n    current: number;\n    pageSize: number;\n  },\n  formData: Object,\n): Promise<Result> => {\n  let query = `page=${current}&size=${pageSize}`;\n  Object.entries(formData).forEach(([key, value]) => {\n    if (value) {\n      query += `&${key}=${value}`;\n    }\n  });\n\n  return fetch(`https://randomuser.me/api?results=55&${query}`)\n    .then((res) => res.json())\n    .then((res) => ({\n      total: res.info.results,\n      list: res.results,\n    }));\n};\n\nexport default () => {\n  const [form] = Form.useForm();\n\n  const { tableProps, search, params } = useAntdTable(getTableData, {\n    defaultPageSize: 5,\n    form,\n  });\n\n  const { type, changeType, submit, reset } = search;\n\n  const columns = [\n    {\n      title: 'name',\n      dataIndex: ['name', 'last'],\n    },\n    {\n      title: 'email',\n      dataIndex: 'email',\n    },\n    {\n      title: 'phone',\n      dataIndex: 'phone',\n    },\n    {\n      title: 'gender',\n      dataIndex: 'gender',\n    },\n  ];\n\n  const advanceSearchForm = (\n    <div>\n      <Form form={form}>\n        <Row gutter={24}>\n          <Col span={8}>\n            <Form.Item label=\"name\" name=\"name\">\n              <Input placeholder=\"name\" />\n            </Form.Item>\n          </Col>\n          <Col span={8}>\n            <Form.Item label=\"email\" name=\"email\">\n              <Input placeholder=\"email\" />\n            </Form.Item>\n          </Col>\n          <Col span={8}>\n            <Form.Item label=\"phone\" name=\"phone\">\n              <Input placeholder=\"phone\" />\n            </Form.Item>\n          </Col>\n        </Row>\n        <Row gutter={24} justify=\"end\" style={{ marginBottom: 24 }}>\n          <Button type=\"primary\" onClick={submit}>\n            Search\n          </Button>\n          <Button onClick={reset} style={{ marginLeft: 16 }}>\n            Reset\n          </Button>\n          <Button type=\"link\" onClick={changeType}>\n            Simple Search\n          </Button>\n        </Row>\n      </Form>\n    </div>\n  );\n\n  const searchForm = (\n    <div style={{ marginBottom: 16 }}>\n      <Form form={form} style={{ display: 'flex', justifyContent: 'flex-end' }}>\n        <Form.Item name=\"gender\" initialValue=\"male\">\n          <Select style={{ width: 120, marginRight: 16 }} onChange={submit}>\n            <Option value=\"\">all</Option>\n            <Option value=\"male\">male</Option>\n            <Option value=\"female\">female</Option>\n          </Select>\n        </Form.Item>\n        <Form.Item name=\"name\">\n          <Input.Search placeholder=\"enter name\" style={{ width: 240 }} onSearch={submit} />\n        </Form.Item>\n        <Button type=\"link\" onClick={changeType}>\n          Advanced Search\n        </Button>\n      </Form>\n    </div>\n  );\n\n  return (\n    <div>\n      {type === 'simple' ? searchForm : advanceSearchForm}\n      <Table columns={columns} rowKey=\"email\" style={{ overflow: 'auto' }} {...tableProps} />\n\n      <div style={{ background: '#f5f5f5', padding: 8 }}>\n        <p>Current Table:</p>\n        <ReactJson src={params[0]!} collapsed={2} />\n        <p>Current Form:</p>\n        <ReactJson src={params[1]!} collapsed={2} />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useAntdTable/demo/init.tsx",
    "content": "import { Button, Col, Form, Input, Row, Table, Select } from 'antd';\nimport { useAntdTable } from 'ahooks';\nimport ReactJson from 'react-json-view';\n\nconst { Option } = Select;\n\ninterface Item {\n  name: {\n    last: string;\n  };\n  email: string;\n  phone: string;\n  gender: 'male' | 'female';\n}\n\ninterface Result {\n  total: number;\n  list: Item[];\n}\n\nconst getTableData = (\n  {\n    current,\n    pageSize,\n  }: {\n    current: number;\n    pageSize: number;\n  },\n  formData: Object,\n): Promise<Result> => {\n  let query = `page=${current}&size=${pageSize}`;\n  Object.entries(formData).forEach(([key, value]) => {\n    if (value) {\n      query += `&${key}=${value}`;\n    }\n  });\n\n  return fetch(`https://randomuser.me/api?results=55&${query}`)\n    .then((res) => res.json())\n    .then((res) => ({\n      total: res.info.results,\n      list: res.results,\n    }));\n};\n\nexport default () => {\n  const [form] = Form.useForm();\n\n  const { tableProps, search, params } = useAntdTable(getTableData, {\n    form,\n    defaultParams: [\n      { current: 2, pageSize: 5 },\n      { name: 'hello', email: 'abc@gmail.com', gender: 'female' },\n    ],\n    defaultType: 'advance',\n  });\n\n  const { type, changeType, submit, reset } = search;\n\n  const columns = [\n    {\n      title: 'name',\n      dataIndex: ['name', 'last'],\n    },\n    {\n      title: 'email',\n      dataIndex: 'email',\n    },\n    {\n      title: 'phone',\n      dataIndex: 'phone',\n    },\n    {\n      title: 'gender',\n      dataIndex: 'gender',\n    },\n  ];\n\n  const advanceSearchForm = (\n    <div>\n      <Form form={form}>\n        <Row gutter={24}>\n          <Col span={8}>\n            <Form.Item label=\"name\" name=\"name\">\n              <Input placeholder=\"name\" />\n            </Form.Item>\n          </Col>\n          <Col span={8}>\n            <Form.Item label=\"email\" name=\"email\">\n              <Input placeholder=\"email\" />\n            </Form.Item>\n          </Col>\n          <Col span={8}>\n            <Form.Item label=\"phone\" name=\"phone\">\n              <Input placeholder=\"phone\" />\n            </Form.Item>\n          </Col>\n        </Row>\n        <Row gutter={24} justify=\"end\" style={{ marginBottom: 24 }}>\n          <Button type=\"primary\" onClick={submit}>\n            Search\n          </Button>\n          <Button onClick={reset} style={{ marginLeft: 16 }}>\n            Reset\n          </Button>\n          <Button type=\"link\" onClick={changeType}>\n            Simple Search\n          </Button>\n        </Row>\n      </Form>\n    </div>\n  );\n\n  const searchForm = (\n    <div style={{ marginBottom: 16 }}>\n      <Form form={form} style={{ display: 'flex', justifyContent: 'flex-end' }}>\n        <Form.Item name=\"gender\" initialValue=\"male\">\n          <Select style={{ width: 120, marginRight: 16 }} onChange={submit}>\n            <Option value=\"\">all</Option>\n            <Option value=\"male\">male</Option>\n            <Option value=\"female\">female</Option>\n          </Select>\n        </Form.Item>\n        <Form.Item name=\"name\">\n          <Input.Search placeholder=\"enter name\" style={{ width: 240 }} onSearch={submit} />\n        </Form.Item>\n        <Button type=\"link\" onClick={changeType}>\n          Advanced Search\n        </Button>\n      </Form>\n    </div>\n  );\n\n  return (\n    <div>\n      {type === 'simple' ? searchForm : advanceSearchForm}\n      <Table columns={columns} rowKey=\"email\" style={{ overflow: 'auto' }} {...tableProps} />\n\n      <div style={{ background: '#f5f5f5', padding: 8 }}>\n        <p>Current Table:</p>\n        <ReactJson src={params[0]!} collapsed={2} />\n        <p>Current Form:</p>\n        <ReactJson src={params[1]!} collapsed={2} />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useAntdTable/demo/ready.tsx",
    "content": "import { useState } from 'react';\nimport { Button, Col, Form, Input, Row, Table, Select } from 'antd';\nimport { useAntdTable } from 'ahooks';\nimport ReactJson from 'react-json-view';\n\nconst { Option } = Select;\n\ninterface Item {\n  name: {\n    last: string;\n  };\n  email: string;\n  phone: string;\n  gender: 'male' | 'female';\n}\n\ninterface Result {\n  total: number;\n  list: Item[];\n}\n\nconst getTableData = (\n  {\n    current,\n    pageSize,\n  }: {\n    current: number;\n    pageSize: number;\n  },\n  formData: Object,\n): Promise<Result> => {\n  let query = `page=${current}&size=${pageSize}`;\n  Object.entries(formData).forEach(([key, value]) => {\n    if (value) {\n      query += `&${key}=${value}`;\n    }\n  });\n\n  return fetch(`https://randomuser.me/api?results=55&${query}`)\n    .then((res) => res.json())\n    .then((res) => ({\n      total: res.info.results,\n      list: res.results,\n    }));\n};\n\nexport default () => {\n  const [form] = Form.useForm();\n\n  const [ready, setReady] = useState(false);\n\n  const { tableProps, search, params } = useAntdTable(getTableData, {\n    form,\n    ready,\n    cacheKey: 'demo-ready',\n    defaultParams: [\n      { current: ready ? 2 : 1, pageSize: 5 },\n      { name: ready ? 'hello' : '', email: 'abc@gmail.com', gender: 'female' },\n    ],\n    defaultType: 'advance',\n  });\n\n  const { type, changeType, submit, reset } = search;\n\n  const columns = [\n    {\n      title: 'name',\n      dataIndex: ['name', 'last'],\n    },\n    {\n      title: 'email',\n      dataIndex: 'email',\n    },\n    {\n      title: 'phone',\n      dataIndex: 'phone',\n    },\n    {\n      title: 'gender',\n      dataIndex: 'gender',\n    },\n  ];\n\n  const advanceSearchForm = (\n    <div>\n      <Form form={form}>\n        <Row gutter={24}>\n          <Col span={8}>\n            <Form.Item label=\"name\" name=\"name\">\n              <Input placeholder=\"name\" />\n            </Form.Item>\n          </Col>\n          <Col span={8}>\n            <Form.Item label=\"email\" name=\"email\">\n              <Input placeholder=\"email\" />\n            </Form.Item>\n          </Col>\n          <Col span={8}>\n            <Form.Item label=\"phone\" name=\"phone\">\n              <Input placeholder=\"phone\" />\n            </Form.Item>\n          </Col>\n        </Row>\n        <Row gutter={24} justify=\"end\" style={{ marginBottom: 24 }}>\n          <Button type=\"primary\" onClick={submit}>\n            Search\n          </Button>\n          <Button onClick={reset} style={{ marginLeft: 16 }}>\n            Reset\n          </Button>\n          <Button type=\"link\" onClick={changeType}>\n            Simple Search\n          </Button>\n        </Row>\n      </Form>\n    </div>\n  );\n\n  const searchForm = (\n    <div style={{ marginBottom: 16 }}>\n      <Form form={form} style={{ display: 'flex', justifyContent: 'flex-end' }}>\n        <Form.Item name=\"gender\" initialValue=\"male\">\n          <Select style={{ width: 120, marginRight: 16 }} onChange={submit}>\n            <Option value=\"\">all</Option>\n            <Option value=\"male\">male</Option>\n            <Option value=\"female\">female</Option>\n          </Select>\n        </Form.Item>\n        <Form.Item name=\"name\">\n          <Input.Search placeholder=\"enter name\" style={{ width: 240 }} onSearch={submit} />\n        </Form.Item>\n        <Button type=\"link\" onClick={changeType}>\n          Advanced Search\n        </Button>\n      </Form>\n    </div>\n  );\n\n  return (\n    <div>\n      <Button onClick={() => setReady((r) => !r)}>toggle ready</Button>\n      {type === 'simple' ? searchForm : advanceSearchForm}\n      <Table columns={columns} rowKey=\"email\" style={{ overflow: 'auto' }} {...tableProps} />\n\n      <div style={{ background: '#f5f5f5', padding: 8 }}>\n        <p>Current Table:</p>\n        <ReactJson src={params[0]!} collapsed={2} />\n        <p>Current Form:</p>\n        <ReactJson src={params[1]!} collapsed={2} />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useAntdTable/demo/table.tsx",
    "content": "import { Table } from 'antd';\nimport { useAntdTable } from 'ahooks';\n\ninterface Item {\n  name: {\n    last: string;\n  };\n  email: string;\n  phone: string;\n  gender: 'male' | 'female';\n}\n\ninterface Result {\n  total: number;\n  list: Item[];\n}\n\nconst getTableData = ({\n  current,\n  pageSize,\n}: {\n  current: number;\n  pageSize: number;\n}): Promise<Result> => {\n  const query = `page=${current}&size=${pageSize}`;\n\n  return fetch(`https://randomuser.me/api?results=55&${query}`)\n    .then((res) => res.json())\n    .then((res) => ({\n      total: res.info.results,\n      list: res.results,\n    }));\n};\n\nexport default () => {\n  const { tableProps } = useAntdTable(getTableData);\n\n  const columns = [\n    {\n      title: 'name',\n      dataIndex: ['name', 'last'],\n    },\n    {\n      title: 'email',\n      dataIndex: 'email',\n    },\n    {\n      title: 'phone',\n      dataIndex: 'phone',\n    },\n    {\n      title: 'gender',\n      dataIndex: 'gender',\n    },\n  ];\n\n  return <Table columns={columns} rowKey=\"email\" style={{ overflow: 'auto' }} {...tableProps} />;\n};\n"
  },
  {
    "path": "packages/hooks/src/useAntdTable/demo/validate.tsx",
    "content": "import { Form, Input, Select, Table } from 'antd';\nimport { useAntdTable } from 'ahooks';\nimport ReactJson from 'react-json-view';\n\nconst { Option } = Select;\n\ninterface Item {\n  name: {\n    last: string;\n  };\n  email: string;\n  phone: string;\n  gender: 'male' | 'female';\n}\n\ninterface Result {\n  total: number;\n  list: Item[];\n}\n\nconst getTableData = (\n  {\n    current,\n    pageSize,\n  }: {\n    current: number;\n    pageSize: number;\n  },\n  formData: Object,\n): Promise<Result> => {\n  let query = `page=${current}&size=${pageSize}`;\n  Object.entries(formData).forEach(([key, value]) => {\n    if (value) {\n      query += `&${key}=${value}`;\n    }\n  });\n\n  return fetch(`https://randomuser.me/api?results=55&${query}`)\n    .then((res) => res.json())\n    .then((res) => ({\n      total: res.info.results,\n      list: res.results,\n    }));\n};\n\nexport default () => {\n  const [form] = Form.useForm();\n\n  const { tableProps, search, params } = useAntdTable(getTableData, {\n    defaultPageSize: 5,\n    form,\n  });\n\n  const { submit } = search;\n\n  const columns = [\n    {\n      title: 'name',\n      dataIndex: ['name', 'last'],\n    },\n    {\n      title: 'email',\n      dataIndex: 'email',\n    },\n    {\n      title: 'phone',\n      dataIndex: 'phone',\n    },\n    {\n      title: 'gender',\n      dataIndex: 'gender',\n    },\n  ];\n\n  const searchForm = (\n    <div style={{ marginBottom: 16 }}>\n      <Form form={form} style={{ display: 'flex', justifyContent: 'flex-end' }}>\n        <Form.Item name=\"gender\" initialValue=\"male\">\n          <Select style={{ width: 120, marginRight: 16 }} onChange={submit}>\n            <Option value=\"\">all</Option>\n            <Option value=\"male\">male</Option>\n            <Option value=\"female\">female</Option>\n          </Select>\n        </Form.Item>\n        <Form.Item\n          name=\"name\"\n          initialValue=\"jack\"\n          rules={[{ required: true, message: 'name is required' }]}\n        >\n          <Input.Search placeholder=\"enter name\" style={{ width: 240 }} onSearch={submit} />\n        </Form.Item>\n      </Form>\n    </div>\n  );\n\n  return (\n    <div>\n      {searchForm}\n      <Table columns={columns} rowKey=\"email\" style={{ overflow: 'auto' }} {...tableProps} />\n\n      <div style={{ background: '#f5f5f5', padding: 8 }}>\n        <p>Current Table:</p>\n        <ReactJson src={params[0]!} collapsed={2} />\n        <p>Current Form:</p>\n        <ReactJson src={params[1]!} collapsed={2} />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useAntdTable/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useAntdTable\n\n`useAntdTable` is implemented based on `useRequest` and encapsulates the commonly used [Ant Design Form](https://ant.design/components/form/) and [Ant Design Table](https://ant.design/components/table/) data binding logic, and supports both antd v3 and v4.\n\nBefore using it, you need to understand a few points that are different from `useRequest`:\n\n1. `service` receives two parameters, the first parameter is the paging data `{ current, pageSize, sorter, filters, extra }`, and the second parameter is the form data.\n2. The data structure returned by `service` must be `{ total: number, list: Item[] }`.\n3. Additional `tableProps` and `search` fields will be returned to manage tables and forms.\n4. When `refreshDeps` changes, it will reset `current` to the first page and re-initiate the request.\n\n## Examples\n\nThe following demos are for antd v4. For v3, please refer to: https://ahooks-v2.js.org/hooks/table/use-antd-table\n\n### Table management\n\n`useAntdTable` will automatically manage the pagination data of `Table`, you only need to pass the returned `tableProps` to the `Table` component.\n\n```tsx | pure\n<Table columns={columns} rowKey=\"email\" {...tableProps} />\n```\n\n<br />\n\n<code src=\"./demo/table.tsx\" />\n\n### Form and Table data binding\n\nWhen `useAntdTable` receives the `form` instance, it will return a search object to handle form related events.\n\n- `search.type` supports switching between `simple` and `advance`\n- `search.changeType`, switch form type\n- `search.submit` submit form\n- `search.reset` reset the current form\n\nIn the following example, you can try out the data binding between form and table.\n\n<code src=\"./demo/form.tsx\" />\n\n### Default Params\n\n`useAntdTable` sets the initial value through `defaultParams`, `defaultParams` is an array, the first item is paging related parameters, and the second item is form related data. If there is a second value, we will initialize the form for you!\n\nIt should be noted that the initial form data can be filled with all the form data of `simple` and `advance`, and we will help you select the form data of the currently activated type.\n\nThe following example sets paging data and form data during initialization.\n\n<code src=\"./demo/init.tsx\" />\n\n### Form Validation\n\nBefore the form is submitted, we will call `form.validateFields` to validate the form data. If the verification fails, the request will not be initiated.\n\n<code src=\"./demo/validate.tsx\" />\n\n### Data Caching\n\nBy setting `cacheKey`, we can apply the data caching for the `Form` and `Table`.\n\n<code src=\"./demo/cache.tsx\" />\n\n## API\n\nAll parameters and returned results of `useRequest` are applicable to `useAntdTable`, so we won't repeat them here.\n\n```typescript\n\ntype Data = { total: number; list: any[] };\ntype Params = [{ current: number; pageSize: number, filters?: any, sorter?: any, extra?: any }, { [key: string]: any }];\n\nconst {\n  ...,\n  tableProps: {\n    dataSource: TData['list'];\n    loading: boolean;\n    onChange: (\n      pagination: any,\n      filters?: any,\n      sorter?: any,\n      extra?: any,\n    ) => void;\n    pagination: {\n      current: number;\n      pageSize: number;\n      total: number;\n    };\n  };\n  search: {\n    type: 'simple' | 'advance';\n    changeType: () => void;\n    submit: () => void;\n    reset: () => void;\n  };\n} = useAntdTable<TData extends Data, TParams extends Params>(\n  service: (...args: TParams) => Promise<TData>,\n  {\n    ...,\n    form?: any;\n    defaultType?: 'simple' | 'advance';\n    defaultParams?: TParams,\n    defaultPageSize?: number;\n    refreshDeps?: any[];\n  }\n);\n```\n\n### Result\n\n| Property          | Description                                | Type                  |\n| ----------------- | ------------------------------------------ | --------------------- |\n| tableProps        | The data required by the `Table` component | -                     |\n| search.type       | Current form type                          | `simple` \\| `advance` |\n| search.changeType | Switch form type                           | `() => void`          |\n| search.submit     | Submit form                                | `() => void`          |\n| search.reset      | Reset the current form                     | `() => void`          |\n\n### Params\n\n| Property        | Description                                                                                | Type                     | Default  |\n| --------------- | ------------------------------------------------------------------------------------------ | ------------------------ | -------- |\n| form            | `Form` instance                                                                            | -                        | -        |\n| defaultType     | Default form type                                                                          | `simple` \\| `advance`    | `simple` |\n| defaultParams   | Default parameters, the first item is paging data, the second item is form data            | `[pagination, formData]` | -        |\n| defaultPageSize | Default page size                                                                          | `number`                 | `10`     |\n| refreshDeps     | Changes in `refreshDeps` will reset current to the first page and re-initiate the request. | `React.DependencyList`   | `[]`     |\n"
  },
  {
    "path": "packages/hooks/src/useAntdTable/index.tsx",
    "content": "import { useEffect, useRef, useState } from 'react';\nimport useMemoizedFn from '../useMemoizedFn';\nimport usePagination from '../usePagination';\nimport useUpdateEffect from '../useUpdateEffect';\n\nimport type {\n  Antd4ValidateFields,\n  AntdTableOptions,\n  Data,\n  Params,\n  Service,\n  AntdTableResult,\n} from './types';\n\nconst useAntdTable = <TData extends Data, TParams extends Params>(\n  service: Service<TData, TParams>,\n  options: AntdTableOptions<TData, TParams> = {},\n) => {\n  const {\n    form,\n    defaultType = 'simple',\n    defaultParams,\n    manual = false,\n    refreshDeps = [],\n    ready = true,\n    ...rest\n  } = options;\n\n  const result = usePagination<TData, TParams>(service, {\n    ready,\n    manual: true,\n    ...rest,\n    onSuccess(...args) {\n      // eslint-disable-next-line @typescript-eslint/no-use-before-define\n      runSuccessRef.current = true;\n      rest.onSuccess?.(...args);\n    },\n  });\n\n  const { params = [], run } = result;\n\n  const cacheFormTableData = params[2] || ({} as any);\n\n  const [type, setType] = useState(cacheFormTableData?.type || defaultType);\n\n  const allFormDataRef = useRef<Record<string, any>>({});\n  const defaultDataSourceRef = useRef([]);\n  const runSuccessRef = useRef(false);\n\n  const isAntdV4 = !!form?.getInternalHooks;\n\n  // get current active field values\n  const getActiveFieldValues = () => {\n    if (!form) {\n      return {};\n    }\n\n    // antd 4\n    if (isAntdV4) {\n      return form.getFieldsValue(null, () => true);\n    }\n\n    // antd 3\n    const allFieldsValue = form.getFieldsValue();\n    const activeFieldsValue: Record<string | number, any> = {};\n    Object.keys(allFieldsValue).forEach((key: string) => {\n      if (form.getFieldInstance ? form.getFieldInstance(key) : true) {\n        activeFieldsValue[key] = allFieldsValue[key];\n      }\n    });\n\n    return activeFieldsValue;\n  };\n\n  const validateFields = (): Promise<Record<string, any>> => {\n    if (!form) {\n      return Promise.resolve({});\n    }\n    const activeFieldsValue = getActiveFieldValues();\n    const fields = Object.keys(activeFieldsValue);\n\n    // antd 4\n    if (isAntdV4) {\n      return (form.validateFields as Antd4ValidateFields)(fields);\n    }\n    // antd 3\n    return new Promise((resolve, reject) => {\n      form.validateFields(fields, (errors, values) => {\n        if (errors) {\n          reject(errors);\n        } else {\n          resolve(values);\n        }\n      });\n    });\n  };\n\n  const restoreForm = () => {\n    if (!form) {\n      return;\n    }\n\n    // antd v4\n    if (isAntdV4) {\n      return form.setFieldsValue(allFormDataRef.current);\n    }\n\n    // antd v3\n    const activeFieldsValue: Record<string | number, any> = {};\n    Object.keys(allFormDataRef.current).forEach((key) => {\n      if (form.getFieldInstance ? form.getFieldInstance(key) : true) {\n        activeFieldsValue[key] = allFormDataRef.current[key];\n      }\n    });\n    form.setFieldsValue(activeFieldsValue);\n  };\n\n  const changeType = () => {\n    const activeFieldsValue = getActiveFieldValues();\n    allFormDataRef.current = {\n      ...allFormDataRef.current,\n      ...activeFieldsValue,\n    };\n    setType((t: string) => (t === 'simple' ? 'advance' : 'simple'));\n  };\n\n  const _submit = (initPagination?: TParams[0]) => {\n    if (!ready) {\n      return;\n    }\n    setTimeout(() => {\n      validateFields()\n        .then((values = {}) => {\n          const pagination = initPagination || {\n            pageSize: options.defaultPageSize || 10,\n            ...(params?.[0] || {}),\n            current: 1,\n          };\n          if (!form) {\n            // @ts-ignore\n            run(pagination);\n            return;\n          }\n\n          // record all form data\n          allFormDataRef.current = {\n            ...allFormDataRef.current,\n            ...values,\n          };\n\n          // @ts-ignore\n          run(pagination, values, {\n            allFormData: allFormDataRef.current,\n            type,\n          });\n        })\n        .catch((err) => err);\n    });\n  };\n\n  const reset = () => {\n    if (form) {\n      form.resetFields();\n    }\n    _submit({\n      ...(defaultParams?.[0] || {}),\n      pageSize: options.defaultPageSize || options.defaultParams?.[0]?.pageSize || 10,\n      current: 1,\n    });\n  };\n\n  const submit = (e?: any) => {\n    e?.preventDefault?.();\n    _submit(\n      runSuccessRef.current\n        ? undefined\n        : {\n            pageSize: options.defaultPageSize || options.defaultParams?.[0]?.pageSize || 10,\n            current: 1,\n            ...(defaultParams?.[0] || {}),\n          },\n    );\n  };\n\n  const onTableChange = (pagination: any, filters: any, sorter: any, extra: any) => {\n    const [oldPaginationParams, ...restParams] = params || [];\n    run(\n      // @ts-ignore\n      {\n        ...oldPaginationParams,\n        current: pagination.current,\n        pageSize: pagination.pageSize,\n        filters,\n        sorter,\n        extra,\n      },\n      ...restParams,\n    );\n  };\n\n  // init\n  useEffect(() => {\n    // if has cache, use cached params. ignore manual and ready.\n    if (params.length > 0) {\n      allFormDataRef.current = cacheFormTableData?.allFormData || {};\n      restoreForm();\n      // @ts-ignore\n      run(...params);\n      return;\n    }\n    if (ready) {\n      allFormDataRef.current = defaultParams?.[1] || {};\n      restoreForm();\n      if (!manual) {\n        _submit(defaultParams?.[0]);\n      }\n    }\n  }, []);\n\n  // change search type, restore form data\n  useUpdateEffect(() => {\n    if (!ready) {\n      return;\n    }\n    restoreForm();\n  }, [type]);\n\n  // refresh & ready change on the same time\n  const hasAutoRun = useRef(false);\n  hasAutoRun.current = false;\n\n  useUpdateEffect(() => {\n    if (!manual && ready) {\n      hasAutoRun.current = true;\n      if (form) {\n        form.resetFields();\n      }\n      allFormDataRef.current = defaultParams?.[1] || {};\n      restoreForm();\n      _submit(defaultParams?.[0]);\n    }\n  }, [ready]);\n\n  useUpdateEffect(() => {\n    if (hasAutoRun.current) {\n      return;\n    }\n    if (!ready) {\n      return;\n    }\n    if (!manual) {\n      hasAutoRun.current = true;\n      if (options.refreshDepsAction) {\n        options.refreshDepsAction();\n      } else {\n        result.pagination.changeCurrent(1);\n      }\n    }\n  }, [...refreshDeps]);\n\n  return {\n    ...result,\n    tableProps: {\n      dataSource: result.data?.list || defaultDataSourceRef.current,\n      loading: result.loading,\n      onChange: useMemoizedFn(onTableChange),\n      pagination: {\n        current: result.pagination.current,\n        pageSize: result.pagination.pageSize,\n        total: result.pagination.total,\n      },\n    },\n    search: {\n      submit: useMemoizedFn(submit),\n      type,\n      changeType: useMemoizedFn(changeType),\n      reset: useMemoizedFn(reset),\n    },\n  } as AntdTableResult<TData, TParams>;\n};\n\nexport default useAntdTable;\n"
  },
  {
    "path": "packages/hooks/src/useAntdTable/index.zh-CN.md",
    "content": "---\nnav:\n  title: Hooks\n  path: /hooks\n---\n\n# useAntdTable\n\n`useAntdTable` 基于 `useRequest` 实现，封装了常用的 [Ant Design Form](https://ant.design/components/form-cn/) 与 [Ant Design Table](https://ant.design/components/table-cn/) 联动逻辑，并且同时支持 antd v3 和 v4。\n\n在使用之前，你需要了解它与 `useRequest` 不同的几个点：\n\n1. `service` 接收两个参数，第一个参数为分页数据 `{ current, pageSize, sorter, filters, extra }`，第二个参数为表单数据。\n2. `service` 返回的数据结构为 `{ total: number, list: Item[] }`。\n3. 会额外返回 `tableProps` 和 `search` 字段，管理表格和表单。\n4. `refreshDeps` 变化，会重置 `current` 到第一页，并重新发起请求。\n\n## 代码演示\n\n以下展示的是 antd v4 的 demo，v3 请参考：https://ahooks-v2.js.org/hooks/table/use-antd-table\n\n### Table 管理\n\n`useAntdTable` 会自动管理 `Table` 分页数据，你只需要把返回的 `tableProps` 传递给 `Table` 组件就可以了。\n\n```tsx | pure\n<Table columns={columns} rowKey=\"email\" {...tableProps} />\n```\n\n<br />\n\n<code src=\"./demo/table.tsx\" />\n\n### Form 与 Table 联动\n\n`useAntdTable` 接收 `form` 实例后，会返回 search 对象，用来处理表单相关事件。\n\n- `search.type` 支持 `simple` 和 `advance` 两个表单切换\n- `search.changeType`，切换表单类型\n- `search.submit` 提交表单行为\n- `search.reset` 重置当前表单\n\n以下示例你可以体验表单与表格联动。\n\n<code src=\"./demo/form.tsx\" />\n\n### 初始化数据\n\n`useAntdTable` 通过 `defaultParams` 设置初始化值，`defaultParams` 是一个数组，第一项为分页相关参数，第二项为表单相关数据。如果有第二个值，我们会帮您初始化表单！\n\n需要注意的是，初始化的表单数据可以填写 `simple` 和 `advance` 全量的表单数据，我们会帮您挑选当前激活的类型中的表单数据。\n\n以下示例在初始化时设置了分页数据和表单数据。\n\n<code src=\"./demo/init.tsx\" />\n\n### 表单验证\n\n表单提交之前，我们会调用 `form.validateFields` 来校验表单数据，如果验证不通过，则不会发起请求。\n\n<code src=\"./demo/validate.tsx\" />\n\n### 缓存\n\n通过设置 `cacheKey`，我们可以实现 `Form` 与 `Table` 数据缓存。\n\n<code src=\"./demo/cache.tsx\" />\n\n## API\n\n`useRequest` 所有参数和返回结果均适用于 `useAntdTable`，此处不再赘述。\n\n```typescript\n\ntype Data = { total: number; list: any[] };\ntype Params = [{ current: number; pageSize: number, filters?: any, sorter?: any, extra?: any }, { [key: string]: any }];\n\nconst {\n  ...,\n  tableProps: {\n    dataSource: TData['list'];\n    loading: boolean;\n    onChange: (\n      pagination: any,\n      filters?: any,\n      sorter?: any,\n      extra?: any,\n    ) => void;\n    pagination: {\n      current: number;\n      pageSize: number;\n      total: number;\n    };\n  };\n  search: {\n    type: 'simple' | 'advance';\n    changeType: () => void;\n    submit: () => void;\n    reset: () => void;\n  };\n} = useAntdTable<TData extends Data, TParams extends Params>(\n  service: (...args: TParams) => Promise<TData>,\n  {\n    ...,\n    form?: any;\n    defaultType?: 'simple' | 'advance';\n    defaultParams?: TParams,\n    defaultPageSize?: number;\n    refreshDeps?: any[];\n  }\n);\n```\n\n### Result\n\n| 参数              | 说明                                                | 类型                  |\n| ----------------- | --------------------------------------------------- | --------------------- |\n| tableProps        | `Table` 组件需要的数据，直接透传给 `Table` 组件即可 | -                     |\n| search.type       | 当前表单类型                                        | `simple` \\| `advance` |\n| search.changeType | 切换表单类型                                        | `() => void`          |\n| search.submit     | 提交表单                                            | `() => void`          |\n| search.reset      | 重置当前表单                                        | `() => void`          |\n\n### Params\n\n| 参数            | 说明                                                          | 类型                     | 默认值   |\n| --------------- | ------------------------------------------------------------- | ------------------------ | -------- |\n| form            | `Form` 实例                                                   | -                        | -        |\n| defaultType     | 默认表单类型                                                  | `simple` \\| `advance`    | `simple` |\n| defaultParams   | 默认参数，第一项为分页数据，第二项为表单数据                  | `[pagination, formData]` | -        |\n| defaultPageSize | 默认分页数量                                                  | `number`                 | `10`     |\n| refreshDeps     | `refreshDeps` 变化，会重置 current 到第一页，并重新发起请求。 | `React.DependencyList`   | `[]`     |\n"
  },
  {
    "path": "packages/hooks/src/useAntdTable/types.ts",
    "content": "import type { PaginationOptions, PaginationResult } from '../usePagination/types';\n\nexport type Data = { total: number; list: any[] };\n\nexport type Params = [\n  {\n    current: number;\n    pageSize: number;\n    sorter?: any;\n    filters?: any;\n    extra?: any;\n    [key: string]: any;\n  },\n  ...any[],\n];\n\nexport type Service<TData extends Data, TParams extends Params> = (\n  ...args: TParams\n) => Promise<TData>;\n\nexport type Antd3ValidateFields = (\n  fieldNames: string[],\n  callback: (errors: any, values: Record<string, any>) => void,\n) => void;\nexport type Antd4ValidateFields = (fieldNames?: string[]) => Promise<Record<string, any>>;\n\nexport interface AntdFormUtils {\n  getFieldInstance?: (name: string) => Record<string, any>;\n  setFieldsValue: (value: Record<string, any>) => void;\n  getFieldsValue: (...args: any) => Record<string, any>;\n  resetFields: (...args: any) => void;\n  validateFields: Antd3ValidateFields | Antd4ValidateFields;\n  getInternalHooks?: any;\n  [key: string]: any;\n}\n\nexport interface AntdTableResult<TData extends Data, TParams extends Params>\n  extends PaginationResult<TData, TParams> {\n  tableProps: {\n    dataSource: TData['list'];\n    loading: boolean;\n    onChange: (pagination: any, filters?: any, sorter?: any) => void;\n    pagination: any;\n    [key: string]: any;\n  };\n  search: {\n    type: 'simple' | 'advance';\n    changeType: () => void;\n    submit: () => void;\n    reset: () => void;\n  };\n}\n\nexport interface AntdTableOptions<TData extends Data, TParams extends Params>\n  extends PaginationOptions<TData, TParams> {\n  form?: AntdFormUtils;\n  defaultType?: 'simple' | 'advance';\n}\n"
  },
  {
    "path": "packages/hooks/src/useAsyncEffect/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { useState } from 'react';\nimport { describe, expect, test } from 'vitest';\nimport { sleep } from '../../utils/testingHelpers';\nimport useAsyncEffect from '../index';\n\ndescribe('useAsyncEffect', () => {\n  test('should work without clean up', async () => {\n    const hook = renderHook(() => {\n      const [x, setX] = useState(0);\n      useAsyncEffect(async () => {\n        await sleep(100);\n        setX(1);\n      }, []);\n      return x;\n    });\n    expect(hook.result.current).toBe(0);\n    await act(async () => {\n      await sleep(150);\n    });\n    expect(hook.result.current).toBe(1);\n  });\n\n  test('should work with yield break', async () => {\n    const hook = renderHook(() => {\n      const [x, setX] = useState(1);\n      const [y, setY] = useState(0);\n      useAsyncEffect(\n        async function* () {\n          await sleep(100);\n          yield;\n          setY(x);\n        },\n        [x],\n      );\n      return {\n        y,\n        setX,\n      };\n    });\n    expect(hook.result.current.y).toBe(0);\n\n    await act(async () => {\n      await sleep(50);\n      hook.result.current.setX(2);\n    });\n    expect(hook.result.current.y).toBe(0);\n\n    await act(async () => {\n      await sleep(20);\n    });\n    expect(hook.result.current.y).toBe(0);\n\n    await act(async () => {\n      await sleep(50);\n      hook.result.current.setX(3);\n    });\n    expect(hook.result.current.y).toBe(0);\n\n    await act(async () => {\n      await sleep(80);\n    });\n    expect(hook.result.current.y).toBe(0);\n\n    await act(async () => {\n      await sleep(50);\n    });\n    expect(hook.result.current.y).toBe(3);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useAsyncEffect/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Do async check when component is mounted.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 组件加载时进行异步的检查\n */\n\nimport { useAsyncEffect } from 'ahooks';\nimport { useState } from 'react';\n\nfunction mockCheck(): Promise<boolean> {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(true);\n    }, 3000);\n  });\n}\n\nexport default () => {\n  const [pass, setPass] = useState<boolean>();\n\n  useAsyncEffect(async () => {\n    setPass(await mockCheck());\n  }, []);\n\n  return (\n    <div>\n      {pass === undefined && 'Checking...'}\n      {pass === true && 'Check passed.'}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useAsyncEffect/demo/demo2.tsx",
    "content": "/**\n * title: Break off\n * desc: Use `yield` to stop the execution when effect has been cleaned up.\n *\n * title.zh-CN: 中断执行\n * desc.zh-CN: 通过 `yield` 语句可以增加一些检查点，如果发现当前 effect 已经被清理，会停止继续往下执行。\n */\n\nimport { useState } from 'react';\nimport { useAsyncEffect } from 'ahooks';\n\nfunction mockCheck(val: string): Promise<boolean> {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(val.length > 0);\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const [value, setValue] = useState('');\n  const [pass, setPass] = useState<boolean>();\n\n  useAsyncEffect(\n    async function* () {\n      setPass(undefined);\n      const result = await mockCheck(value);\n      yield; // Check whether the effect is still valid, if it is has been cleaned up, stop at here.\n      setPass(result);\n    },\n    [value],\n  );\n\n  return (\n    <div>\n      <input\n        value={value}\n        onChange={(e) => {\n          setValue(e.target.value);\n        }}\n      />\n      <p>\n        {pass === null && 'Checking...'}\n        {pass === false && 'Check failed.'}\n        {pass === true && 'Check passed.'}\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useAsyncEffect/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useAsyncEffect\n\nuseEffect support async function.\n\n## 代码演示\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Break off\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nfunction useAsyncEffect(\n  effect: () => AsyncGenerator | Promise,\n  deps: DependencyList\n);\n```\n"
  },
  {
    "path": "packages/hooks/src/useAsyncEffect/index.ts",
    "content": "import type { DependencyList } from 'react';\nimport { useEffect } from 'react';\nimport { isFunction } from '../utils';\n\nfunction isAsyncGenerator(\n  val: AsyncGenerator<void, void, void> | Promise<void>,\n): val is AsyncGenerator<void, void, void> {\n  return isFunction((val as any)[Symbol.asyncIterator]);\n}\n\nfunction useAsyncEffect(\n  effect: () => AsyncGenerator<void, void, void> | Promise<void>,\n  deps?: DependencyList,\n) {\n  useEffect(() => {\n    const e = effect();\n    let cancelled = false;\n    async function execute() {\n      if (isAsyncGenerator(e)) {\n        while (true) {\n          const result = await e.next();\n          if (result.done || cancelled) {\n            break;\n          }\n        }\n      } else {\n        await e;\n      }\n    }\n    execute();\n    return () => {\n      cancelled = true;\n    };\n  }, deps);\n}\n\nexport default useAsyncEffect;\n"
  },
  {
    "path": "packages/hooks/src/useAsyncEffect/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useAsyncEffect\n\nuseEffect 支持异步函数。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 中断执行\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nfunction useAsyncEffect(\n  effect: () => AsyncGenerator | Promise,\n  deps: DependencyList\n);\n```\n"
  },
  {
    "path": "packages/hooks/src/useBoolean/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useBoolean from '../index';\n\nconst setUp = (defaultValue?: boolean) => renderHook(() => useBoolean(defaultValue));\n\ndescribe('useBoolean', () => {\n  test('test on methods', async () => {\n    const { result } = setUp();\n    expect(result.current[0]).toBe(false);\n    act(() => {\n      result.current[1].setTrue();\n    });\n    expect(result.current[0]).toBe(true);\n    act(() => {\n      result.current[1].setFalse();\n    });\n    expect(result.current[0]).toBe(false);\n    act(() => {\n      result.current[1].toggle();\n    });\n    expect(result.current[0]).toBe(true);\n    act(() => {\n      result.current[1].toggle();\n    });\n    expect(result.current[0]).toBe(false);\n    act(() => {\n      result.current[1].set(false);\n    });\n    expect(result.current[0]).toBe(false);\n    act(() => {\n      result.current[1].set(true);\n    });\n    expect(result.current[0]).toBe(true);\n    act(() => {\n      // @ts-ignore\n      result.current[1].set(0);\n    });\n    expect(result.current[0]).toBe(false);\n    act(() => {\n      // @ts-ignore\n      result.current[1].set('a');\n    });\n    expect(result.current[0]).toBe(true);\n  });\n\n  test('test on default value', () => {\n    const hook1 = setUp(true);\n    expect(hook1.result.current[0]).toBe(true);\n    const hook2 = setUp();\n    expect(hook2.result.current[0]).toBe(false);\n    // @ts-ignore\n    const hook3 = setUp(0);\n    expect(hook3.result.current[0]).toBe(false);\n    // @ts-ignore\n    const hook4 = setUp('');\n    expect(hook4.result.current[0]).toBe(false);\n    // @ts-ignore\n    const hook5 = setUp('hello');\n    expect(hook5.result.current[0]).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useBoolean/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Toggle boolean, default value can be set optionally.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 切换 boolean，可以接收默认值。\n */\n\nimport { useBoolean } from 'ahooks';\n\nexport default () => {\n  const [state, { toggle, setTrue, setFalse }] = useBoolean(true);\n\n  return (\n    <div>\n      <p>Effects：{JSON.stringify(state)}</p>\n      <p>\n        <button type=\"button\" onClick={toggle}>\n          Toggle\n        </button>\n        <button type=\"button\" onClick={setFalse} style={{ margin: '0 16px' }}>\n          Set false\n        </button>\n        <button type=\"button\" onClick={setTrue}>\n          Set true\n        </button>\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useBoolean/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useBoolean\n\nA hook that elegantly manages boolean state.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst [state, { toggle, set, setTrue, setFalse }] = useBoolean(\n  defaultValue?: boolean,\n);\n```\n\n### Params\n\n| Property     | Description                               | Type      | Default |\n| ------------ | ----------------------------------------- | --------- | ------- |\n| defaultValue | The default value of the state. Optional. | `boolean` | `false` |\n\n### Result\n\n| Property | Description                            | Type      |\n| -------- | -------------------------------------- | --------- |\n| state    | Current value                          | `boolean` |\n| actions  | A set of methods to update state value | `Actions` |\n\n### Actions\n\n| Property | Description          | Type                       |\n| -------- | -------------------- | -------------------------- |\n| toggle   | Toggle state         | `() => void`               |\n| set      | Set state            | `(value: boolean) => void` |\n| setTrue  | Set state to `true`  | `() => void`               |\n| setFalse | Set state to `false` | `() => void`               |\n"
  },
  {
    "path": "packages/hooks/src/useBoolean/index.ts",
    "content": "import { useMemo } from 'react';\nimport useToggle from '../useToggle';\n\nexport interface Actions {\n  setTrue: () => void;\n  setFalse: () => void;\n  set: (value: boolean) => void;\n  toggle: () => void;\n}\n\nexport default function useBoolean(defaultValue = false): [boolean, Actions] {\n  const [state, { toggle, set }] = useToggle(!!defaultValue);\n\n  const actions: Actions = useMemo(() => {\n    const setTrue = () => set(true);\n    const setFalse = () => set(false);\n    return {\n      toggle,\n      set: (v) => set(!!v),\n      setTrue,\n      setFalse,\n    };\n  }, []);\n\n  return [state, actions];\n}\n"
  },
  {
    "path": "packages/hooks/src/useBoolean/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useBoolean\n\n优雅的管理 boolean 状态的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst [state, { toggle, set, setTrue, setFalse }] = useBoolean(\n  defaultValue?: boolean,\n);\n```\n\n### Params\n\n| 参数         | 说明                     | 类型      | 默认值  |\n| ------------ | ------------------------ | --------- | ------- |\n| defaultValue | 可选项，传入默认的状态值 | `boolean` | `false` |\n\n### Result\n\n| 参数    | 说明     | 类型      |\n| ------- | -------- | --------- |\n| state   | 状态值   | `boolean` |\n| actions | 操作集合 | `Actions` |\n\n### Actions\n\n| 参数     | 说明         | 类型                       |\n| -------- | ------------ | -------------------------- |\n| toggle   | 切换 state   | `() => void`               |\n| set      | 设置 state   | `(value: boolean) => void` |\n| setTrue  | 设置为 true  | `() => void`               |\n| setFalse | 设置为 false | `() => void`               |\n"
  },
  {
    "path": "packages/hooks/src/useClickAway/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { afterEach, beforeEach, describe, expect, test } from 'vitest';\nimport useClickAway from '../index';\n\ndescribe('useClickAway', () => {\n  let container: HTMLDivElement;\n  let container1: HTMLDivElement;\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    container1 = document.createElement('div');\n    container1.setAttribute('id', 'ele');\n    document.body.appendChild(container);\n    document.body.appendChild(container1);\n  });\n\n  afterEach(() => {\n    document.body.removeChild(container);\n    document.body.removeChild(container1);\n  });\n\n  test('test on dom optional', async () => {\n    let state: number = 0;\n    const { rerender, unmount } = renderHook((dom: any) =>\n      useClickAway(() => {\n        state++;\n      }, dom),\n    );\n\n    rerender(container);\n    container.click();\n    expect(state).toBe(0);\n    document.body.click();\n    expect(state).toBe(1);\n\n    rerender(container1);\n    container1.click();\n    expect(state).toBe(1);\n    document.body.click();\n    expect(state).toBe(2);\n\n    unmount();\n    document.body.click();\n    expect(state).toBe(2);\n  });\n\n  test('should works on multiple target', async () => {\n    let state: number = 0;\n    const { rerender, unmount } = renderHook((dom: any) =>\n      useClickAway(() => {\n        state++;\n      }, dom),\n    );\n\n    rerender([container, container1]);\n    container.click();\n    expect(state).toBe(0);\n    container1.click();\n    expect(state).toBe(0);\n    document.body.click();\n    expect(state).toBe(1);\n\n    unmount();\n    document.body.click();\n    expect(state).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useClickAway/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Please click button or outside of button to show effects.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 请点击按钮或按钮外查看效果。\n */\n\nimport { useState, useRef } from 'react';\nimport { useClickAway } from 'ahooks';\n\nexport default () => {\n  const [counter, setCounter] = useState(0);\n  const ref = useRef<HTMLButtonElement>(null);\n  useClickAway(() => {\n    setCounter((s) => s + 1);\n  }, ref);\n\n  return (\n    <div>\n      <button ref={ref} type=\"button\">\n        box\n      </button>\n      <p>counter: {counter}</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useClickAway/demo/demo2.tsx",
    "content": "/**\n * title: Support DOM\n * desc: Support pass in a DOM element or function.\n *\n * title.zh-CN: 支持传入 DOM\n * desc.zh-CN: 支持直接传入 DOM 对象或 function。\n */\n\nimport { useState } from 'react';\nimport { useClickAway } from 'ahooks';\n\nexport default () => {\n  const [counter, setCounter] = useState(0);\n\n  useClickAway(\n    () => {\n      setCounter((s) => s + 1);\n    },\n    () => document.getElementById('use-click-away-button'),\n  );\n\n  return (\n    <div>\n      <button type=\"button\" id=\"use-click-away-button\">\n        box\n      </button>\n      <p>counter: {counter}</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useClickAway/demo/demo3.tsx",
    "content": "/**\n * title: Support multiple DOM\n * desc: Support pass multiple DOM elements.\n *\n * title.zh-CN: 支持多个 DOM 对象\n * desc.zh-CN: 支持传入多个目标对象。\n */\n\nimport { useState, useRef } from 'react';\nimport { useClickAway } from 'ahooks';\n\nexport default () => {\n  const [counter, setCounter] = useState(0);\n  const ref1 = useRef(null);\n  const ref2 = useRef(null);\n  useClickAway(() => {\n    setCounter((s) => s + 1);\n  }, [ref1, ref2]);\n\n  return (\n    <div>\n      <button type=\"button\" ref={ref1}>\n        box1\n      </button>\n      <button type=\"button\" ref={ref2} style={{ marginLeft: 16 }}>\n        box2\n      </button>\n      <p>counter: {counter}</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useClickAway/demo/demo4.tsx",
    "content": "/**\n * title: Listen to other events\n * desc: By setting eventName, you can specify the event to be listened, Try click the right mouse.\n *\n * title.zh-CN: 监听其它事件\n * desc.zh-CN: 通过设置 eventName，可以指定需要监听的事件，试试点击鼠标右键。\n */\n\nimport { useState, useRef } from 'react';\nimport { useClickAway } from 'ahooks';\n\nexport default () => {\n  const [counter, setCounter] = useState(0);\n  const ref = useRef<HTMLButtonElement>(null);\n  useClickAway(\n    () => {\n      setCounter((s) => s + 1);\n    },\n    ref,\n    'contextmenu',\n  );\n\n  return (\n    <div>\n      <button ref={ref} type=\"button\">\n        box\n      </button>\n      <p>counter: {counter}</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useClickAway/demo/demo5.tsx",
    "content": "/**\n * title: Support multiple events\n * desc: Set up multiple events, you can try using the mouse click or right click.\n *\n * title.zh-CN: 支持传入多个事件名称\n * desc.zh-CN: 设置了多个事件，你可以试试用鼠标左键或者右键。\n */\n\nimport { useState, useRef } from 'react';\nimport { useClickAway } from 'ahooks';\n\nexport default () => {\n  const [counter, setCounter] = useState(0);\n  const ref = useRef(null);\n  useClickAway(\n    () => {\n      setCounter((s) => s + 1);\n    },\n    ref,\n    ['click', 'contextmenu'],\n  );\n\n  return (\n    <div>\n      <button type=\"button\" ref={ref}>\n        box\n      </button>\n      <p>counter: {counter}</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useClickAway/demo/demo6.tsx",
    "content": "/**\n * title: Support shadow DOM\n * desc: Add the addEventListener to shadow DOM root instead of the document\n *\n * title.zh-CN: 支持 shadow DOM\n * desc.zh-CN: 将 addEventListener 添加到 shadow DOM root\n */\n\nimport { useState, useRef } from 'react';\nimport { useClickAway } from 'ahooks';\nimport root from 'react-shadow';\n\nexport default () => {\n  const [counter, setCounter] = useState(0);\n  const ref = useRef(null);\n  useClickAway(\n    () => {\n      setCounter((s) => s + 1);\n    },\n    ref,\n    ['click', 'contextmenu'],\n  );\n\n  return (\n    <root.div>\n      <div>\n        <button type=\"button\" ref={ref}>\n          box\n        </button>\n        <p>counter: {counter}</p>\n      </div>\n    </root.div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useClickAway/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useClickAway\n\nListen for click events outside the target element.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Custom DOM\n\n<code src=\"./demo/demo2.tsx\" />\n\n### Support multiple DOM\n\n<code src=\"./demo/demo3.tsx\" />\n\n### Listen for other events\n\n<code src=\"./demo/demo4.tsx\" />\n\n### Support multiple events\n\n<code src=\"./demo/demo5.tsx\"/>\n\n### Support shadow DOM\n\n<code src=\"./demo/demo6.tsx\"/>\n\n## API\n\n```typescript\ntype Target = Element | (() => Element) | React.MutableRefObject<Element>;\ntype DocumentEventKey = keyof DocumentEventMap;\n\nuseClickAway<T extends Event = Event>(\n  onClickAway: (event: T) => void,\n  target: Target | Target[],\n  eventName?: DocumentEventKey | DocumentEventKey[]\n);\n```\n\n### Params\n\n| Property    | Description                                    | Type                                       | Default |\n| ----------- | ---------------------------------------------- | ------------------------------------------ | ------- |\n| onClickAway | Trigger Function                               | `(event: T) => void`                       | -       |\n| target      | DOM elements or Ref or Function, support array | `Target` \\| `Target[]`                     | -       |\n| eventName   | Set the event to be listened, support array    | `DocumentEventKey` \\| `DocumentEventKey[]` | `click` |\n"
  },
  {
    "path": "packages/hooks/src/useClickAway/index.ts",
    "content": "import useLatest from '../useLatest';\nimport type { BasicTarget } from '../utils/domTarget';\nimport { getTargetElement } from '../utils/domTarget';\nimport getDocumentOrShadow from '../utils/getDocumentOrShadow';\nimport useEffectWithTarget from '../utils/useEffectWithTarget';\n\ntype DocumentEventKey = keyof DocumentEventMap;\n\nexport default function useClickAway<T extends Event = Event>(\n  onClickAway: (event: T) => void,\n  target: BasicTarget | BasicTarget[],\n  eventName: DocumentEventKey | DocumentEventKey[] = 'click',\n) {\n  const onClickAwayRef = useLatest(onClickAway);\n\n  useEffectWithTarget(\n    () => {\n      const handler = (event: any) => {\n        const targets = Array.isArray(target) ? target : [target];\n        if (\n          targets.some((item) => {\n            const targetElement = getTargetElement(item);\n            return !targetElement || targetElement.contains(event.target);\n          })\n        ) {\n          return;\n        }\n        onClickAwayRef.current(event);\n      };\n\n      const documentOrShadow = getDocumentOrShadow(target);\n\n      const eventNames = Array.isArray(eventName) ? eventName : [eventName];\n\n      eventNames.forEach((event) => documentOrShadow.addEventListener(event, handler));\n\n      return () => {\n        eventNames.forEach((event) => documentOrShadow.removeEventListener(event, handler));\n      };\n    },\n    Array.isArray(eventName) ? eventName : [eventName],\n    target,\n  );\n}\n"
  },
  {
    "path": "packages/hooks/src/useClickAway/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useClickAway\n\n监听目标元素外的点击事件。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 自定义 DOM\n\n<code src=\"./demo/demo2.tsx\" />\n\n### 支持多个 DOM 对象\n\n<code src=\"./demo/demo3.tsx\" />\n\n### 监听其它事件\n\n<code src=\"./demo/demo4.tsx\" />\n\n### 支持多个事件\n\n<code src=\"./demo/demo5.tsx\" />\n\n### 支持 shadow DOM\n\n<code src=\"./demo/demo6.tsx\" />\n\n## API\n\n```typescript\ntype Target = Element | (() => Element) | React.MutableRefObject<Element>;\ntype DocumentEventKey = keyof DocumentEventMap;\n\nuseClickAway<T extends Event = Event>(\n  onClickAway: (event: T) => void,\n  target: Target | Target[],\n  eventName?: DocumentEventKey | DocumentEventKey[]\n);\n```\n\n### Params\n\n| 参数        | 说明                                | 类型                                       | 默认值  |\n| ----------- | ----------------------------------- | ------------------------------------------ | ------- |\n| onClickAway | 触发函数                            | `(event: T) => void`                       | -       |\n| target      | DOM 节点或者 Ref 或者函数，支持数组 | `Target` \\| `Target[]`                     | -       |\n| eventName   | 指定需要监听的事件，支持数组        | `DocumentEventKey` \\| `DocumentEventKey[]` | `click` |\n"
  },
  {
    "path": "packages/hooks/src/useControllableValue/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport type { Options, Props } from '../index';\nimport useControllableValue from '../index';\n\ndescribe('useControllableValue', () => {\n  const setUp = (props?: Props, options?: Options<any>) =>\n    renderHook(() => useControllableValue(props, options));\n\n  test('defaultValue should work', () => {\n    const hook = setUp({ defaultValue: 1 });\n    expect(hook.result.current[0]).toBe(1);\n  });\n\n  test('value should work', () => {\n    const hook = setUp({ defaultValue: 1, value: 2 });\n    expect(hook.result.current[0]).toBe(2);\n  });\n\n  test('state should be undefined', () => {\n    const hook = setUp();\n    expect(hook.result.current[0]).toBeUndefined();\n  });\n\n  test('onChange should work', () => {\n    let extraParam: string = '';\n    const props = {\n      value: 2,\n      onChange(v: any, extra: any) {\n        this.value = v;\n        extraParam = extra;\n      },\n    };\n    const hook = setUp(props);\n    expect(hook.result.current[0]).toBe(2);\n    act(() => {\n      hook.result.current[1](3, 'extraParam');\n    });\n    expect(props.value).toBe(3);\n    expect(extraParam).toBe('extraParam');\n  });\n\n  test('test on state update', () => {\n    const props: any = {\n      value: 1,\n    };\n    const { result, rerender } = setUp(props);\n    props.value = 2;\n    rerender(props);\n    expect(result.current[0]).toBe(2);\n    props.value = 3;\n    rerender(props);\n    expect(result.current[0]).toBe(3);\n  });\n\n  test('test set state', async () => {\n    const { result } = setUp({\n      newValue: 1,\n    });\n    const [, setValue] = result.current;\n    act(() => setValue(undefined));\n    expect(result.current[0]).toBeUndefined();\n\n    act(() => setValue(null));\n    expect(result.current[0]).toBeNull();\n\n    act(() => setValue(55));\n    expect(result.current[0]).toBe(55);\n\n    act(() => setValue((prevState: number) => prevState + 1));\n    expect(result.current[0]).toBe(56);\n  });\n\n  test('type inference should work', async () => {\n    type Value = {\n      foo: number;\n    };\n    const props: {\n      value: Value;\n      defaultValue: Value;\n      onChange: (val: Value) => void;\n    } = {\n      value: {\n        foo: 123,\n      },\n      defaultValue: {\n        foo: 123,\n      },\n      onChange: () => {},\n    };\n    const hook = renderHook(() => useControllableValue(props));\n    const [v] = hook.result.current;\n    expect(v.foo).toBe(123);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useControllableValue/demo/demo1.tsx",
    "content": "/**\n * title: Uncontrolled component\n * desc: If there is no value in props, the component manage state by self\n *\n * title.zh-CN: 非受控组件\n * desc.zh-CN: 如果 props 中没有 value，则组件内部自己管理 state\n */\nimport { useControllableValue } from 'ahooks';\n\nexport default (props: any) => {\n  const [state, setState] = useControllableValue<string>(props, {\n    defaultValue: '',\n  });\n\n  return (\n    <>\n      <input value={state} onChange={(e) => setState(e.target.value)} style={{ width: 300 }} />\n      <button type=\"button\" onClick={() => setState('')} style={{ marginLeft: 8 }}>\n        Clear\n      </button>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useControllableValue/demo/demo2.tsx",
    "content": "/**\n * title: Controlled component\n * desc: If props has the value field, then the state is controlled by it's parent\n *\n * title.zh-CN: 受控组件\n * desc.zh-CN: 如果 props 有 value 字段，则由父级接管控制 state\n */\n\nimport { useState } from 'react';\nimport { useControllableValue } from 'ahooks';\n\nconst ControllableComponent = (props: any) => {\n  const [state, setState] = useControllableValue<string>(props);\n\n  return <input value={state} onChange={(e) => setState(e.target.value)} style={{ width: 300 }} />;\n};\n\nconst Parent = () => {\n  const [state, setState] = useState<string>('');\n  const clear = () => {\n    setState('');\n  };\n\n  return (\n    <>\n      <ControllableComponent value={state} onChange={setState} />\n      <button type=\"button\" onClick={clear} style={{ marginLeft: 8 }}>\n        Clear\n      </button>\n    </>\n  );\n};\nexport default Parent;\n"
  },
  {
    "path": "packages/hooks/src/useControllableValue/demo/demo3.tsx",
    "content": "/**\n * title: No value, have onChange component\n * desc: If there is an onChange field in props, the onChange will be trigger when state change\n *\n * title.zh-CN: 无 value，有 onChange 的组件\n * desc.zh-CN: 只要 props 中有 onChange 字段，则在 state 变化时，就会触发 onChange 函数\n */\n\nimport { useState } from 'react';\nimport { useControllableValue } from 'ahooks';\n\nconst ControllableComponent = (props: any) => {\n  const [state, setState] = useControllableValue<string>(props);\n\n  return (\n    <input\n      value={state}\n      onChange={(e) => {\n        setState(e.target.value);\n      }}\n      style={{ width: 300 }}\n    />\n  );\n};\nconst Parent = () => {\n  const [state, setState] = useState<number>(0);\n\n  return (\n    <>\n      <div style={{ marginBottom: 8 }}>state:{state}</div>\n      <ControllableComponent onChange={setState} />\n    </>\n  );\n};\nexport default Parent;\n"
  },
  {
    "path": "packages/hooks/src/useControllableValue/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useControllableValue\n\nIn some components, we need the state to be managed by itself or controlled by it's parent. `useControllableValue` is a Hook that helps you manage this kind of state.\n\n## Examples\n\n### Uncontrolled component\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Controlled component\n\n<code src=\"./demo/demo2.tsx\" />\n\n### No value, have onChange component\n\n<code src=\"./demo/demo3.tsx\" />\n\n## API\n\n```typescript\nconst [state, setState] = useControllableValue(props: Record<string, any>, options?: Options);\n```\n\n### Result\n\n| Property | Description | Type                                                |\n| -------- | ----------- | --------------------------------------------------- |\n| state    | State       | -                                                   |\n| setState | Set state   | `(value: any \\| ((prevState: any) => any)) => void` |\n\n### Params\n\n| Property | Description            | Type                  | Default |\n| -------- | ---------------------- | --------------------- | ------- |\n| props    | Component props        | `Record<string, any>` | -       |\n| options  | Optional configuration | `Options`             | -       |\n\n### Options\n\n| Property             | Description                                                                     | Type     | Default        |\n| -------------------- | ------------------------------------------------------------------------------- | -------- | -------------- |\n| defaultValue         | The default value, will be overridden by `props.defaultValue` and `props.value` | -        | -              |\n| defaultValuePropName | Custom defaultValue attribute name                                               | `string` | `defaultValue` |\n| valuePropName        | Custom value attribute name                                                     | `string` | `value`        |\n| trigger              | Custom trigger attribute name                                                   | `string` | `onChange`     |\n"
  },
  {
    "path": "packages/hooks/src/useControllableValue/index.ts",
    "content": "import { useMemo, useRef } from 'react';\nimport type { SetStateAction } from 'react';\nimport { isFunction } from '../utils';\nimport useMemoizedFn from '../useMemoizedFn';\nimport useUpdate from '../useUpdate';\n\nexport interface Options<T> {\n  defaultValue?: T;\n  defaultValuePropName?: string;\n  valuePropName?: string;\n  trigger?: string;\n}\n\nexport type Props = Record<string, any>;\n\nexport interface StandardProps<T> {\n  value: T;\n  defaultValue?: T;\n  onChange: (val: T) => void;\n}\n\nfunction useControllableValue<T = any>(\n  props: StandardProps<T>,\n): [T, (v: SetStateAction<T>) => void];\nfunction useControllableValue<T = any>(\n  props?: Props,\n  options?: Options<T>,\n): [T, (v: SetStateAction<T>, ...args: any[]) => void];\nfunction useControllableValue<T = any>(defaultProps?: Props, options: Options<T> = {}) {\n  const props = defaultProps ?? {};\n\n  const {\n    defaultValue,\n    defaultValuePropName = 'defaultValue',\n    valuePropName = 'value',\n    trigger = 'onChange',\n  } = options;\n\n  const value = props[valuePropName] as T;\n  const isControlled = Object.prototype.hasOwnProperty.call(props, valuePropName);\n\n  const initialValue = useMemo(() => {\n    if (isControlled) {\n      return value;\n    }\n    if (Object.prototype.hasOwnProperty.call(props, defaultValuePropName)) {\n      return props[defaultValuePropName];\n    }\n    return defaultValue;\n  }, []);\n\n  const stateRef = useRef(initialValue);\n  if (isControlled) {\n    stateRef.current = value;\n  }\n\n  const update = useUpdate();\n\n  function setState(v: SetStateAction<T>, ...args: any[]) {\n    const r = isFunction(v) ? v(stateRef.current) : v;\n\n    if (!isControlled) {\n      stateRef.current = r;\n      update();\n    }\n    if (props[trigger]) {\n      props[trigger](r, ...args);\n    }\n  }\n\n  return [stateRef.current, useMemoizedFn(setState)] as const;\n}\n\nexport default useControllableValue;\n"
  },
  {
    "path": "packages/hooks/src/useControllableValue/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useControllableValue\n\n在某些组件开发时，我们需要组件的状态既可以自己管理，也可以被外部控制，`useControllableValue` 就是帮你管理这种状态的 Hook。\n\n## 代码演示\n\n### 非受控组件\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 受控组件\n\n<code src=\"./demo/demo2.tsx\" />\n\n### 无 value，有 onChange 的组件\n\n<code src=\"./demo/demo3.tsx\" />\n\n## API\n\n```typescript\nconst [state, setState] = useControllableValue(props: Record<string, any>, options?: Options);\n```\n\n### Result\n\n| 参数     | 说明              | 类型                                                |\n| -------- | ----------------- | --------------------------------------------------- |\n| state    | 状态值            | -                                                   |\n| setState | 修改 state 的函数 | `(value: any \\| ((prevState: any) => any)) => void` |\n\n### Params\n\n| 参数    | 说明         | 类型                  | 默认值 |\n| ------- | ------------ | --------------------- | ------ |\n| props   | 组件的 props | `Record<string, any>` | -      |\n| options | 可选配置项   | `Options`             | -      |\n\n### Options\n\n| 参数                 | 说明                                                    | 类型     | 默认值         |\n| -------------------- | ------------------------------------------------------- | -------- | -------------- |\n| defaultValue         | 默认值，会被 `props.defaultValue` 和 `props.value` 覆盖 | -        | -              |\n| defaultValuePropName | 默认值的属性名                                          | `string` | `defaultValue` |\n| valuePropName        | 值的属性名                                              | `string` | `value`        |\n| trigger              | 修改值时，触发的函数                                    | `string` | `onChange`     |\n"
  },
  {
    "path": "packages/hooks/src/useCookieState/__tests__/index.spec.tsx",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport Cookies from 'js-cookie';\nimport { describe, expect, test } from 'vitest';\nimport type { Options } from '../index';\nimport useCookieState from '../index';\n\ndescribe('useCookieState', () => {\n  const setUp = (key: string, options: Options) =>\n    renderHook(() => {\n      const [state, setState] = useCookieState(key, options);\n      return {\n        state,\n        setState,\n      } as const;\n    });\n\n  test('getKey should work', () => {\n    const COOKIE = 'test-key';\n    const hook = setUp(COOKIE, {\n      defaultValue: 'A',\n    });\n    expect(hook.result.current.state).toBe('A');\n    act(() => {\n      hook.result.current.setState('B');\n    });\n    expect(hook.result.current.state).toBe('B');\n    const anotherHook = setUp(COOKIE, {\n      defaultValue: 'A',\n    });\n    expect(anotherHook.result.current.state).toBe('B');\n    act(() => {\n      anotherHook.result.current.setState('C');\n    });\n    expect(anotherHook.result.current.state).toBe('C');\n    expect(hook.result.current.state).toBe('B');\n    expect(Cookies.get(COOKIE)).toBe('C');\n  });\n\n  test('should support undefined', () => {\n    const COOKIE = 'test-boolean-key-with-undefined';\n    const hook = setUp(COOKIE, {\n      defaultValue: 'undefined',\n    });\n    expect(hook.result.current.state).toBe('undefined');\n    act(() => {\n      hook.result.current.setState(undefined);\n    });\n    expect(hook.result.current.state).toBeUndefined();\n    const anotherHook = setUp(COOKIE, {\n      defaultValue: 'false',\n    });\n    expect(anotherHook.result.current.state).toBe('false');\n    expect(Cookies.get(COOKIE)).toBeUndefined();\n    act(() => {\n      // @ts-ignore\n      hook.result.current.setState();\n    });\n    expect(hook.result.current.state).toBeUndefined();\n    expect(Cookies.get(COOKIE)).toBeUndefined();\n  });\n\n  test('should support empty string', () => {\n    Cookies.set('test-key-empty-string', '');\n    expect(Cookies.get('test-key-empty-string')).toBe('');\n    const COOKIE = 'test-key-empty-string';\n    const hook = setUp(COOKIE, {\n      defaultValue: 'hello',\n    });\n    expect(hook.result.current.state).toBe('');\n  });\n\n  test('should support function updater', () => {\n    const COOKIE = 'test-func-updater';\n    const hook = setUp(COOKIE, {\n      defaultValue: () => 'hello world',\n    });\n    expect(hook.result.current.state).toBe('hello world');\n    act(() => {\n      hook.result.current.setState((state) => `${state}, zhangsan`);\n    });\n    expect(hook.result.current.state).toBe('hello world, zhangsan');\n  });\n\n  test('using the same cookie name', () => {\n    const COOKIE_NAME = 'test-same-cookie-name';\n    const { result: result1 } = setUp(COOKIE_NAME, { defaultValue: 'A' });\n    const { result: result2 } = setUp(COOKIE_NAME, { defaultValue: 'B' });\n    expect(result1.current.state).toBe('A');\n    expect(result2.current.state).toBe('B');\n    act(() => {\n      result1.current.setState('C');\n    });\n    expect(result1.current.state).toBe('C');\n    expect(result2.current.state).toBe('B');\n    expect(Cookies.get(COOKIE_NAME)).toBe('C');\n    act(() => {\n      result2.current.setState('D');\n    });\n    expect(result1.current.state).toBe('C');\n    expect(result2.current.state).toBe('D');\n    expect(Cookies.get(COOKIE_NAME)).toBe('D');\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useCookieState/demo/demo1.tsx",
    "content": "/**\n * title: Store state into Cookie\n * desc: Refresh this page and you will get the state from Cookie.\n *\n * title.zh-CN: 将 state 存储在 Cookie 中\n * desc.zh-CN: 刷新页面后，可以看到输入框中的内容被从 Cookie 中恢复了。\n */\n\nimport { useCookieState } from 'ahooks';\n\nexport default () => {\n  const [message, setMessage] = useCookieState('useCookieStateString');\n  return (\n    <input\n      value={message}\n      placeholder=\"Please enter some words...\"\n      onChange={(e) => setMessage(e.target.value)}\n      style={{ width: 300 }}\n    />\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useCookieState/demo/demo2.tsx",
    "content": "/**\n * title: SetState can receive function\n * desc: Function updater is also acceptable with useCookieState's setState，similar to how useState is used.\n *\n * title.zh-CN: setState 可以接收函数\n * desc.zh-CN: useCookieState 的 setState 可以接收 function updater，就像 useState 那样。\n */\n\nimport { useCookieState } from 'ahooks';\n\nexport default function App() {\n  const [value, setValue] = useCookieState('useCookieStateUpdater', {\n    defaultValue: '0',\n  });\n\n  return (\n    <>\n      <p>{value}</p>\n      <button\n        type=\"button\"\n        style={{ marginRight: '16px' }}\n        onClick={() => setValue((v) => String(Number(v) + 1))}\n      >\n        inc +\n      </button>\n      <button\n        type=\"button\"\n        style={{ marginRight: '16px' }}\n        onClick={() => setValue((v) => String(Number(v) - 1))}\n      >\n        dec -\n      </button>\n      <button type=\"button\" onClick={() => setValue('0')}>\n        reset\n      </button>\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/hooks/src/useCookieState/demo/demo3.tsx",
    "content": "/**\n * title: Use the option property to configure Cookie\n * desc: 'Available options: defaultValue、expires、path、domain、secure、sameSite etc.'\n *\n * title.zh-CN: 使用 option 配置 Cookie\n * desc.zh-CN: 可配置属性：默认值、有效时间、路径、域名、协议、跨域等，详见 Options 文档。\n */\n\nimport { useCookieState } from 'ahooks';\n\nexport default function App() {\n  const [value, setValue] = useCookieState('useCookieStateOptions', {\n    defaultValue: '0',\n    path: '/',\n    expires: (() => new Date(+new Date() + 10000))(),\n  });\n\n  return (\n    <>\n      <p>{value}</p>\n      <button\n        type=\"button\"\n        style={{ marginRight: 16 }}\n        onClick={() =>\n          setValue((v) => String(Number(v) + 1), {\n            expires: (() => new Date(+new Date() + 10000))(),\n          })\n        }\n      >\n        inc + (10s expires)\n      </button>\n      <button\n        type=\"button\"\n        style={{ marginRight: 16 }}\n        onClick={() =>\n          setValue((v) => String(Number(v) - 1), {\n            expires: (() => new Date(+new Date() + 10000))(),\n          })\n        }\n      >\n        dec - (10s expires)\n      </button>\n      <button type=\"button\" onClick={() => setValue('0')}>\n        reset\n      </button>\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/hooks/src/useCookieState/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useCookieState\n\nA Hook that store state into Cookie.\n\n## Examples\n\n### Store state into Cookie\n\n<code src=\"./demo/demo1.tsx\" />\n\n### SetState can receive function\n\n<code src=\"./demo/demo2.tsx\" />\n\n### Use the option property to configure Cookie\n\n<code src=\"./demo/demo3.tsx\" />\n\n## API\n\n```typescript\ntype State = string | undefined;\n\ntype SetState = (\n  newValue?: State | ((prevState?: State) => State),\n  options?: Cookies.CookieAttributes,\n) => void;\n\nconst [state, setState]: [State, SetState] = useCookieState(\n  cookieKey: string,\n  options?: Options,\n);\n```\n\nIf you want to delete this record from document.cookie, use `setState()` or `setState(undefined)`.\n\n### Params\n\n| Property  | Description                    | Type      | Default |\n| --------- | ------------------------------ | --------- | ------- |\n| cookieKey | The key of Cookie              | `string`  | -       |\n| options   | Optional. Cookie configuration | `Options` | -       |\n\n### Result\n\n| Property | Description         | Type                    |\n| -------- | ------------------- | ----------------------- |\n| state    | Local Cookie value  | `string` \\| `undefined` |\n| setState | Update Cookie value | `SetState`              |\n\nsetState can update cookie options, which will be merged with the options set by `useCookieState`.\n\n`const targetOptions = { ...options, ...updateOptions }`\n\n### Options\n\n| Property     | Description                                                                                | Type                                                       | Default     |\n| ------------ | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------- | ----------- |\n| defaultValue | Optional. Default value, but not store to Cookie                                           | `string` \\| `undefined` \\| `(() => (string \\| undefined))` | `undefined` |\n| expires      | Optional. Set Cookie expiration time                                                       | `number` \\| `Date`                                         | -           |\n| path         | Optional. Specify available paths                                                          | `string`                                                   | `/`         |\n| domain       | Optional. Specify available domain. Default creation domain                                | `string`                                                   | -           |\n| secure       | Optional. Specify whether the Cookie can only be transmitted over secure protocol as https | `boolean`                                                  | `false`     |\n| sameSite     | Optional. Specify whether the browser can send this Cookie along with cross-site requests  | `strict` \\| `lax` \\| `none`                                | -           |\n\nOptions is same as [js-cookie attributes](https://github.com/js-cookie/js-cookie#cookie-attributes).\n"
  },
  {
    "path": "packages/hooks/src/useCookieState/index.ts",
    "content": "import Cookies from 'js-cookie';\nimport { useState } from 'react';\nimport useMemoizedFn from '../useMemoizedFn';\nimport { isFunction, isString } from '../utils';\n\nexport type State = string | undefined;\n\nexport interface Options extends Cookies.CookieAttributes {\n  defaultValue?: State | (() => State);\n}\n\nfunction useCookieState(cookieKey: string, options: Options = {}) {\n  const [state, setState] = useState<State>(() => {\n    const cookieValue = Cookies.get(cookieKey);\n\n    if (isString(cookieValue)) {\n      return cookieValue;\n    }\n\n    if (isFunction(options.defaultValue)) {\n      return options.defaultValue();\n    }\n\n    return options.defaultValue;\n  });\n\n  const updateState = useMemoizedFn(\n    (\n      newValue: State | ((prevState: State) => State),\n      newOptions: Cookies.CookieAttributes = {},\n    ) => {\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars\n      const { defaultValue, ...restOptions } = { ...options, ...newOptions };\n      const value = isFunction(newValue) ? newValue(state) : newValue;\n\n      setState(value);\n\n      if (value === undefined) {\n        Cookies.remove(cookieKey);\n      } else {\n        Cookies.set(cookieKey, value, restOptions);\n      }\n    },\n  );\n\n  return [state, updateState] as const;\n}\n\nexport default useCookieState;\n"
  },
  {
    "path": "packages/hooks/src/useCookieState/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useCookieState\n\n一个可以将状态存储在 Cookie 中的 Hook 。\n\n## 代码演示\n\n### 将 state 存储在 Cookie 中\n\n<code src=\"./demo/demo1.tsx\" />\n\n### setState 可以接收函数\n\n<code src=\"./demo/demo2.tsx\" />\n\n### 使用 option 配置 Cookie\n\n<code src=\"./demo/demo3.tsx\" />\n\n## API\n\n```typescript\ntype State = string | undefined;\n\ntype SetState = (\n  newValue?: State | ((prevState?: State) => State),\n  options?: Cookies.CookieAttributes,\n) => void;\n\nconst [state, setState]: [State, SetState] = useCookieState(\n  cookieKey: string,\n  options?: Options,\n);\n```\n\n注意：如果想从 document.cookie 中删除这条数据，可以使用 `setState()` 或 `setState(undefined)`。\n\n### Params\n\n| 参数      | 说明                     | 类型      | 默认值 |\n| --------- | ------------------------ | --------- | ------ |\n| cookieKey | Cookie 的 key 值         | `string`  | -      |\n| options   | 可选项，配置 Cookie 属性 | `Options` | -      |\n\n### Result\n\n| 参数     | 说明           | 类型                    |\n| -------- | -------------- | ----------------------- |\n| state    | 本地 Cookie 值 | `string` \\| `undefined` |\n| setState | 设置 Cookie 值 | `SetState`              |\n\nsetState 可以更新 cookie options，会与 `useCookieState` 设置的 options 进行 merge 操作。\n\n`const targetOptions = { ...options, ...updateOptions }`\n\n### Options\n\n| 参数         | 说明                                                 | 类型                                                       | 默认值      |\n| ------------ | ---------------------------------------------------- | ---------------------------------------------------------- | ----------- |\n| defaultValue | 可选，定义 Cookie 默认值，但不同步到本地 Cookie      | `string` \\| `undefined` \\| `(() => (string \\| undefined))` | `undefined` |\n| expires      | 可选，定义 Cookie 存储有效时间                       | `number` \\| `Date`                                         | -           |\n| path         | 可选，定义 Cookie 可用的路径                         | `string`                                                   | `/`         |\n| domain       | 可选，定义 Cookie 可用的域，默认为 Cookie 创建的域名 | `string`                                                   | -           |\n| secure       | 可选，Cookie 传输是否需要 https 安全协议             | `boolean`                                                  | `false`     |\n| sameSite     | 可选，Cookie 不能与跨域请求一起发送                  | `strict` \\| `lax` \\| `none`                                | -           |\n\nOptions 与 [js-cookie attributes](https://github.com/js-cookie/js-cookie#cookie-attributes) 保持一致。\n"
  },
  {
    "path": "packages/hooks/src/useCountDown/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { afterAll, beforeAll, describe, expect, test, vi } from 'vitest';\nimport type { Options } from '../index';\nimport useCountDown from '../index';\n\nconst setup = (options: Options = {}) =>\n  renderHook((props: Options = options) => useCountDown(props));\n\ndescribe('useCountDown', () => {\n  beforeAll(() => {\n    vi.useFakeTimers();\n    vi.setSystemTime(1479427200000);\n  });\n\n  afterAll(() => {\n    vi.useRealTimers();\n  });\n\n  test('should initialize correctly with undefined targetDate', () => {\n    const { result } = setup();\n\n    const [count, formattedRes] = result.current;\n\n    expect(count).toBe(0);\n    expect(formattedRes).toEqual({\n      days: 0,\n      hours: 0,\n      minutes: 0,\n      seconds: 0,\n      milliseconds: 0,\n    });\n  });\n\n  test('should initialize correctly with correct targetDate', () => {\n    const { result } = setup({\n      targetDate: Date.now() + 5000,\n      interval: 1000,\n    });\n    const [count, formattedRes] = result.current;\n    expect(count).toBe(5000);\n    expect(formattedRes.seconds).toBe(5);\n    expect(formattedRes.milliseconds).toBe(0);\n  });\n\n  test('should work manually', () => {\n    const { result, rerender } = setup({ interval: 100 });\n\n    rerender({ targetDate: Date.now() + 5000, interval: 1000 });\n    expect(result.current[0]).toBe(5000);\n    expect(result.current[1].seconds).toBe(5);\n\n    act(() => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current[0]).toBe(4000);\n    expect(result.current[1].seconds).toBe(4);\n\n    act(() => {\n      vi.advanceTimersByTime(4000);\n    });\n    expect(result.current[0]).toBe(0);\n    expect(result.current[1].seconds).toBe(0);\n\n    act(() => {\n      vi.advanceTimersByTime(1000);\n    });\n\n    expect(result.current[0]).toBe(0);\n    expect(result.current[1].seconds).toBe(0);\n  });\n\n  test('should work automatically', () => {\n    const { result } = setup({\n      targetDate: Date.now() + 5000,\n      interval: 1000,\n    });\n\n    expect(result.current[0]).toBe(5000);\n    expect(result.current[1].seconds).toBe(5);\n\n    act(() => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current[0]).toBe(4000);\n    expect(result.current[1].seconds).toBe(4);\n\n    act(() => {\n      vi.advanceTimersByTime(4000);\n    });\n    expect(result.current[0]).toBe(0);\n    expect(result.current[1].seconds).toBe(0);\n  });\n\n  test('should work stop', () => {\n    const { result, rerender } = setup({\n      targetDate: Date.now() + 5000,\n      interval: 1000,\n    });\n\n    rerender({\n      targetDate: Date.now() + 5000,\n      interval: 1000,\n    });\n    expect(result.current[0]).toBe(5000);\n    expect(result.current[1].seconds).toBe(5);\n\n    act(() => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current[0]).toBe(4000);\n    expect(result.current[1].seconds).toBe(4);\n\n    rerender({\n      targetDate: undefined,\n    });\n    expect(result.current[0]).toBe(0);\n    expect(result.current[1].seconds).toBe(0);\n  });\n\n  test('it onEnd should work', () => {\n    const onEnd = vi.fn();\n    setup({\n      targetDate: Date.now() + 5000,\n      interval: 1000,\n      onEnd,\n    });\n    act(() => {\n      vi.advanceTimersByTime(6000);\n    });\n    expect(onEnd).toBeCalled();\n  });\n\n  test('timeLeft should be 0 when target date less than current time', () => {\n    const { result } = setup({\n      targetDate: Date.now() - 5000,\n    });\n    expect(result.current[0]).toBe(0);\n  });\n\n  test('should initialize correctly with undefined leftTime', () => {\n    const { result } = setup();\n\n    const [count, formattedRes] = result.current;\n\n    expect(count).toBe(0);\n    expect(formattedRes).toEqual({\n      days: 0,\n      hours: 0,\n      minutes: 0,\n      seconds: 0,\n      milliseconds: 0,\n    });\n  });\n\n  test('should initialize correctly with correct leftTime', () => {\n    const { result } = setup({ leftTime: 5 * 1000, interval: 1000 });\n    const [count, formattedRes] = result.current;\n    expect(count).toBe(5000);\n    expect(formattedRes.seconds).toBe(5);\n    expect(formattedRes.milliseconds).toBe(0);\n  });\n\n  test('should work manually', () => {\n    const { result, rerender } = setup({ interval: 100 });\n\n    rerender({ leftTime: 5 * 1000, interval: 1000 });\n    expect(result.current[0]).toBe(5000);\n    expect(result.current[1].seconds).toBe(5);\n\n    act(() => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current[0]).toBe(4000);\n    expect(result.current[1].seconds).toBe(4);\n\n    act(() => {\n      vi.advanceTimersByTime(4000);\n    });\n    expect(result.current[0]).toBe(0);\n    expect(result.current[1].seconds).toBe(0);\n\n    act(() => {\n      vi.advanceTimersByTime(1000);\n    });\n\n    expect(result.current[0]).toBe(0);\n    expect(result.current[1].seconds).toBe(0);\n  });\n\n  test('should work automatically', () => {\n    const { result } = setup({ leftTime: 5 * 1000, interval: 1000 });\n\n    expect(result.current[0]).toBe(5000);\n    expect(result.current[1].seconds).toBe(5);\n\n    act(() => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current[0]).toBe(4000);\n    expect(result.current[1].seconds).toBe(4);\n\n    act(() => {\n      vi.advanceTimersByTime(4000);\n    });\n    expect(result.current[0]).toBe(0);\n    expect(result.current[1].seconds).toBe(0);\n  });\n\n  test('should work stop', () => {\n    const { result, rerender } = setup({ leftTime: 5 * 1000, interval: 1000 });\n\n    rerender({ leftTime: 5 * 1000, interval: 1000 });\n    expect(result.current[0]).toBe(5000);\n    expect(result.current[1].seconds).toBe(5);\n\n    act(() => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current[0]).toBe(4000);\n    expect(result.current[1].seconds).toBe(4);\n\n    rerender({ leftTime: undefined });\n    expect(result.current[0]).toBe(0);\n    expect(result.current[1].seconds).toBe(0);\n  });\n\n  test('it onEnd should work', () => {\n    const onEnd = vi.fn();\n    setup({ leftTime: 5 * 1000, interval: 1000, onEnd });\n    act(() => {\n      vi.advanceTimersByTime(6000);\n    });\n    expect(onEnd).toBeCalled();\n  });\n\n  test('timeLeft should be 0 when leftTime less than current time', () => {\n    const { result } = setup({ leftTime: -5 * 1000 });\n    expect(result.current[0]).toBe(0);\n  });\n\n  test('run with timeLeft should not be reset after targetDate changed', async () => {\n    let targetDate = Date.now() + 8000;\n\n    const { result, rerender } = setup({\n      leftTime: 6000,\n      targetDate,\n    });\n    expect(result.current[0]).toBe(6000);\n\n    act(() => {\n      vi.advanceTimersByTime(2000);\n    });\n    rerender({\n      leftTime: 6000,\n      targetDate: targetDate,\n    });\n    expect(result.current[0]).toBe(4000);\n\n    targetDate = Date.now() + 9000;\n    rerender({\n      leftTime: 6000,\n      targetDate: targetDate,\n    });\n    expect(result.current[0]).toBe(4000);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useCountDown/demo/demo1.tsx",
    "content": "/**\n * title: Basic Usage\n * desc: Basic countdown management.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 基础的倒计时管理。\n */\n\nimport { useCountDown } from 'ahooks';\n\nexport default () => {\n  const [, formattedRes] = useCountDown({\n    targetDate: `${new Date().getFullYear()}-12-31 23:59:59`,\n  });\n  const { days, hours, minutes, seconds, milliseconds } = formattedRes;\n  return (\n    <p>\n      There are {days} days {hours} hours {minutes} minutes {seconds} seconds {milliseconds}{' '}\n      milliseconds until {new Date().getFullYear()}-12-31 23:59:59\n    </p>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useCountDown/demo/demo2.tsx",
    "content": "/**\n * title: Adcanved Uasge\n * desc: Dynamic change targetDate, suitable for verification codes or similar scenarios.\n *\n * title.zh-CN: 进阶使用\n * desc.zh-CN: 动态变更配置项, 适用于验证码或类似场景，时间结束后会触发 onEnd 回调。\n */\n\nimport { useState } from 'react';\nimport { useCountDown } from 'ahooks';\n\nexport default () => {\n  const [targetDate, setTargetDate] = useState<number>();\n\n  const [countdown] = useCountDown({\n    targetDate,\n    onEnd: () => {\n      alert('End of the time');\n    },\n  });\n\n  return (\n    <>\n      <button\n        onClick={() => {\n          setTargetDate(Date.now() + 5000);\n        }}\n        disabled={countdown !== 0}\n      >\n        {countdown === 0 ? 'Start Interval' : `Reset After ${Math.round(countdown / 1000)}s`}\n      </button>\n      <button\n        onClick={() => {\n          setTargetDate(undefined);\n        }}\n        style={{ marginLeft: 8 }}\n      >\n        stop\n      </button>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useCountDown/demo/demo3.tsx",
    "content": "/**\n * title: The rest of time\n * desc: A countdown to the number of milliseconds remaining.\n *\n * title.zh-CN: 剩余时间\n * desc.zh-CN: 剩余时间毫秒数的倒计时\n */\n\nimport React from 'react';\nimport { useCountDown } from 'ahooks';\n\nconst App: React.FC = () => {\n  const [countdown] = useCountDown({ leftTime: 60 * 1000 });\n  return <p>{countdown}</p>;\n};\n\nexport default App;\n"
  },
  {
    "path": "packages/hooks/src/useCountDown/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useCountDown\n\nA hook for manage countdown.\n\n## Countdown to target time\n\n<code src=\"./demo/demo1.tsx\" />\n\n## Dynamic config\n\n<code src=\"./demo/demo2.tsx\" />\n\n## Config leftTime\n\n<code src=\"./demo/demo3.tsx\" />\n\n## API\n\n```typescript\ntype TDate = Date | number | string | undefined;\n\ninterface FormattedRes {\n  days: number;\n  hours: number;\n  minutes: number;\n  seconds: number;\n  milliseconds: number;\n}\n\nconst [countdown, formattedRes] = useCountDown(\n  {\n    leftTime,\n    targetDate,\n    interval,\n    onEnd\n  }\n);\n```\n\n**Remark**\n\nThe precision of useCountDown is milliseconds, which may cause the following problems\n\n- Even if the interval time is set to 1000ms, the update interval of useCountDown may not be exactly 1000ms, but around it.\n- In the second demo, countdown is generally 499x milliseconds at the beginning due to the execution delay of the program.\n\nIf you only need to be accurate to the second, you can use it like this `Math.round(countdown / 1000)`.\n\nIf both `leftTime` and `targetDate` are passed, the `targetDate` is ignored, the `leftTime` is dominant.\n\n### Params\n\n| Property   | Description                                  | Type         | Default |\n| ---------- | -------------------------------------------- | ------------ | ------- |\n| leftTime   | The rest of time, in milliseconds            | `number`     | -       |\n| targetDate | Target time                                  | `TDate`      | -       |\n| interval   | Time interval between ticks, in milliseconds | `number`     | `1000`  |\n| onEnd      | Function to call when countdown completes    | `() => void` | -       |\n\n### Return\n\n| Params          | Description                              | Type           |\n| --------------- | ---------------------------------------- | -------------- |\n| countdown       | Timestamp to targetDate, in milliseconds | `number`       |\n| formattedResult | Formatted countdown                      | `FormattedRes` |\n\n## Remark\n\n`leftTime`、`targetDate`、`interval`、`onEnd` support dynamic change.\n"
  },
  {
    "path": "packages/hooks/src/useCountDown/index.ts",
    "content": "import dayjs from 'dayjs';\nimport { useEffect, useMemo, useState } from 'react';\nimport useLatest from '../useLatest';\nimport { isNumber } from '../utils/index';\n\nexport type TDate = dayjs.ConfigType;\n\nexport interface Options {\n  leftTime?: number;\n  targetDate?: TDate;\n  interval?: number;\n  onEnd?: () => void;\n}\n\nexport interface FormattedRes {\n  days: number;\n  hours: number;\n  minutes: number;\n  seconds: number;\n  milliseconds: number;\n}\n\nconst calcLeft = (target?: TDate) => {\n  if (!target) {\n    return 0;\n  }\n  // https://stackoverflow.com/questions/4310953/invalid-date-in-safari\n  const left = dayjs(target).valueOf() - Date.now();\n  return left < 0 ? 0 : left;\n};\n\nconst parseMs = (milliseconds: number): FormattedRes => {\n  return {\n    days: Math.floor(milliseconds / 86400000),\n    hours: Math.floor(milliseconds / 3600000) % 24,\n    minutes: Math.floor(milliseconds / 60000) % 60,\n    seconds: Math.floor(milliseconds / 1000) % 60,\n    milliseconds: Math.floor(milliseconds) % 1000,\n  };\n};\n\nconst useCountdown = (options: Options = {}) => {\n  const { leftTime, targetDate, interval = 1000, onEnd } = options || {};\n\n  const memoLeftTime = useMemo<TDate>(() => {\n    return isNumber(leftTime) && leftTime > 0 ? Date.now() + leftTime : undefined;\n  }, [leftTime]);\n\n  const target = 'leftTime' in options ? memoLeftTime : targetDate;\n\n  const [timeLeft, setTimeLeft] = useState(() => calcLeft(target));\n\n  const onEndRef = useLatest(onEnd);\n\n  useEffect(() => {\n    if (!target) {\n      // for stop\n      setTimeLeft(0);\n      return;\n    }\n\n    // 立即执行一次\n    setTimeLeft(calcLeft(target));\n\n    const timer = setInterval(() => {\n      const targetLeft = calcLeft(target);\n      setTimeLeft(targetLeft);\n      if (targetLeft === 0) {\n        clearInterval(timer);\n        onEndRef.current?.();\n      }\n    }, interval);\n\n    return () => clearInterval(timer);\n  }, [target, interval]);\n\n  const formattedRes = useMemo(() => parseMs(timeLeft), [timeLeft]);\n\n  return [timeLeft, formattedRes] as const;\n};\n\nexport default useCountdown;\n"
  },
  {
    "path": "packages/hooks/src/useCountDown/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useCountDown\n\n一个用于管理倒计时的 Hook。\n\n## 到未来某一时间点的倒计时\n\n<code src=\"./demo/demo1.tsx\" />\n\n## 配置项动态变化\n\n<code src=\"./demo/demo2.tsx\" />\n\n## 通过 leftTime 配置剩余时间\n\n<code src=\"./demo/demo3.tsx\" />\n\n**说明**\n\nuseCountDown 的精度为毫秒，可能会造成以下几个问题\n\n- 即使设置 interval 时间为 1000 毫秒，useCountDown 每次更新间隔也**不一定**正好是 1000 毫秒，而是 1000 毫秒左右。\n- 在第二个 demo 中，countdown 开始一般是 499x 毫秒，因为程序执行有延迟。\n\n如果你的精度只要到秒就好了，可以这样用 `Math.round(countdown / 1000)`。\n\n如果同时传了 `leftTime` 和 `targetDate`，则会忽略 `targetDate`，以 `leftTime` 为主\n\n## API\n\n```typescript\ntype TDate = Date | number | string | undefined;\n\ninterface FormattedRes {\n  days: number;\n  hours: number;\n  minutes: number;\n  seconds: number;\n  milliseconds: number;\n}\n\nconst [countdown, formattedRes] = useCountDown(\n  {\n    leftTime,\n    targetDate,\n    interval,\n    onEnd\n  }\n);\n```\n\n### Params\n\n| 参数       | 说明                 | 类型         | 默认值 |\n| ---------- | -------------------- | ------------ | ------ |\n| leftTime   | 剩余时间（毫秒）     | `number`     | -      |\n| targetDate | 目标时间             | `TDate`      | -      |\n| interval   | 变化时间间隔（毫秒） | `number`     | `1000` |\n| onEnd      | 倒计时结束触发       | `() => void` | -      |\n\n### Result\n\n| 参数         | 说明                 | 类型           |\n| ------------ | -------------------- | -------------- |\n| countdown    | 倒计时时间戳（毫秒） | `number`       |\n| formattedRes | 格式化后的倒计时     | `FormattedRes` |\n\n## 备注\n\n`leftTime`、`targetDate`、`interval`、`onEnd` 支持动态变化\n"
  },
  {
    "path": "packages/hooks/src/useCounter/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport type { Options } from '../index';\nimport useCounter from '../index';\n\nconst setUp = (init?: number, options?: Options) => renderHook(() => useCounter(init, options));\n\ndescribe('useCounter', () => {\n  test('should init counter', () => {\n    const { result } = setUp(100);\n    const [current] = result.current;\n    expect(current).toBe(100);\n  });\n\n  test('should max, min, actions work', () => {\n    const { result } = setUp(100, { max: 10, min: 1 });\n    const [current, { inc, dec, reset, set }] = result.current;\n    expect(current).toBe(10);\n    act(() => {\n      inc(1);\n    });\n    expect(result.current[0]).toBe(10);\n    act(() => {\n      dec(100);\n    });\n    expect(result.current[0]).toBe(1);\n    act(() => {\n      inc();\n    });\n    expect(result.current[0]).toBe(2);\n    act(() => {\n      reset();\n    });\n    expect(result.current[0]).toBe(10);\n    act(() => {\n      set(-1000);\n    });\n    expect(result.current[0]).toBe(1);\n    act(() => {\n      set((c) => c + 2);\n    });\n    expect(result.current[0]).toBe(3);\n\n    act(() => {\n      inc();\n      inc();\n      inc();\n    });\n    expect(result.current[0]).toBe(6);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useCounter/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Simple example of counter management.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 简单的 counter 管理示例。\n */\n\nimport { useCounter } from 'ahooks';\n\nexport default () => {\n  const [current, { inc, dec, set, reset }] = useCounter(100, { min: 1, max: 10 });\n\n  return (\n    <div>\n      <p>{current} [max: 10; min: 1;]</p>\n      <div>\n        <button\n          type=\"button\"\n          onClick={() => {\n            inc();\n          }}\n          style={{ marginRight: 8 }}\n        >\n          inc()\n        </button>\n        <button\n          type=\"button\"\n          onClick={() => {\n            dec();\n          }}\n          style={{ marginRight: 8 }}\n        >\n          dec()\n        </button>\n        <button\n          type=\"button\"\n          onClick={() => {\n            set(3);\n          }}\n          style={{ marginRight: 8 }}\n        >\n          set(3)\n        </button>\n        <button type=\"button\" onClick={reset} style={{ marginRight: 8 }}>\n          reset()\n        </button>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useCounter/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useCounter\n\nA hook that manage counter.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst [current, {\n  inc,\n  dec,\n  set,\n  reset\n}] = useCounter(initialValue, { min, max });\n```\n\n### Result\n\n| Property | Description                          | Type                                                   |\n| -------- | ------------------------------------ | ------------------------------------------------------ |\n| current  | Current value                        | `number`                                               |\n| inc      | Increment, default delta is 1        | `(delta?: number) => void`                             |\n| dec      | Decrement, default delta is 1        | `(delta?: number) => void`                             |\n| set      | Set current value                    | `(value: number` \\| `((c: number) => number)) => void` |\n| reset    | Reset current value to initial value | `() => void`                                           |\n\n### Params\n\n| Property     | Description   | Type     | Default |\n| ------------ | ------------- | -------- | ------- |\n| initialValue | Initial count | `number` | `0`     |\n| min          | Min count     | `number` | -       |\n| max          | Max count     | `number` | -       |\n"
  },
  {
    "path": "packages/hooks/src/useCounter/index.ts",
    "content": "import { useState } from 'react';\nimport useMemoizedFn from '../useMemoizedFn';\nimport { isNumber } from '../utils';\n\nexport interface Options {\n  min?: number;\n  max?: number;\n}\n\nexport interface Actions {\n  inc: (delta?: number) => void;\n  dec: (delta?: number) => void;\n  set: (value: number | ((c: number) => number)) => void;\n  reset: () => void;\n}\n\nexport type ValueParam = number | ((c: number) => number);\n\nfunction getTargetValue(val: number, options: Options = {}) {\n  const { min, max } = options;\n  let target = val;\n  if (isNumber(max)) {\n    target = Math.min(max, target);\n  }\n  if (isNumber(min)) {\n    target = Math.max(min, target);\n  }\n  return target;\n}\n\nfunction useCounter(initialValue: number = 0, options: Options = {}) {\n  const { min, max } = options;\n\n  const [current, setCurrent] = useState(() => {\n    return getTargetValue(initialValue, {\n      min,\n      max,\n    });\n  });\n\n  const setValue = (value: ValueParam) => {\n    setCurrent((c) => {\n      const target = isNumber(value) ? value : value(c);\n      return getTargetValue(target, {\n        max,\n        min,\n      });\n    });\n  };\n\n  const inc = (delta: number = 1) => {\n    setValue((c) => c + delta);\n  };\n\n  const dec = (delta: number = 1) => {\n    setValue((c) => c - delta);\n  };\n\n  const set = (value: ValueParam) => {\n    setValue(value);\n  };\n\n  const reset = () => {\n    setValue(initialValue);\n  };\n\n  return [\n    current,\n    {\n      inc: useMemoizedFn(inc),\n      dec: useMemoizedFn(dec),\n      set: useMemoizedFn(set),\n      reset: useMemoizedFn(reset),\n    },\n  ] as const;\n}\n\nexport default useCounter;\n"
  },
  {
    "path": "packages/hooks/src/useCounter/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useCounter\n\n管理计数器的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst [current, {\n  inc,\n  dec,\n  set,\n  reset\n}] = useCounter(initialValue, { min, max });\n```\n\n### Result\n\n| 参数    | 说明         | 类型                                                   |\n| ------- | ------------ | ------------------------------------------------------ |\n| current | 当前值       | `number`                                               |\n| inc     | 加，默认加 1 | `(delta?: number) => void`                             |\n| dec     | 减，默认减 1 | `(delta?: number) => void`                             |\n| set     | 设置 current | `(value: number` \\| `((c: number) => number)) => void` |\n| reset   | 重置为默认值 | `() => void`                                           |\n\n### Params\n\n| 参数         | 说明   | 类型     | 默认值 |\n| ------------ | ------ | -------- | ------ |\n| initialValue | 默认值 | `number` | `0`    |\n| min          | 最小值 | `number` | -      |\n| max          | 最大值 | `number` | -      |\n"
  },
  {
    "path": "packages/hooks/src/useCreation/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { useState } from 'react';\nimport { describe, expect, test } from 'vitest';\nimport useCreation from '../index';\n\ndescribe('useCreation', () => {\n  class Foo {\n    constructor() {\n      this.data = Math.random();\n    }\n\n    data: number;\n  }\n\n  const setUp = () =>\n    renderHook(() => {\n      const [count, setCount] = useState(0);\n      const [, setFlag] = useState({});\n      const foo = useCreation(() => new Foo(), [count]);\n      return {\n        foo,\n        setCount,\n        count,\n        setFlag,\n      };\n    });\n\n  test('should work', () => {\n    const hook = setUp();\n    const { foo } = hook.result.current;\n    act(() => {\n      hook.result.current.setFlag({});\n    });\n    expect(hook.result.current.foo).toBe(foo);\n    act(() => {\n      hook.result.current.setCount(1);\n    });\n    expect(hook.result.current.foo).not.toBe(foo);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useCreation/demo/demo1.tsx",
    "content": "/**\n * title: Make sure only one instance is created\n * desc: You can click the \"Rerender\" button and trigger the update of this component. But the instance of Foo will not change.\n *\n * title.zh-CN: 确保实例不会被重复创建\n * desc.zh-CN: 点击 \"Rerender\" 按钮，触发组件的更新，但 Foo 的实例会保持不变\n */\n\nimport { useState } from 'react';\nimport { useCreation } from 'ahooks';\n\nclass Foo {\n  constructor() {\n    this.data = Math.random();\n  }\n\n  data: number;\n}\n\nexport default function () {\n  const foo = useCreation(() => new Foo(), []);\n  const [, setFlag] = useState({});\n  return (\n    <>\n      <p>{foo.data}</p>\n      <button\n        type=\"button\"\n        onClick={() => {\n          setFlag({});\n        }}\n      >\n        Rerender\n      </button>\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/hooks/src/useCreation/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useCreation\n\n`useCreation` is the replacement for `useMemo` or `useRef`.\n\n`useMemo` can't guarantee the memoized value will not be recalculated, while `useCreation` can guarantee that. As the the official document of React.js says:\n\n> **You may rely on useMemo as a performance optimization, not as a semantic guarantee.** In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without `useMemo` — and then add it to optimize performance.\n\nAnd similar to `useRef`, you can use `useCreation` to create some constants. But `useCreation` can avoid performance hazards.\n\n```javascript\nconst a = useRef(new Subject()); // A new Subject instance is created in every render.\nconst b = useCreation(() => new Subject(), []); // By using factory function, Subject is only instantiated once.\n```\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```javascript\nfunction useCreation<T>(factory: () => T, deps: any[]): T;\n```\n\n### Params\n\n| Property | Description                              | Type        | Default |\n| -------- | ---------------------------------------- | ----------- | ------- |\n| factory  | A function used for creating the object. | `() => any` | -       |\n| deps     | The dependencies list.                   | `any[]`     | -       |\n"
  },
  {
    "path": "packages/hooks/src/useCreation/index.ts",
    "content": "import type { DependencyList } from 'react';\nimport { useRef } from 'react';\nimport depsAreSame from '../utils/depsAreSame';\n\nconst useCreation = <T>(factory: () => T, deps: DependencyList) => {\n  const { current } = useRef({\n    deps,\n    obj: undefined as T,\n    initialized: false,\n  });\n  if (current.initialized === false || !depsAreSame(current.deps, deps)) {\n    current.deps = deps;\n    current.obj = factory();\n    current.initialized = true;\n  }\n  return current.obj;\n};\n\nexport default useCreation;\n"
  },
  {
    "path": "packages/hooks/src/useCreation/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useCreation\n\n`useCreation` 是 `useMemo` 或 `useRef` 的替代品。\n\n因为 `useMemo` 不能保证被 memo 的值一定不会被重新计算，而 `useCreation` 可以保证这一点。以下为 React 官方文档中的介绍：\n\n> **You may rely on useMemo as a performance optimization, not as a semantic guarantee.** In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without `useMemo` — and then add it to optimize performance.\n\n而相比于 `useRef`，你可以使用 `useCreation` 创建一些常量，这些常量和 `useRef` 创建出来的 ref 有很多使用场景上的相似，但对于复杂常量的创建，`useRef` 却容易出现潜在的性能隐患。\n\n```javascript\nconst a = useRef(new Subject()); // 每次重渲染，都会执行实例化 Subject 的过程，即便这个实例立刻就被扔掉了\nconst b = useCreation(() => new Subject(), []); // 通过 factory 函数，可以避免性能隐患\n```\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nfunction useCreation<T>(factory: () => T, deps: any[]): T;\n```\n\n### Params\n\n| 参数    | 说明                   | 类型        | 默认值 |\n| ------- | ---------------------- | ----------- | ------ |\n| factory | 用来创建所需对象的函数 | `() => any` | -      |\n| deps    | 传入依赖变化的对象     | `any[]`     | -      |\n"
  },
  {
    "path": "packages/hooks/src/useDebounce/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport { sleep } from '../../utils/testingHelpers';\nimport useDebounce from '../index';\n\ndescribe('useDebounce', () => {\n  test('useDebounce wait:200ms', async () => {\n    let mountedState = 0;\n    const { result, rerender } = renderHook(() => useDebounce(mountedState, { wait: 200 }));\n    expect(result.current).toBe(0);\n\n    mountedState = 1;\n    rerender();\n    await sleep(50);\n    expect(result.current).toBe(0);\n\n    mountedState = 2;\n    rerender();\n    await sleep(100);\n    expect(result.current).toBe(0);\n\n    mountedState = 3;\n    rerender();\n    await sleep(150);\n    expect(result.current).toBe(0);\n\n    mountedState = 4;\n    rerender();\n    await act(async () => {\n      await sleep(250);\n    });\n    expect(result.current).toBe(4);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useDebounce/debounceOptions.ts",
    "content": "export interface DebounceOptions {\n  wait?: number;\n  leading?: boolean;\n  trailing?: boolean;\n  maxWait?: number;\n}\n"
  },
  {
    "path": "packages/hooks/src/useDebounce/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: DebouncedValue will change after the input ends 500ms.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: DebouncedValue 只会在输入结束 500ms 后变化。\n */\n\nimport { useState } from 'react';\nimport { useDebounce } from 'ahooks';\n\nexport default () => {\n  const [value, setValue] = useState<string>();\n  const debouncedValue = useDebounce(value, { wait: 500 });\n\n  return (\n    <div>\n      <input\n        value={value}\n        onChange={(e) => setValue(e.target.value)}\n        placeholder=\"Typed value\"\n        style={{ width: 280 }}\n      />\n      <p style={{ marginTop: 16 }}>DebouncedValue: {debouncedValue}</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useDebounce/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useDebounce\n\nA hook that deal with the debounced value.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst debouncedValue = useDebounce(\n  value: any,\n  options?: Options\n);\n```\n\n### Params\n\n| Property | Description                        | Type      | Default |\n| -------- | ---------------------------------- | --------- | ------- |\n| value    | The value to debounce.             | `any`     | -       |\n| options  | Config for the debounce behaviors. | `Options` | -       |\n\n### Options\n\n| Property | Description                                                         | Type      | Default |\n| -------- | ------------------------------------------------------------------- | --------- | ------- |\n| wait     | The number of milliseconds to delay.                                | `number`  | `1000`  |\n| leading  | Specify invoking on the leading edge of the timeout.                | `boolean` | `false` |\n| trailing | Specify invoking on the trailing edge of the timeout.               | `boolean` | `true`  |\n| maxWait  | The maximum time func is allowed to be delayed before it’s invoked. | `number`  | -       |\n"
  },
  {
    "path": "packages/hooks/src/useDebounce/index.ts",
    "content": "import { useEffect, useState } from 'react';\nimport useDebounceFn from '../useDebounceFn';\nimport type { DebounceOptions } from './debounceOptions';\n\nfunction useDebounce<T>(value: T, options?: DebounceOptions) {\n  const [debounced, setDebounced] = useState(value);\n\n  const { run } = useDebounceFn(() => {\n    setDebounced(value);\n  }, options);\n\n  useEffect(() => {\n    run();\n  }, [value]);\n\n  return debounced;\n}\n\nexport default useDebounce;\n"
  },
  {
    "path": "packages/hooks/src/useDebounce/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useDebounce\n\n用来处理防抖值的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst debouncedValue = useDebounce(\n  value: any,\n  options?: Options\n);\n```\n\n### Params\n\n| 参数    | 说明           | 类型      | 默认值 |\n| ------- | -------------- | --------- | ------ |\n| value   | 需要防抖的值   | `any`     | -      |\n| options | 配置防抖的行为 | `Options` | -      |\n\n### Options\n\n| 参数     | 说明                     | 类型      | 默认值  |\n| -------- | ------------------------ | --------- | ------- |\n| wait     | 超时时间，单位为毫秒     | `number`  | `1000`  |\n| leading  | 是否在延迟开始前调用函数 | `boolean` | `false` |\n| trailing | 是否在延迟开始后调用函数 | `boolean` | `true`  |\n| maxWait  | 最大等待时间，单位为毫秒 | `number`  | -       |\n"
  },
  {
    "path": "packages/hooks/src/useDebounceEffect/__tests__/index.spec.ts",
    "content": "import { act, type RenderHookResult, renderHook } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\nimport { sleep } from '../../utils/testingHelpers';\nimport useDebounceEffect from '../index';\n\nlet hook: RenderHookResult<any, any>;\n\ndescribe('useDebounceEffect', () => {\n  test('useDebounceEffect should work', async () => {\n    let mountedState = 1;\n    const mockEffect = vi.fn(() => {});\n    const mockCleanUp = vi.fn(() => {});\n    act(() => {\n      hook = renderHook(() =>\n        useDebounceEffect(\n          () => {\n            mockEffect();\n            return () => {\n              mockCleanUp();\n            };\n          },\n          [mountedState],\n          { wait: 200 },\n        ),\n      );\n    });\n\n    expect(mockEffect.mock.calls.length).toBe(0);\n    expect(mockCleanUp.mock.calls.length).toBe(0);\n    mountedState = 2;\n    hook.rerender();\n    await sleep(50);\n    mountedState = 3;\n    hook.rerender();\n    expect(mockEffect.mock.calls.length).toBe(0);\n    expect(mockCleanUp.mock.calls.length).toBe(0);\n    await act(async () => {\n      await sleep(300);\n    });\n    expect(mockEffect.mock.calls.length).toBe(1);\n    expect(mockCleanUp.mock.calls.length).toBe(0);\n    mountedState = 4;\n    hook.rerender();\n    expect(mockEffect.mock.calls.length).toBe(1);\n    expect(mockCleanUp.mock.calls.length).toBe(0);\n    await act(async () => {\n      await sleep(300);\n    });\n    expect(mockEffect.mock.calls.length).toBe(2);\n    expect(mockCleanUp.mock.calls.length).toBe(1);\n  });\n\n  test('should cancel timeout on unmount', async () => {\n    const mockEffect = vi.fn(() => {});\n    const mockCleanUp = vi.fn(() => {});\n\n    const hook2 = renderHook(\n      (props) =>\n        useDebounceEffect(\n          () => {\n            mockEffect();\n            return () => {\n              mockCleanUp();\n            };\n          },\n          [props],\n          { wait: 200 },\n        ),\n      { initialProps: 0 },\n    );\n\n    expect(mockEffect.mock.calls.length).toBe(0);\n    expect(mockCleanUp.mock.calls.length).toBe(0);\n\n    hook2.rerender(1);\n    await sleep(50);\n    expect(mockEffect.mock.calls.length).toBe(0);\n    expect(mockCleanUp.mock.calls.length).toBe(0);\n\n    await act(async () => {\n      await sleep(300);\n    });\n    expect(mockEffect.mock.calls.length).toBe(1);\n    expect(mockCleanUp.mock.calls.length).toBe(0);\n\n    hook2.rerender(2);\n    await act(async () => {\n      await sleep(300);\n    });\n    expect(mockEffect.mock.calls.length).toBe(2);\n    expect(mockCleanUp.mock.calls.length).toBe(1);\n\n    hook2.unmount();\n    expect(mockEffect.mock.calls.length).toBe(2);\n    expect(mockCleanUp.mock.calls.length).toBe(2);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useDebounceEffect/demo/demo1.tsx",
    "content": "import { useDebounceEffect } from 'ahooks';\nimport { useState } from 'react';\n\nexport default () => {\n  const [value, setValue] = useState('hello');\n  const [records, setRecords] = useState<string[]>([]);\n  useDebounceEffect(\n    () => {\n      setRecords((val) => [...val, value]);\n    },\n    [value],\n    {\n      wait: 1000,\n    },\n  );\n  return (\n    <div>\n      <input\n        value={value}\n        onChange={(e) => setValue(e.target.value)}\n        placeholder=\"Typed value\"\n        style={{ width: 280 }}\n      />\n      <p style={{ marginTop: 16 }}>\n        <ul>\n          {records.map((record, index) => (\n            <li key={index}>{record}</li>\n          ))}\n        </ul>\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useDebounceEffect/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useDebounceEffect\n\nDebounce your `useEffect`.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseDebounceEffect(\n  effect: EffectCallback,\n  deps?: DependencyList,\n  options?: Options\n);\n```\n\n### Params\n\n| Property | Description                                                                   | Type             | Default |\n| -------- | ----------------------------------------------------------------------------- | ---------------- | ------- |\n| effect   | The effect callback.                                                          | `EffectCallback` | -       |\n| deps     | The dependencies list.                                                        | `DependencyList` | -       |\n| options  | Config for the debounce behaviors. See the Options section below for details. | `Options`        | -       |\n\n### Options\n\n| Property | Description                                                         | Type      | Default |\n| -------- | ------------------------------------------------------------------- | --------- | ------- |\n| wait     | The number of milliseconds to wait.                                 | `number`  | `1000`  |\n| leading  | Specify invoking on the leading edge of the timeout.                | `boolean` | `false` |\n| trailing | Specify invoking on the trailing edge of the timeout.               | `boolean` | `true`  |\n| maxWait  | The maximum time func is allowed to be delayed before it’s invoked. | `number`  | -       |\n"
  },
  {
    "path": "packages/hooks/src/useDebounceEffect/index.ts",
    "content": "import { useEffect, useState } from 'react';\nimport type { DependencyList, EffectCallback } from 'react';\nimport type { DebounceOptions } from '../useDebounce/debounceOptions';\nimport useDebounceFn from '../useDebounceFn';\nimport useUpdateEffect from '../useUpdateEffect';\n\nfunction useDebounceEffect(\n  effect: EffectCallback,\n  deps?: DependencyList,\n  options?: DebounceOptions,\n) {\n  const [flag, setFlag] = useState({});\n\n  const { run } = useDebounceFn(() => {\n    setFlag({});\n  }, options);\n\n  useEffect(() => {\n    return run();\n  }, deps);\n\n  useUpdateEffect(effect, [flag]);\n}\n\nexport default useDebounceEffect;\n"
  },
  {
    "path": "packages/hooks/src/useDebounceEffect/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useDebounceEffect\n\n为 `useEffect` 增加防抖的能力。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseDebounceEffect(\n  effect: EffectCallback,\n  deps?: DependencyList,\n  options?: Options\n);\n```\n\n### Params\n\n| 参数    | 说明                               | 类型             | 默认值 |\n| ------- | ---------------------------------- | ---------------- | ------ |\n| effect  | 执行函数                           | `EffectCallback` | -      |\n| deps    | 依赖数组                           | `DependencyList` | -      |\n| options | 配置防抖的行为，详见下面的 Options | `Options`        | -      |\n\n### Options\n\n| 参数     | 说明                     | 类型      | 默认值  |\n| -------- | ------------------------ | --------- | ------- |\n| wait     | 等待时间，单位为毫秒     | `number`  | `1000`  |\n| leading  | 是否在在延迟开始前调用   | `boolean` | `false` |\n| trailing | 是否在在延迟结束后调用   | `boolean` | `true`  |\n| maxWait  | 最大等待时间，单位为毫秒 | `number`  | -       |\n"
  },
  {
    "path": "packages/hooks/src/useDebounceFn/__tests__/index.spec.ts",
    "content": "import { act, type RenderHookResult, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport { sleep } from '../../utils/testingHelpers';\nimport useDebounceFn from '../index';\n\ninterface ParamsObj {\n  fn: (...arg: any) => any;\n  deps?: any[];\n  wait: number;\n}\n\nlet count = 0;\nconst debounceFn = (gap: number) => {\n  count += gap;\n};\n\nconst setUp = ({ fn, wait }: ParamsObj) => renderHook(() => useDebounceFn(fn, { wait }));\n\nlet hook: RenderHookResult<any, any>;\n\ndescribe('useDebounceFn', () => {\n  test('run, cancel and flush should work', async () => {\n    act(() => {\n      hook = setUp({\n        fn: debounceFn,\n        wait: 200,\n      });\n    });\n    await act(async () => {\n      hook.result.current.run(2);\n      hook.result.current.run(2);\n      hook.result.current.run(2);\n      hook.result.current.run(2);\n      expect(count).toBe(0);\n      await sleep(300);\n      expect(count).toBe(2);\n\n      hook.result.current.run(4);\n      expect(count).toBe(2);\n      await sleep(300);\n      expect(count).toBe(6);\n\n      hook.result.current.run(4);\n      expect(count).toBe(6);\n      hook.result.current.cancel();\n      expect(count).toBe(6);\n      await sleep(300);\n      expect(count).toBe(6);\n\n      hook.result.current.run(1);\n      expect(count).toBe(6);\n      hook.result.current.flush();\n      expect(count).toBe(7);\n      await sleep(300);\n      expect(count).toBe(7);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useDebounceFn/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Frequent calls run, but the function is executed only after all the clicks have completed 500ms.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 频繁调用 run，但只会在所有点击完成 500ms 后执行一次相关函数\n */\n\nimport { useDebounceFn } from 'ahooks';\nimport { useState } from 'react';\n\nexport default () => {\n  const [value, setValue] = useState(0);\n  const { run } = useDebounceFn(\n    () => {\n      setValue(value + 1);\n    },\n    {\n      wait: 500,\n    },\n  );\n\n  return (\n    <div>\n      <p style={{ marginTop: 16 }}> Clicked count: {value} </p>\n      <button type=\"button\" onClick={run}>\n        Click fast!\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useDebounceFn/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useDebounceFn\n\nA hook that deal with the debounced function.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst {\n  run,\n  cancel,\n  flush\n} = useDebounceFn(\n  fn: (...args: any[]) => any,\n  options?: Options\n);\n```\n\n### Params\n\n| Property | Description                        | Type                      | Default |\n| -------- | ---------------------------------- | ------------------------- | ------- |\n| fn       | The function to debounce.          | `(...args: any[]) => any` | -       |\n| options  | Config for the debounce behaviors. | `Options`                 | -       |\n\n### Options\n\n| Property | Description                                                         | Type      | Default |\n| -------- | ------------------------------------------------------------------- | --------- | ------- |\n| wait     | The number of milliseconds to delay.                                | `number`  | `1000`  |\n| leading  | Specify invoking on the leading edge of the timeout.                | `boolean` | `false` |\n| trailing | Specify invoking on the trailing edge of the timeout.               | `boolean` | `true`  |\n| maxWait  | The maximum time func is allowed to be delayed before it’s invoked. | `number`  | -       |\n\n### Result\n\n| Property | Description                                            | Type                      |\n| -------- | ------------------------------------------------------ | ------------------------- |\n| run      | invoke and pass parameters to fn.                      | `(...args: any[]) => any` |\n| cancel   | Cancel the invocation of currently debounced function. | `() => void`              |\n| flush    | Immediately invoke currently debounced function.       | `() => void`              |\n"
  },
  {
    "path": "packages/hooks/src/useDebounceFn/index.ts",
    "content": "import { debounce } from '../utils/lodash-polyfill';\nimport { useMemo } from 'react';\nimport type { DebounceOptions } from '../useDebounce/debounceOptions';\nimport useLatest from '../useLatest';\nimport useUnmount from '../useUnmount';\nimport { isFunction } from '../utils';\nimport isDev from '../utils/isDev';\n\ntype noop = (...args: any[]) => any;\n\nfunction useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {\n  if (isDev) {\n    if (!isFunction(fn)) {\n      console.error(`useDebounceFn expected parameter is a function, got ${typeof fn}`);\n    }\n  }\n\n  const fnRef = useLatest(fn);\n\n  const wait = options?.wait ?? 1000;\n\n  const debounced = useMemo(\n    () =>\n      debounce(\n        (...args: Parameters<T>): ReturnType<T> => {\n          return fnRef.current(...args);\n        },\n        wait,\n        options,\n      ),\n    [],\n  );\n\n  useUnmount(() => {\n    debounced.cancel();\n  });\n\n  return {\n    run: debounced,\n    cancel: debounced.cancel,\n    flush: debounced.flush,\n  };\n}\n\nexport default useDebounceFn;\n"
  },
  {
    "path": "packages/hooks/src/useDebounceFn/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useDebounceFn\n\n用来处理防抖函数的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst {\n  run,\n  cancel,\n  flush\n} = useDebounceFn(\n  fn: (...args: any[]) => any,\n  options?: Options\n);\n```\n\n### Params\n\n| 参数    | 说明               | 类型                      | 默认值 |\n| ------- | ------------------ | ------------------------- | ------ |\n| fn      | 需要防抖执行的函数 | `(...args: any[]) => any` | -      |\n| options | 配置防抖的行为     | `Options`                 | -      |\n\n### Options\n\n| 参数     | 说明                     | 类型      | 默认值  |\n| -------- | ------------------------ | --------- | ------- |\n| wait     | 等待时间，单位为毫秒     | `number`  | `1000`  |\n| leading  | 是否在延迟开始前调用函数 | `boolean` | `false` |\n| trailing | 是否在延迟开始后调用函数 | `boolean` | `true`  |\n| maxWait  | 最大等待时间，单位为毫秒 | `number`  | -       |\n\n### Result\n\n| 参数   | 说明                               | 类型                      |\n| ------ | ---------------------------------- | ------------------------- |\n| run    | 触发执行 fn，函数参数将会传递给 fn | `(...args: any[]) => any` |\n| cancel | 取消当前防抖                       | `() => void`              |\n| flush  | 立即调用当前防抖函数               | `() => void`              |\n"
  },
  {
    "path": "packages/hooks/src/useDeepCompareEffect/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { useState } from 'react';\nimport { describe, expect, test } from 'vitest';\nimport useDeepCompareEffect from '../index';\n\ndescribe('useDeepCompareEffect', () => {\n  test('test deep compare', async () => {\n    const hook = renderHook(() => {\n      const [x, setX] = useState(0);\n      const [y, setY] = useState({});\n      useDeepCompareEffect(() => {\n        setX((prevState) => prevState + 1);\n      }, [y]);\n      return { x, setY };\n    });\n    expect(hook.result.current.x).toBe(1);\n\n    await act(async () => {\n      hook.result.current.setY({});\n    });\n    expect(hook.result.current.x).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useDeepCompareEffect/demo/demo1.tsx",
    "content": "import { useDeepCompareEffect } from 'ahooks';\nimport { useEffect, useState, useRef } from 'react';\n\nexport default () => {\n  const [, setCount] = useState(0);\n  const effectCountRef = useRef(0);\n  const deepCompareCountRef = useRef(0);\n\n  useEffect(() => {\n    effectCountRef.current += 1;\n  }, [{}]);\n\n  useDeepCompareEffect(() => {\n    deepCompareCountRef.current += 1;\n    return () => {\n      // do something\n    };\n  }, [{}]);\n\n  return (\n    <div>\n      <p>effectCount: {effectCountRef.current}</p>\n      <p>deepCompareCount: {deepCompareCountRef.current}</p>\n      <p>\n        <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\n          reRender\n        </button>\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useDeepCompareEffect/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useDeepCompareEffect\n\nUsage is the same as `useEffect`, but deps are compared with [react-fast-compare](https://www.npmjs.com/package/react-fast-compare).\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseDeepCompareEffect(\n  effect: React.EffectCallback,\n  deps: React.DependencyList\n);\n```\n"
  },
  {
    "path": "packages/hooks/src/useDeepCompareEffect/index.tsx",
    "content": "import { useEffect } from 'react';\nimport { createDeepCompareEffect } from '../createDeepCompareEffect';\n\nexport default createDeepCompareEffect(useEffect);\n"
  },
  {
    "path": "packages/hooks/src/useDeepCompareEffect/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useDeepCompareEffect\n\n用法与 useEffect 一致，但 deps 通过 [react-fast-compare](https://www.npmjs.com/package/react-fast-compare) 进行深比较。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseDeepCompareEffect(\n  effect: React.EffectCallback,\n  deps: React.DependencyList\n);\n```\n"
  },
  {
    "path": "packages/hooks/src/useDeepCompareLayoutEffect/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { useState } from 'react';\nimport { describe, expect, test } from 'vitest';\nimport useDeepCompareLayoutEffect from '../index';\n\ndescribe('useDeepCompareLayoutEffect', () => {\n  test('test deep compare', async () => {\n    const hook = renderHook(() => {\n      const [x, setX] = useState(0);\n      const [y, setY] = useState({});\n      useDeepCompareLayoutEffect(() => {\n        setX((x) => x + 1);\n      }, [y]);\n      return { x, setY };\n    });\n    expect(hook.result.current.x).toBe(1);\n\n    await act(async () => {\n      hook.result.current.setY({});\n    });\n    expect(hook.result.current.x).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useDeepCompareLayoutEffect/demo/demo1.tsx",
    "content": "import { useDeepCompareLayoutEffect } from 'ahooks';\nimport { useLayoutEffect, useState, useRef } from 'react';\n\nexport default () => {\n  const [, setCount] = useState(0);\n  const effectCountRef = useRef(0);\n  const deepCompareCountRef = useRef(0);\n\n  useLayoutEffect(() => {\n    effectCountRef.current += 1;\n  }, [{}]);\n\n  useDeepCompareLayoutEffect(() => {\n    deepCompareCountRef.current += 1;\n    return () => {\n      // do something\n    };\n  }, [{}]);\n\n  return (\n    <div>\n      <p>effectCount: {effectCountRef.current}</p>\n      <p>deepCompareCount: {deepCompareCountRef.current}</p>\n      <p>\n        <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\n          reRender\n        </button>\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useDeepCompareLayoutEffect/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useDeepCompareLayoutEffect\n\nUsage is the same as `useLayoutEffect`, but deps are compared with [react-fast-compare](https://www.npmjs.com/package/react-fast-compare).\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseDeepCompareLayoutEffect(\n  effect: React.EffectCallback,\n  deps: React.DependencyList\n);\n```\n"
  },
  {
    "path": "packages/hooks/src/useDeepCompareLayoutEffect/index.tsx",
    "content": "import { useLayoutEffect } from 'react';\nimport { createDeepCompareEffect } from '../createDeepCompareEffect';\n\nexport default createDeepCompareEffect(useLayoutEffect);\n"
  },
  {
    "path": "packages/hooks/src/useDeepCompareLayoutEffect/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useDeepCompareLayoutEffect\n\n用法与 useLayoutEffect 一致，但 deps 通过 [react-fast-compare](https://www.npmjs.com/package/react-fast-compare) 进行深比较。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseDeepCompareLayoutEffect(\n  effect: React.EffectCallback,\n  deps: React.DependencyList\n);\n```\n"
  },
  {
    "path": "packages/hooks/src/useDocumentVisibility/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { afterAll, describe, expect, test, vi } from 'vitest';\nimport useDocumentVisibility from '../index';\n\nconst mockIsBrowser = vi.fn();\nconst mockDocumentVisibilityState = vi.spyOn(document, 'visibilityState', 'get');\n\nvi.mock('../../utils/isBrowser', () => {\n  return {\n    __esModule: true,\n    get default() {\n      return mockIsBrowser();\n    },\n  };\n});\n\nafterAll(() => {\n  vi.clearAllMocks();\n});\n\ndescribe('useDocumentVisibility', () => {\n  test('isBrowser effect correct', async () => {\n    mockDocumentVisibilityState.mockReturnValue('hidden');\n    // Object.defineProperty(document, 'visibilityState', { value: 'hidden', writable: true });\n    mockIsBrowser.mockReturnValue(false);\n    const { result } = renderHook(() => useDocumentVisibility());\n    expect(result.current).toBe('visible');\n  });\n\n  test('visibilitychange update correct ', async () => {\n    mockDocumentVisibilityState.mockReturnValue('hidden');\n    // Object.defineProperty(document, 'visibilityState', { value: 'hidden', writable: true });\n    mockIsBrowser.mockReturnValue(true);\n    const { result } = renderHook(() => useDocumentVisibility());\n    expect(result.current).toBe('hidden');\n    mockDocumentVisibilityState.mockReturnValue('visible');\n    // Object.defineProperty(document, 'visibilityState', { value: 'visible', writable: true });\n    act(() => {\n      document.dispatchEvent(new Event('visibilitychange'));\n    });\n    expect(result.current).toBe('visible');\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useDocumentVisibility/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Listen to document visibility change.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 监听 document 的可见状态\n */\n\nimport { useEffect } from 'react';\nimport { useDocumentVisibility } from 'ahooks';\n\nexport default () => {\n  const documentVisibility = useDocumentVisibility();\n\n  useEffect(() => {\n    console.log(`Current document visibility state: ${documentVisibility}`);\n  }, [documentVisibility]);\n\n  return <div>Current document visibility state: {documentVisibility}</div>;\n};\n"
  },
  {
    "path": "packages/hooks/src/useDocumentVisibility/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useDocumentVisibility\n\nA Hook can tell if the page is visible, refer to [visibilityState API](https://developer.mozilla.org/docs/Web/API/Document/visibilityState)\n\n## Examples\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst documentVisibility = useDocumentVisibility();\n```\n\n### Result\n\n| Property           | Description                     | Type                                               |\n| ------------------ | ------------------------------- | -------------------------------------------------- |\n| documentVisibility | Whether the document is visible | `visible`\\| `hidden` \\| `prerender` \\| `undefined` |\n"
  },
  {
    "path": "packages/hooks/src/useDocumentVisibility/index.ts",
    "content": "import { useState } from 'react';\nimport useEventListener from '../useEventListener';\nimport isBrowser from '../utils/isBrowser';\n\ntype VisibilityState = 'hidden' | 'visible' | 'prerender' | undefined;\n\nconst getVisibility = () => {\n  if (!isBrowser) {\n    return 'visible';\n  }\n  return document.visibilityState;\n};\n\nfunction useDocumentVisibility(): VisibilityState {\n  const [documentVisibility, setDocumentVisibility] = useState(getVisibility);\n\n  useEventListener(\n    'visibilitychange',\n    () => {\n      setDocumentVisibility(getVisibility());\n    },\n    {\n      target: () => document,\n    },\n  );\n\n  return documentVisibility;\n}\n\nexport default useDocumentVisibility;\n"
  },
  {
    "path": "packages/hooks/src/useDocumentVisibility/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useDocumentVisibility\n\n监听页面是否可见，参考 [visibilityState API](https://developer.mozilla.org/docs/Web/API/Document/visibilityState)\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst documentVisibility = useDocumentVisibility();\n```\n\n### Result\n\n| 参数               | 说明                           | 类型                                               |\n| ------------------ | ------------------------------ | -------------------------------------------------- |\n| documentVisibility | 判断 document 是否处于可见状态 | `visible`\\| `hidden` \\| `prerender` \\| `undefined` |\n"
  },
  {
    "path": "packages/hooks/src/useDrag/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { beforeEach, describe, expect, test, vi } from 'vitest';\nimport type { BasicTarget } from '../../utils/domTarget';\nimport type { Options } from '../index';\nimport useDrag from '../index';\n\nconst setup = <T>(data: T, target: BasicTarget, options?: Options) =>\n  renderHook((newData: T) => useDrag(newData ? newData : data, target, options));\n\nconst events: Record<string, (event: any) => void> = {};\nconst mockTarget = {\n  addEventListener: vi.fn((event, callback) => {\n    events[event] = callback;\n  }),\n  removeEventListener: vi.fn((event) => {\n    Reflect.deleteProperty(events, event);\n  }),\n  setAttribute: vi.fn(),\n};\n\ndescribe('useDrag', () => {\n  beforeEach(() => {\n    vi.clearAllMocks();\n  });\n  test('should add/remove listener on mount/unmount', () => {\n    const { unmount } = setup(1, mockTarget as any);\n    expect(mockTarget.addEventListener).toBeCalled();\n    expect(mockTarget.addEventListener.mock.calls[0][0]).toBe('dragstart');\n    expect(mockTarget.addEventListener.mock.calls[1][0]).toBe('dragend');\n    expect(mockTarget.setAttribute).toBeCalledWith('draggable', 'true');\n    unmount();\n    expect(mockTarget.removeEventListener).toBeCalled();\n  });\n\n  test('should trigger drag callback', () => {\n    const onDragStart = vi.fn();\n    const onDragEnd = vi.fn();\n    const mockEvent = {\n      dataTransfer: {\n        setData: vi.fn(),\n      },\n    };\n    const hook = setup(1, mockTarget as any, {\n      onDragStart,\n      onDragEnd,\n    });\n    events.dragstart(mockEvent);\n    expect(onDragStart).toBeCalled();\n    expect(mockEvent.dataTransfer.setData).toBeCalledWith('custom', '1');\n    events.dragend(mockEvent);\n    expect(onDragEnd).toBeCalled();\n\n    hook.rerender(2);\n\n    events.dragstart(mockEvent);\n    expect(onDragStart).toBeCalled();\n    expect(mockEvent.dataTransfer.setData).toHaveBeenLastCalledWith('custom', '2');\n    events.dragend(mockEvent);\n    expect(onDragEnd).toBeCalled();\n  });\n\n  test(`should not work when target don't support addEventListener method`, () => {\n    Object.defineProperty(mockTarget, 'addEventListener', {\n      get() {\n        return false;\n      },\n    });\n    setup(1, mockTarget as any);\n    expect(mockTarget.setAttribute).not.toBeCalled();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useDrag/index.ts",
    "content": "import { useRef } from 'react';\nimport useLatest from '../useLatest';\nimport useMount from '../useMount';\nimport { isString } from '../utils';\nimport type { BasicTarget } from '../utils/domTarget';\nimport { getTargetElement } from '../utils/domTarget';\nimport useEffectWithTarget from '../utils/useEffectWithTarget';\n\nexport interface Options {\n  onDragStart?: (event: React.DragEvent) => void;\n  onDragEnd?: (event: React.DragEvent) => void;\n  dragImage?: {\n    image: string | Element;\n    offsetX?: number;\n    offsetY?: number;\n  };\n}\n\nconst useDrag = <T>(data: T, target: BasicTarget, options: Options = {}) => {\n  const optionsRef = useLatest(options);\n  const dataRef = useLatest(data);\n  const imageElementRef = useRef<Element>(undefined);\n\n  const { dragImage } = optionsRef.current;\n\n  useMount(() => {\n    if (dragImage?.image) {\n      const { image } = dragImage;\n\n      if (isString(image)) {\n        const imageElement = new Image();\n\n        imageElement.src = image;\n        imageElementRef.current = imageElement;\n      } else {\n        imageElementRef.current = image;\n      }\n    }\n  });\n\n  useEffectWithTarget(\n    () => {\n      const targetElement = getTargetElement(target);\n      if (!targetElement?.addEventListener) {\n        return;\n      }\n\n      const onDragStart = (event: React.DragEvent) => {\n        optionsRef.current.onDragStart?.(event);\n        event.dataTransfer.setData('custom', JSON.stringify(dataRef.current));\n\n        if (dragImage?.image && imageElementRef.current) {\n          const { offsetX = 0, offsetY = 0 } = dragImage;\n\n          event.dataTransfer.setDragImage(imageElementRef.current, offsetX, offsetY);\n        }\n      };\n\n      const onDragEnd = (event: React.DragEvent) => {\n        optionsRef.current.onDragEnd?.(event);\n      };\n\n      targetElement.setAttribute('draggable', 'true');\n\n      targetElement.addEventListener('dragstart', onDragStart as any);\n      targetElement.addEventListener('dragend', onDragEnd as any);\n\n      return () => {\n        targetElement.removeEventListener('dragstart', onDragStart as any);\n        targetElement.removeEventListener('dragend', onDragEnd as any);\n      };\n    },\n    [],\n    target,\n  );\n};\n\nexport default useDrag;\n"
  },
  {
    "path": "packages/hooks/src/useDrop/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\nimport type { BasicTarget } from '../../utils/domTarget';\nimport type { Options } from '../index';\nimport useDrop from '../index';\n\nconst setup = (target: unknown, options?: Options) =>\n  renderHook(() => useDrop(target as BasicTarget, options));\n\nconst events: Record<string, (event?: any) => void> = {};\nconst mockTarget = {\n  addEventListener: vi.fn((event: string, callback: (event?: any) => void) => {\n    events[event] = callback;\n  }),\n  removeEventListener: vi.fn((event) => {\n    Reflect.deleteProperty(events, event);\n  }),\n};\n\nconst mockEvent = {\n  dataTransfer: {\n    getData: (format?: string) => 'mock' as unknown,\n    get items() {\n      return [] as unknown[];\n    },\n    get files() {\n      return [] as unknown[];\n    },\n  },\n  clipboardData: {\n    getData: (format?: string) => 'mock' as unknown,\n    get items() {\n      return [] as unknown[];\n    },\n    get files() {\n      return [] as unknown[];\n    },\n  },\n  preventDefault: vi.fn(),\n  stopPropagation: vi.fn(),\n};\n\ndescribe('useDrop', () => {\n  test(`should not work when target don't support addEventListener method`, () => {\n    const originAddEventListener = mockTarget.addEventListener;\n    Object.defineProperty(mockTarget, 'addEventListener', {\n      value: false,\n    });\n    setup(mockTarget);\n    expect(Object.keys(events)).toHaveLength(0);\n    Object.defineProperty(mockTarget, 'addEventListener', {\n      value: originAddEventListener,\n    });\n  });\n\n  test('should add/remove listener on mount/unmount', () => {\n    const { unmount } = setup(mockTarget);\n    const eventNames = ['dragenter', 'dragover', 'dragleave', 'drop', 'paste'];\n    expect(mockTarget.addEventListener).toBeCalledTimes(eventNames.length);\n    eventNames.forEach((eventName, i) => {\n      expect(mockTarget.addEventListener.mock.calls[i][0]).toBe(eventName);\n    });\n    unmount();\n    expect(mockTarget.removeEventListener).toBeCalledTimes(eventNames.length);\n    eventNames.forEach((eventName, i) => {\n      expect(mockTarget.addEventListener.mock.calls[i][0]).toBe(eventName);\n    });\n  });\n\n  test('should call callback', () => {\n    const onDragEnter = vi.fn();\n    const onDragOver = vi.fn();\n    const onDragLeave = vi.fn();\n    const onDrop = vi.fn();\n    const onPaste = vi.fn();\n\n    setup(mockTarget, {\n      onDragEnter,\n      onDragOver,\n      onDragLeave,\n      onDrop,\n      onPaste,\n    });\n    const callbacks = [onDragEnter, onDragOver, onDragLeave, onDrop, onPaste];\n    const eventNames = ['dragenter', 'dragover', 'dragleave', 'drop', 'paste'];\n    eventNames.forEach((event) => {\n      events[event](mockEvent);\n    });\n    callbacks.forEach((callback) => expect(callback).toBeCalled());\n  });\n\n  test('should call onText on drop', async () => {\n    vi.spyOn(mockEvent.dataTransfer, 'items', 'get').mockReturnValue([\n      {\n        getAsString: (callback: (text: string) => void) => {\n          callback('drop text');\n        },\n      },\n    ]);\n\n    const onText = vi.fn();\n    setup(mockTarget, {\n      onText,\n    });\n    events['dragenter'](mockEvent);\n    events['drop'](mockEvent);\n    expect(onText.mock.calls[0][0]).toBe('drop text');\n  });\n\n  test('should call onFiles on drop', async () => {\n    const file = new File(['hello'], 'hello.png');\n    vi.spyOn(mockEvent.dataTransfer, 'files', 'get').mockReturnValue([file]);\n    const onFiles = vi.fn();\n    setup(mockTarget, {\n      onFiles,\n    });\n    events['dragenter'](mockEvent);\n    events['drop'](mockEvent);\n    expect(onFiles.mock.calls[0][0]).toHaveLength(1);\n  });\n\n  test('should call onUri on drop', async () => {\n    const url = 'https://alipay.com';\n    vi.spyOn(mockEvent.dataTransfer, 'getData').mockImplementation((format?: string) => {\n      if (format === 'text/uri-list') return url;\n      return undefined;\n    });\n\n    const onUri = vi.fn();\n    setup(mockTarget, {\n      onUri,\n    });\n    events['dragenter'](mockEvent);\n    events['drop'](mockEvent);\n    expect(onUri.mock.calls[0][0]).toBe(url);\n  });\n\n  test('should call onDom on drop', async () => {\n    const data = {\n      value: 'mock',\n    };\n    vi.spyOn(mockEvent.dataTransfer, 'getData').mockImplementation((format?: string) => {\n      if (format === 'custom') return data;\n      return undefined;\n    });\n\n    const onDom = vi.fn();\n    setup(mockTarget, {\n      onDom,\n    });\n    events['dragenter'](mockEvent);\n    events['drop'](mockEvent);\n    expect(onDom.mock.calls[0][0]).toMatchObject(data);\n\n    // catch JSON.parse error\n    vi.spyOn(mockEvent.dataTransfer, 'getData').mockImplementation((format?: string) => {\n      if (format === 'custom') return {};\n      return undefined;\n    });\n    events['dragenter'](mockEvent);\n    events['drop'](mockEvent);\n    expect(onDom.mock.calls[0][0]).toMatchObject({});\n  });\n\n  test('should call onText on paste', async () => {\n    vi.spyOn(mockEvent.clipboardData, 'items', 'get').mockReturnValue([\n      {\n        getAsString: (callback: (text: string) => void) => {\n          callback('paste text');\n        },\n      },\n    ]);\n\n    const onText = vi.fn();\n    setup(mockTarget, {\n      onText,\n    });\n    events['dragenter'](mockEvent);\n    events['paste'](mockEvent);\n    expect(onText.mock.calls[0][0]).toBe('paste text');\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useDrop/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: The drop area can accept files, uri, text or one of the boxes below.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 拖拽区域可以接受文件，链接，文字，和下方的 box 节点。\n */\n\nimport { useRef, useState } from 'react';\nimport { useDrop, useDrag } from 'ahooks';\n\nconst DragItem = ({ data }: { data: string }) => {\n  const dragRef = useRef(null);\n\n  const [dragging, setDragging] = useState(false);\n\n  useDrag(data, dragRef, {\n    onDragStart: () => {\n      setDragging(true);\n    },\n    onDragEnd: () => {\n      setDragging(false);\n    },\n  });\n\n  return (\n    <div\n      ref={dragRef}\n      style={{\n        border: '1px solid #e8e8e8',\n        padding: 16,\n        width: 80,\n        textAlign: 'center',\n        marginRight: 16,\n      }}\n    >\n      {dragging ? 'dragging' : `box-${data}`}\n    </div>\n  );\n};\n\nexport default () => {\n  const [isHovering, setIsHovering] = useState(false);\n\n  const dropRef = useRef(null);\n\n  useDrop(dropRef, {\n    onText: (text, e) => {\n      console.log(e);\n      alert(`'text: ${text}' dropped`);\n    },\n    onFiles: (files, e) => {\n      console.log(e, files);\n      alert(`${files.length} file dropped`);\n    },\n    onUri: (uri, e) => {\n      console.log(e);\n      alert(`uri: ${uri} dropped`);\n    },\n    onDom: (content: string, e) => {\n      alert(`custom: ${content} dropped`);\n    },\n    onDragEnter: () => setIsHovering(true),\n    onDragLeave: () => setIsHovering(false),\n  });\n\n  return (\n    <div>\n      <div ref={dropRef} style={{ border: '1px dashed #e8e8e8', padding: 16, textAlign: 'center' }}>\n        {isHovering ? 'release here' : 'drop here'}\n      </div>\n\n      <div style={{ display: 'flex', marginTop: 8, overflow: 'auto' }}>\n        {['1', '2', '3', '4', '5'].map((e) => (\n          <DragItem key={e} data={e} />\n        ))}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useDrop/demo/demo2.tsx",
    "content": "/**\n * title: Customize Image\n * desc: Customize image that follow the mouse pointer during dragging.\n *\n * title.zh-CN: 自定义拖拽图像\n * desc.zh-CN: 自定义拖拽过程中跟随鼠标指针的图像。\n */\n\nimport { useRef } from 'react';\nimport { useDrag } from 'ahooks';\n\nconst COMMON_STYLE: React.CSSProperties = {\n  border: '1px solid #e8e8e8',\n  height: '50px',\n  lineHeight: '50px',\n  padding: '16px',\n  textAlign: 'center',\n  marginRight: '16px',\n};\n\nexport default () => {\n  const dragRef = useRef(null);\n\n  useDrag('', dragRef, {\n    dragImage: {\n      image: '/logo.svg',\n    },\n  });\n\n  return (\n    <div ref={dragRef} style={{ display: 'flex' }}>\n      <img style={COMMON_STYLE} src=\"/simple-logo.svg\" />\n      <div style={COMMON_STYLE}>drag me</div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useDrop/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useDrop & useDrag\n\nA pair of hooks to help you manage data transfer between drag and drop\n\n> useDrop can be used alone to accept file, text or uri dropping.\n>\n> useDrag should be used along with useDrop.\n>\n> Paste into the drop area will also be treated as content drop.\n\n## Examples\n\n### Basic Usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Customize Image\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n### useDrag\n\n```typescript\nuseDrag<T>(\n  data: any,\n  target: (() => Element) | Element | MutableRefObject<Element>,\n  options?: DragOptions\n);\n```\n\n#### Params\n\n| Property | Description        | Type                                                        | Default |\n| -------- | ------------------ | ----------------------------------------------------------- | ------- |\n| data     | Drag data          | `any`                                                       | -       |\n| target   | DOM element or ref | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -       |\n| options  | More config        | `DragOptions`                                               | -       |\n\n#### DragOptions\n\n| Property    | Description                                                   | Type                           | Default |\n| ----------- | ------------------------------------------------------------- | ------------------------------ | ------- |\n| onDragStart | On drag start callback                                        | `(e: React.DragEvent) => void` | -       |\n| onDragEnd   | On drag end callback                                          | `(e: React.DragEvent) => void` | -       |\n| dragImage   | Customize image that follow the mouse pointer during dragging | `DragImageOptions`             | -       |\n\n#### DragImageOptions\n\n| 参数    | 说明                                                                                                                                                                                                                                                                                                          | 类型                | 默认值 |\n| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ------ |\n| image   | An image Element element to use for the drag feedback image. The image will typically be an [`<img>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img) element but it can also be a [`<canvas>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas) or any other visible element | `string \\| Element` | -      |\n| offsetX | the horizontal offset within the image                                                                                                                                                                                                                                                                        | `number`            | 0      |\n| offsetY | the vertical offset within the image                                                                                                                                                                                                                                                                          | `number`            | 0      |\n\n### useDrop\n\n```typescript\nuseDrop<T>(\n  target: (() => Element) | Element | MutableRefObject<Element>,\n  options?: DropOptions\n);\n```\n\n#### Params\n\n| Property | Description        | Type                                                        | Default |\n| -------- | ------------------ | ----------------------------------------------------------- | ------- |\n| target   | DOM element or ref | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -       |\n| options  | More config        | `DropOptions`                                               | -       |\n\n#### DropOptions\n\n| Property    | Description                                 | Type                                          | Default |\n| ----------- | ------------------------------------------- | --------------------------------------------- | ------- |\n| onText      | The callback when text is dropped or pasted | `(text: string, e: React.DragEvent) => void`  | -       |\n| onFiles     | The callback when file is dropped or pasted | `(files: File[], e: React.DragEvent) => void` | -       |\n| onUri       | The callback when uri is dropped or pasted  | `(text: string, e: React.DragEvent) => void`  | -       |\n| onDom       | The callback when DOM is dropped or pasted  | `(content: any, e: React.DragEvent) => void`  | -       |\n| onDrop      | The callback when any is dropped            | `(e: React.DragEvent) => void`                | -       |\n| onPaste     | The callback when any is pasted             | `(e: React.DragEvent) => void`                | -       |\n| onDragEnter | On drag enter callback                      | `(e: React.DragEvent) => void`                | -       |\n| onDragOver  | On drag over callback                       | `(e: React.DragEvent) => void`                | -       |\n| onDragLeave | On drag leave callback                      | `(e: React.DragEvent) => void`                | -       |\n"
  },
  {
    "path": "packages/hooks/src/useDrop/index.ts",
    "content": "import useLatest from '../useLatest';\nimport type { BasicTarget } from '../utils/domTarget';\nimport { getTargetElement } from '../utils/domTarget';\nimport useEffectWithTarget from '../utils/useEffectWithTarget';\nimport { useRef } from 'react';\n\nexport interface Options {\n  onFiles?: (files: File[], event?: React.DragEvent) => void;\n  onUri?: (url: string, event?: React.DragEvent) => void;\n  onDom?: (content: any, event?: React.DragEvent) => void;\n  onText?: (text: string, event?: React.ClipboardEvent) => void;\n  onDragEnter?: (event?: React.DragEvent) => void;\n  onDragOver?: (event?: React.DragEvent) => void;\n  onDragLeave?: (event?: React.DragEvent) => void;\n  onDrop?: (event?: React.DragEvent) => void;\n  onPaste?: (event?: React.ClipboardEvent) => void;\n}\n\nconst useDrop = (target: BasicTarget, options: Options = {}) => {\n  const optionsRef = useLatest(options);\n\n  // https://stackoverflow.com/a/26459269\n  const dragEnterTarget = useRef<EventTarget>(undefined);\n\n  useEffectWithTarget(\n    () => {\n      const targetElement = getTargetElement(target);\n      if (!targetElement?.addEventListener) {\n        return;\n      }\n\n      const onData = (\n        dataTransfer: DataTransfer,\n        event: React.DragEvent | React.ClipboardEvent,\n      ) => {\n        const uri = dataTransfer.getData('text/uri-list');\n        const dom = dataTransfer.getData('custom');\n\n        if (dom && optionsRef.current.onDom) {\n          let data = dom;\n          try {\n            data = JSON.parse(dom);\n          } catch {\n            data = dom;\n          }\n          optionsRef.current.onDom(data, event as React.DragEvent);\n          return;\n        }\n\n        if (uri && optionsRef.current.onUri) {\n          optionsRef.current.onUri(uri, event as React.DragEvent);\n          return;\n        }\n\n        if (dataTransfer.files && dataTransfer.files.length && optionsRef.current.onFiles) {\n          optionsRef.current.onFiles(Array.from(dataTransfer.files), event as React.DragEvent);\n          return;\n        }\n\n        if (dataTransfer.items && dataTransfer.items.length && optionsRef.current.onText) {\n          dataTransfer.items[0].getAsString((text) => {\n            optionsRef.current.onText!(text, event as React.ClipboardEvent);\n          });\n        }\n      };\n\n      const onDragEnter = (event: React.DragEvent) => {\n        event.preventDefault();\n        event.stopPropagation();\n\n        dragEnterTarget.current = event.target;\n        optionsRef.current.onDragEnter?.(event);\n      };\n\n      const onDragOver = (event: React.DragEvent) => {\n        event.preventDefault();\n        optionsRef.current.onDragOver?.(event);\n      };\n\n      const onDragLeave = (event: React.DragEvent) => {\n        if (event.target === dragEnterTarget.current) {\n          optionsRef.current.onDragLeave?.(event);\n        }\n      };\n\n      const onDrop = (event: React.DragEvent) => {\n        event.preventDefault();\n        onData(event.dataTransfer, event);\n        optionsRef.current.onDrop?.(event);\n      };\n\n      const onPaste = (event: React.ClipboardEvent) => {\n        onData(event.clipboardData, event);\n        optionsRef.current.onPaste?.(event);\n      };\n\n      targetElement.addEventListener('dragenter', onDragEnter as any);\n      targetElement.addEventListener('dragover', onDragOver as any);\n      targetElement.addEventListener('dragleave', onDragLeave as any);\n      targetElement.addEventListener('drop', onDrop as any);\n      targetElement.addEventListener('paste', onPaste as any);\n\n      return () => {\n        targetElement.removeEventListener('dragenter', onDragEnter as any);\n        targetElement.removeEventListener('dragover', onDragOver as any);\n        targetElement.removeEventListener('dragleave', onDragLeave as any);\n        targetElement.removeEventListener('drop', onDrop as any);\n        targetElement.removeEventListener('paste', onPaste as any);\n      };\n    },\n    [],\n    target,\n  );\n};\n\nexport default useDrop;\n"
  },
  {
    "path": "packages/hooks/src/useDrop/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useDrop & useDrag\n\n处理元素拖拽的 Hook。\n\n> useDrop 可以单独使用来接收文件、文字和网址的拖拽。\n>\n> useDrag 允许一个 DOM 节点被拖拽，需要配合 useDrop 使用。\n>\n> 向节点内触发粘贴动作也会被视为拖拽。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 自定义拖拽图像\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n### useDrag\n\n```typescript\nuseDrag<T>(\n  data: any,\n  target: (() => Element) | Element | MutableRefObject<Element>,\n  options?: DragOptions\n);\n```\n\n#### Params\n\n| 参数    | 说明                  | 类型                                                        | 默认值 |\n| ------- | --------------------- | ----------------------------------------------------------- | ------ |\n| data    | 拖拽的内容            | `any`                                                       | -      |\n| target  | DOM 节点或者 Ref 对象 | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -      |\n| options | 额外的配置项          | `DragOptions`                                               | -      |\n\n#### DragOptions\n\n| 参数        | 说明                               | 类型                           | 默认值 |\n| ----------- | ---------------------------------- | ------------------------------ | ------ |\n| onDragStart | 开始拖拽的回调                     | `(e: React.DragEvent) => void` | -      |\n| onDragEnd   | 结束拖拽的回调                     | `(e: React.DragEvent) => void` | -      |\n| dragImage   | 自定义拖拽过程中跟随鼠标指针的图像 | `DragImageOptions`             | -      |\n\n#### DragImageOptions\n\n| 参数    | 说明                                                                                                                                                                                                                                       | 类型                | 默认值 |\n| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- | ------ |\n| image   | 拖拽过程中跟随鼠标指针的图像。图像通常是一个 [`<img>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img) 元素，但也可以是 [`<canvas>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas) 或任何其他图像元素。 | `string \\| Element` | -      |\n| offsetX | 水平偏移                                                                                                                                                                                                                                   | `number`            | 0      |\n| offsetY | 垂直偏移                                                                                                                                                                                                                                   | `number`            | 0      |\n\n### useDrop\n\n```typescript\nuseDrop<T>(\n  target: (() => Element) | Element | MutableRefObject<Element>,\n  options?: DropOptions\n);\n```\n\n#### Params\n\n| 参数    | 说明                  | 类型                                                        | 默认值 |\n| ------- | --------------------- | ----------------------------------------------------------- | ------ |\n| target  | DOM 节点或者 Ref 对象 | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -      |\n| options | 额外的配置项          | `DragOptions`                                               | -      |\n\n#### DropOptions\n\n| 参数        | 说明                           | 类型                                          | 默认值 |\n| ----------- | ------------------------------ | --------------------------------------------- | ------ |\n| onText      | 拖拽/粘贴文字的回调            | `(text: string, e: React.DragEvent) => void`  | -      |\n| onFiles     | 拖拽/粘贴文件的回调            | `(files: File[], e: React.DragEvent) => void` | -      |\n| onUri       | 拖拽/粘贴链接的回调            | `(text: string, e: React.DragEvent) => void`  | -      |\n| onDom       | 拖拽/粘贴自定义 DOM 节点的回调 | `(content: any, e: React.DragEvent) => void`  | -      |\n| onDrop      | 拖拽任意内容的回调             | `(e: React.DragEvent) => void`                | -      |\n| onPaste     | 粘贴内容的回调                 | `(e: React.DragEvent) => void`                | -      |\n| onDragEnter | 拖拽进入                       | `(e: React.DragEvent) => void`                | -      |\n| onDragOver  | 拖拽中                         | `(e: React.DragEvent) => void`                | -      |\n| onDragLeave | 拖拽出去                       | `(e: React.DragEvent) => void`                | -      |\n"
  },
  {
    "path": "packages/hooks/src/useDynamicList/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { afterAll, afterEach, describe, expect, test, vi } from 'vitest';\nimport useDynamicList from '../index';\n\ndescribe('useDynamicList', () => {\n  const setUp = (props: any): any => renderHook(() => useDynamicList(props));\n  const warnSpy = vi.spyOn(console, 'error').mockImplementation(() => {});\n\n  afterEach(() => {\n    warnSpy.mockReset();\n  });\n\n  afterAll(() => {\n    warnSpy.mockRestore();\n  });\n\n  test('getKey should work', () => {\n    const hook = setUp([1, 2, 3]);\n    expect(hook.result.current.list[0]).toBe(1);\n    expect(hook.result.current.getKey(0)).toBe(0);\n    expect(hook.result.current.getKey(1)).toBe(1);\n    expect(hook.result.current.getKey(2)).toBe(2);\n  });\n\n  test('methods should work', () => {\n    const hook = setUp([\n      { name: 'aaa', age: 18 },\n      { name: 'bbb', age: 19 },\n      { name: 'ccc', age: 20 },\n    ]);\n\n    expect(hook.result.current.list[0].age).toBe(18);\n    expect(hook.result.current.list[1].age).toBe(19);\n    expect(hook.result.current.list[2].age).toBe(20);\n\n    expect(hook.result.current.getKey(0)).toBe(0);\n    expect(hook.result.current.getKey(1)).toBe(1);\n    expect(hook.result.current.getKey(2)).toBe(2);\n\n    // unshift\n    act(() => {\n      hook.result.current.unshift({ name: 'ddd', age: 21 });\n    });\n\n    expect(hook.result.current.list[0].name).toBe('ddd');\n    expect(hook.result.current.getKey(0)).toBe(3);\n\n    // push\n    act(() => {\n      hook.result.current.push({ name: 'ddd', age: 21 });\n    });\n\n    expect(hook.result.current.list[4].name).toBe('ddd');\n    expect(hook.result.current.getKey(0)).toBe(3);\n    expect(hook.result.current.getKey(4)).toBe(4);\n\n    // insert\n    act(() => {\n      hook.result.current.insert(1, { name: 'eee', age: 22 });\n    });\n    expect(hook.result.current.list[1].name).toBe('eee');\n    expect(hook.result.current.getKey(1)).toBe(5);\n\n    // merge\n    act(() => {\n      hook.result.current.merge(0, [1, 2, 3, 4]);\n    });\n    expect(hook.result.current.list[0]).toBe(1);\n    expect(hook.result.current.getKey(0)).toBe(6);\n\n    // move\n    act(() => {\n      hook.result.current.move(0, 1);\n    });\n    expect(hook.result.current.list[0]).toBe(2);\n    expect(hook.result.current.getKey(0)).toBe(7);\n\n    // move without changes\n    act(() => {\n      hook.result.current.move(2, 2);\n    });\n    expect(hook.result.current.list[0]).toBe(2);\n    expect(hook.result.current.getKey(0)).toBe(7);\n\n    // shift\n    act(() => {\n      hook.result.current.shift();\n    });\n    expect(hook.result.current.list[0]).toBe(1);\n    expect(hook.result.current.getKey(0)).toBe(6);\n    expect(hook.result.current.list.length).toBe(9);\n\n    // pop\n    act(() => {\n      hook.result.current.pop();\n    });\n    expect(hook.result.current.list.length).toBe(8);\n\n    // replace\n    act(() => {\n      hook.result.current.replace(7, { value: 8 });\n    });\n    expect(hook.result.current.list[7].value).toBe(8);\n\n    // remove\n    act(() => {\n      hook.result.current.remove(7);\n    });\n    expect(hook.result.current.list.length).toBe(7);\n\n    // batch remove\n    act(() => {\n      hook.result.current.batchRemove(1);\n    });\n    expect(warnSpy).toHaveBeenCalledWith(\n      '`indexes` parameter of `batchRemove` function expected to be an array, but got \"number\".',\n    );\n    act(() => {\n      hook.result.current.batchRemove([0, 1, 2]);\n    });\n    expect(hook.result.current.list.length).toBe(4);\n  });\n\n  test('same items should have different keys', () => {\n    const hook = setUp([1, 1, 1, 1]);\n    expect(hook.result.current.getKey(0)).toBe(0);\n    expect(hook.result.current.getKey(1)).toBe(1);\n    expect(hook.result.current.getKey(2)).toBe(2);\n    expect(hook.result.current.getKey(3)).toBe(3);\n\n    act(() => {\n      hook.result.current.push(1);\n    });\n\n    expect(hook.result.current.getKey(4)).toBe(4);\n    const testObj = {};\n\n    act(() => {\n      hook.result.current.push({});\n      hook.result.current.push(testObj);\n      hook.result.current.push(testObj);\n    });\n\n    expect(hook.result.current.getKey(5)).toBe(5);\n    expect(hook.result.current.getKey(6)).toBe(6);\n    expect(hook.result.current.getKey(7)).toBe(7);\n  });\n\n  test('initialValue changes', () => {\n    const hook = renderHook(({ initialValue }) => useDynamicList(initialValue), {\n      initialProps: {\n        initialValue: [1],\n      },\n    });\n    expect(hook.result.current.list[0]).toBe(1);\n    expect(hook.result.current.getKey(0)).toBe(0);\n\n    act(() => {\n      hook.result.current.resetList([2]);\n    });\n\n    expect(hook.result.current.list[0]).toBe(2);\n    expect(hook.result.current.getKey(0)).toBe(1);\n\n    act(() => {\n      hook.result.current.resetList([3]);\n    });\n\n    expect(hook.result.current.list[0]).toBe(3);\n    expect(hook.result.current.getKey(0)).toBe(2);\n  });\n\n  test('sortList', () => {\n    const hook = setUp([1, 2, 3, 4]);\n    const formData = [\n      {\n        name: 'my bro',\n        age: '23',\n        memo: \"he's my bro\",\n      },\n      {\n        name: 'my sis',\n        age: '21',\n        memo: \"she's my sis\",\n      },\n      null,\n      {\n        name: '新增行',\n        age: '25',\n      },\n    ];\n\n    let sorted = hook.result.current.sortList(formData);\n    expect(sorted.length).toBe(3);\n    expect(sorted[0].name).toBe('my bro');\n\n    act(() => {\n      hook.result.current.move(3, 0);\n    });\n    sorted = hook.result.current.sortList(formData);\n    expect(sorted[0].name).toBe('新增行');\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useDynamicList/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Dynamic list management\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 管理动态列表\n */\n\nimport { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';\nimport { useDynamicList } from 'ahooks';\nimport { Button, Input, Space } from 'antd';\n\nexport default () => {\n  const { list, remove, batchRemove, getKey, insert, replace } = useDynamicList(['David', 'Jack']);\n  const listIndexes = list.map((item, index) => index);\n\n  const Row = (index: number, item: any) => (\n    <div key={getKey(index)} style={{ marginBottom: 16 }}>\n      <Input\n        style={{ width: 300 }}\n        placeholder=\"Please enter name\"\n        onChange={(e) => replace(index, e.target.value)}\n        value={item}\n      />\n\n      {list.length > 1 && (\n        <MinusCircleOutlined\n          style={{ marginLeft: 8 }}\n          onClick={() => {\n            remove(index);\n          }}\n        />\n      )}\n      <PlusCircleOutlined\n        style={{ marginLeft: 8 }}\n        onClick={() => {\n          insert(index + 1, '');\n        }}\n      />\n    </div>\n  );\n\n  return (\n    <>\n      {list.map((ele, index) => Row(index, ele))}\n\n      <Space style={{ marginBottom: 16 }}>\n        <Button\n          danger\n          disabled={list.length <= 1}\n          onClick={() => batchRemove(listIndexes.filter((index) => index % 2 === 0))}\n        >\n          Remove odd items\n        </Button>\n        <Button\n          danger\n          disabled={list.length <= 1}\n          onClick={() => batchRemove(listIndexes.filter((index) => index % 2 !== 0))}\n        >\n          Remove even items\n        </Button>\n      </Space>\n\n      <div>{JSON.stringify([list])}</div>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useDynamicList/demo/demo2.tsx",
    "content": "/**\n * title: Used in antd Form\n * desc: Used in antd Form, a component can be packaged independently, like DynamicInputs in the example.\n *\n * title.zh-CN: 在 antd Form 中使用\n * desc.zh-CN: 在 antd Form 中使用，可以独立封装一个组件，比如例子中的 DynamicInputs。\n */\n\nimport { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';\nimport { useDynamicList } from 'ahooks';\nimport { Button, Form, Input } from 'antd';\nimport { useEffect, useState } from 'react';\n\nconst DynamicInputs = ({\n  value = [],\n  onChange,\n}: {\n  value?: string[];\n  onChange?: (value: string[]) => void;\n}) => {\n  const { list, remove, getKey, insert, replace, resetList } = useDynamicList(value);\n\n  useEffect(() => {\n    // If value change manual, reset list\n    if (value !== list) {\n      resetList(value);\n    }\n  }, [value]);\n\n  useEffect(() => {\n    onChange?.(list);\n  }, [list]);\n\n  const Row = (index: number, item: any) => (\n    <div key={getKey(index)} style={{ marginBottom: 16 }}>\n      <Input\n        style={{ width: 300 }}\n        placeholder=\"Please enter name\"\n        onChange={(e) => replace(index, e.target.value)}\n        value={item}\n      />\n\n      {list.length > 1 && (\n        <MinusCircleOutlined\n          style={{ marginLeft: 8 }}\n          onClick={() => {\n            remove(index);\n          }}\n        />\n      )}\n      <PlusCircleOutlined\n        style={{ marginLeft: 8 }}\n        onClick={() => {\n          insert(index + 1, '');\n        }}\n      />\n    </div>\n  );\n\n  return <>{list.map((ele, index) => Row(index, ele))}</>;\n};\n\nexport default () => {\n  const [form] = Form.useForm();\n\n  const [result, setResult] = useState('');\n\n  return (\n    <>\n      <Form form={form}>\n        <Form.Item name=\"names\" initialValue={['David', 'Jack']}>\n          <DynamicInputs />\n        </Form.Item>\n      </Form>\n      <Button\n        type=\"primary\"\n        onClick={() =>\n          form\n            .validateFields()\n            .then((val) => {\n              setResult(JSON.stringify(val.names));\n            })\n            .catch(() => {})\n        }\n      >\n        Submit\n      </Button>\n      <Button style={{ marginLeft: 16 }} onClick={() => form.resetFields()}>\n        Reset\n      </Button>\n\n      <p>{result}</p>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useDynamicList/demo/demo3.tsx",
    "content": "/**\n * title: Used in antd Form\n * desc: Pay attention to the use of sortList. The data of antd Form is not sorted correctly. sortList can be used to calibrate the sorting.\n *\n * title.zh-CN: 在 antd Form 中使用的另一种写法\n * desc.zh-CN: 注意 sortList 的使用，antd Form 获取的数据排序不对，通过 sortList 可以校准排序。\n */\n\nimport { useState } from 'react';\nimport { Form, Button, Input } from 'antd';\nimport { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';\nimport { useDynamicList } from 'ahooks';\n\nexport default () => {\n  const { list, remove, getKey, insert, resetList, sortList } = useDynamicList(['David', 'Jack']);\n  const [form] = Form.useForm();\n\n  const [result, setResult] = useState('');\n\n  const Row = (index: number, item: any) => (\n    <div style={{ display: 'flex' }} key={getKey(index)}>\n      <div>\n        <Form.Item\n          rules={[{ required: true, message: 'required' }]}\n          name={['names', getKey(index)]}\n          initialValue={item}\n        >\n          <Input style={{ width: 300 }} placeholder=\"Please enter your name\" />\n        </Form.Item>\n      </div>\n      <div style={{ marginTop: 4 }}>\n        {list.length > 1 && (\n          <MinusCircleOutlined\n            style={{ marginLeft: 8 }}\n            onClick={() => {\n              remove(index);\n            }}\n          />\n        )}\n        <PlusCircleOutlined\n          style={{ marginLeft: 8 }}\n          onClick={() => {\n            insert(index + 1, '');\n          }}\n        />\n      </div>\n    </div>\n  );\n\n  return (\n    <>\n      <Form form={form}>{list.map((ele, index) => Row(index, ele))}</Form>\n      <Button\n        type=\"primary\"\n        onClick={() =>\n          form\n            .validateFields()\n            .then((val) => {\n              const sortedResult = sortList(val.names);\n              setResult(JSON.stringify(sortedResult, null, 2));\n            })\n            .catch(() => {})\n        }\n      >\n        Submit\n      </Button>\n      <Button style={{ marginLeft: 16 }} onClick={() => resetList(['David', 'Jack'])}>\n        Reset\n      </Button>\n\n      <div>{result}</div>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useDynamicList/demo/demo4.tsx",
    "content": "/**\n * title: Draggable dynamic table\n * desc: Using antd Table to build dynamic table form.\n *\n * title.zh-CN: 可拖拽的动态表格\n * desc.zh-CN: 使用 antd table 构建动态表格\n */\n\nimport { DragOutlined } from '@ant-design/icons';\nimport { Button, Form, Input, Table } from 'antd';\nimport { useState } from 'react';\nimport ReactDragListView from 'react-drag-listview';\nimport { useDynamicList } from 'ahooks';\n\ninterface Item {\n  name?: string;\n  age?: string;\n  memo?: string;\n}\n\nexport default () => {\n  const { list, remove, getKey, move, push, sortList } = useDynamicList<Item>([\n    { name: 'my bro', age: '23', memo: \"he's my bro\" },\n    { name: 'my sis', age: '21', memo: \"she's my sis\" },\n    {},\n  ]);\n\n  const [form] = Form.useForm();\n\n  const [result, setResult] = useState('');\n\n  const columns = [\n    {\n      title: 'Name',\n      dataIndex: 'name',\n      key: 'name',\n      render: (text: string, row: Item, index: number) => (\n        <>\n          <DragOutlined style={{ cursor: 'move', marginRight: 8 }} />\n          <Form.Item name={['params', getKey(index), 'name']} initialValue={text} noStyle>\n            <Input style={{ width: 120, marginRight: 16 }} placeholder=\"name\" />\n          </Form.Item>\n        </>\n      ),\n    },\n    {\n      title: 'Age',\n      dataIndex: 'age',\n      key: 'age',\n      render: (text: string, row: Item, index: number) => (\n        <Form.Item name={['params', getKey(index), 'age']} initialValue={text} noStyle>\n          <Input style={{ width: 120, marginRight: 16 }} placeholder=\"age\" />\n        </Form.Item>\n      ),\n    },\n    {\n      key: 'memo',\n      title: 'Memo',\n      dataIndex: 'memo',\n      render: (text: string, row: Item, index: number) => (\n        <>\n          <Form.Item name={['params', getKey(index), 'memo']} initialValue={text} noStyle>\n            <Input style={{ width: 300, marginRight: 16 }} placeholder=\"please input the memo\" />\n          </Form.Item>\n          <Button.Group>\n            <Button danger onClick={() => remove(index)}>\n              Delete\n            </Button>\n          </Button.Group>\n        </>\n      ),\n    },\n  ];\n\n  return (\n    <div>\n      <Form form={form}>\n        {/* @ts-ignore - ReactDragListView types issue */}\n        <ReactDragListView\n          onDragEnd={(oldIndex: number, newIndex: number) => move(oldIndex, newIndex)}\n          handleSelector={'span[aria-label=\"drag\"]'}\n        >\n          <Table\n            columns={columns}\n            dataSource={list}\n            rowKey={(r: Item, index?: number) => getKey(index || 0).toString()}\n            pagination={false}\n            style={{ overflow: 'auto' }}\n          />\n        </ReactDragListView>\n      </Form>\n      <Button\n        style={{ marginTop: 8 }}\n        block\n        type=\"dashed\"\n        onClick={() => push({ name: 'new row', age: '25' })}\n      >\n        + Add row\n      </Button>\n      <Button\n        type=\"primary\"\n        style={{ marginTop: 16 }}\n        onClick={() => {\n          form\n            .validateFields()\n            .then((val) => {\n              console.log(val, val.params);\n              const sortedResult = sortList(val.params);\n              setResult(JSON.stringify(sortedResult, null, 2));\n            })\n            .catch(() => {});\n        }}\n      >\n        Submit\n      </Button>\n      <div style={{ whiteSpace: 'pre' }}>{result && `content: ${result}`}</div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useDynamicList/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useDynamicList\n\nA hook that helps you manage dynamic list and generate unique key for each item.\n\n## Examples\n\n### Basic usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Using with antd Form\n\n<code src=\"./demo/demo2.tsx\" />\n\n### Another way of writing used in antd Form\n\n<code src=\"./demo/demo3.tsx\" />\n\n### Draggable dynamic table\n\n<code src=\"./demo/demo4.tsx\" />\n\n## API\n\n```typescript\nconst result: Result = useDynamicList(initialValue: T[]);\n```\n\n### Result\n\n| Property  | Description                              | Type                                           | Remarks                                                                                    |\n| --------- | ---------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------ |\n| list      | Current list                             | `T[]`                                          | -                                                                                          |\n| resetList | Reset list current data                  | `(list: T[]) => void`                          | -                                                                                          |\n| insert    | Add item at specific position            | `(index: number, item: T) => void`             | -                                                                                          |\n| merge     | Merge items into specific position       | `(index: number, items: T[]) => void`          | -                                                                                          |\n| replace   | Replace item at specific position        | `(index: number, item: T) => void`             | -                                                                                          |\n| remove    | Delete specific item                     | `(index: number) => void`                      | -                                                                                          |\n| move      | Move item from old index to new index    | `(oldIndex: number, newIndex: number) => void` | -                                                                                          |\n| getKey    | Get the uuid of specific item            | `(index: number) => number`                    | -                                                                                          |\n| getIndex  | Retrieve index from uuid                 | `(key: number) => number`                      | -                                                                                          |\n| sortList  | Sort the form data(using with antd form) | `(list: T[]) => T[]`                           | see[`Another way of writing used in antd Form`](#another-way-of-writing-used-in-antd-form) |\n| push      | Push new item at the end of list         | `(item: T) => void`                            | -                                                                                          |\n| pop       | Remove the last item from the list       | `() => void`                                   | -                                                                                          |\n| unshift   | Add new item at the front of the list    | `(item: T) => void`                            | -                                                                                          |\n| shift     | Remove the first item from the list      | `() => void`                                   | -                                                                                          |\n\n### Params\n\n| Property     | Description               | Type  | Default |\n| ------------ | ------------------------- | ----- | ------- |\n| initialValue | Initial value of the list | `T[]` | `[]`    |\n"
  },
  {
    "path": "packages/hooks/src/useDynamicList/index.ts",
    "content": "import { useCallback, useRef, useState } from 'react';\nimport isDev from '../utils/isDev';\n\nconst useDynamicList = <T>(initialList: T[] = []) => {\n  const counterRef = useRef(-1);\n\n  const keyList = useRef<number[]>([]);\n\n  const setKey = useCallback((index: number) => {\n    counterRef.current += 1;\n    keyList.current.splice(index, 0, counterRef.current);\n  }, []);\n\n  const [list, setList] = useState(() => {\n    initialList.forEach((_, index) => {\n      setKey(index);\n    });\n    return initialList;\n  });\n\n  const resetList = useCallback((newList: T[]) => {\n    keyList.current = [];\n    setList(() => {\n      newList.forEach((_, index) => {\n        setKey(index);\n      });\n      return newList;\n    });\n  }, []);\n\n  const insert = useCallback((index: number, item: T) => {\n    setList((l) => {\n      const temp = [...l];\n      temp.splice(index, 0, item);\n      setKey(index);\n      return temp;\n    });\n  }, []);\n\n  const getKey = useCallback((index: number) => keyList.current[index], []);\n\n  const getIndex = useCallback(\n    (key: number) => keyList.current.findIndex((ele) => ele === key),\n    [],\n  );\n\n  const merge = useCallback((index: number, items: T[]) => {\n    setList((l) => {\n      const temp = [...l];\n      items.forEach((_, i) => {\n        setKey(index + i);\n      });\n      temp.splice(index, 0, ...items);\n      return temp;\n    });\n  }, []);\n\n  const replace = useCallback((index: number, item: T) => {\n    setList((l) => {\n      const temp = [...l];\n      temp[index] = item;\n      return temp;\n    });\n  }, []);\n\n  const remove = useCallback((index: number) => {\n    setList((l) => {\n      const temp = [...l];\n      temp.splice(index, 1);\n\n      // remove keys if necessary\n      try {\n        keyList.current.splice(index, 1);\n      } catch (e) {\n        console.error(e);\n      }\n      return temp;\n    });\n  }, []);\n\n  const batchRemove = useCallback((indexes: number[]) => {\n    if (!Array.isArray(indexes)) {\n      if (isDev) {\n        console.error(\n          `\\`indexes\\` parameter of \\`batchRemove\\` function expected to be an array, but got \"${typeof indexes}\".`,\n        );\n      }\n      return;\n    }\n    if (!indexes.length) {\n      return;\n    }\n\n    setList((prevList) => {\n      const newKeyList: number[] = [];\n      const newList = prevList.filter((item, index) => {\n        const shouldKeep = !indexes.includes(index);\n\n        if (shouldKeep) {\n          newKeyList.push(getKey(index));\n        }\n\n        return shouldKeep;\n      });\n\n      keyList.current = newKeyList;\n\n      return newList;\n    });\n  }, []);\n\n  const move = useCallback((oldIndex: number, newIndex: number) => {\n    if (oldIndex === newIndex) {\n      return;\n    }\n    setList((l) => {\n      const newList = [...l];\n      const temp = newList.filter((_, index: number) => index !== oldIndex);\n      temp.splice(newIndex, 0, newList[oldIndex]);\n\n      // move keys if necessary\n      try {\n        const keyTemp = keyList.current.filter((_, index: number) => index !== oldIndex);\n        keyTemp.splice(newIndex, 0, keyList.current[oldIndex]);\n        keyList.current = keyTemp;\n      } catch (e) {\n        console.error(e);\n      }\n\n      return temp;\n    });\n  }, []);\n\n  const push = useCallback((item: T) => {\n    setList((l) => {\n      setKey(l.length);\n      return l.concat([item]);\n    });\n  }, []);\n\n  const pop = useCallback(() => {\n    // remove keys if necessary\n    try {\n      keyList.current = keyList.current.slice(0, keyList.current.length - 1);\n    } catch (e) {\n      console.error(e);\n    }\n\n    setList((l) => l.slice(0, l.length - 1));\n  }, []);\n\n  const unshift = useCallback((item: T) => {\n    setList((l) => {\n      setKey(0);\n      return [item].concat(l);\n    });\n  }, []);\n\n  const shift = useCallback(() => {\n    // remove keys if necessary\n    try {\n      keyList.current = keyList.current.slice(1, keyList.current.length);\n    } catch (e) {\n      console.error(e);\n    }\n    setList((l) => l.slice(1, l.length));\n  }, []);\n\n  const sortList = useCallback(\n    (result: T[]) =>\n      result\n        .map((item, index) => ({ key: index, item })) // add index into obj\n        .sort((a, b) => getIndex(a.key) - getIndex(b.key)) // sort based on the index of table\n        .filter((item) => !!item.item) // remove undefined(s)\n        .map((item) => item.item), // retrive the data\n    [],\n  );\n\n  return {\n    list,\n    insert,\n    merge,\n    replace,\n    remove,\n    batchRemove,\n    getKey,\n    getIndex,\n    move,\n    push,\n    pop,\n    unshift,\n    shift,\n    sortList,\n    resetList,\n  };\n};\n\nexport default useDynamicList;\n"
  },
  {
    "path": "packages/hooks/src/useDynamicList/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useDynamicList\n\n一个帮助你管理动态列表状态，并能生成唯一 key 的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 在 antd Form 中使用\n\n<code src=\"./demo/demo2.tsx\" />\n\n### 在 antd Form 中使用的另一种写法\n\n<code src=\"./demo/demo3.tsx\" />\n\n### 可拖拽的动态表格\n\n<code src=\"./demo/demo4.tsx\" />\n\n## API\n\n```typescript\nconst result: Result = useDynamicList(initialList?: T[]);\n```\n\n### Result\n\n| 参数      | 说明                   | 类型                                           | 备注                                                                             |\n| --------- | ---------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------- |\n| list      | 当前的列表             | `T[]`                                          | -                                                                                |\n| resetList | 重新设置 list 的值     | `(list: T[]) => void`                          | -                                                                                |\n| insert    | 在指定位置插入元素     | `(index: number, item: T) => void`             | -                                                                                |\n| merge     | 在指定位置插入多个元素 | `(index: number, items: T[]) => void`          | -                                                                                |\n| replace   | 替换指定元素           | `(index: number, item: T) => void`             | -                                                                                |\n| remove    | 删除指定元素           | `(index: number) => void`                      | -                                                                                |\n| move      | 移动元素               | `(oldIndex: number, newIndex: number) => void` | -                                                                                |\n| getKey    | 获得某个元素的 uuid    | `(index: number) => number`                    | -                                                                                |\n| getIndex  | 获得某个 key 的 index  | `(key: number) => number`                      | -                                                                                |\n| push      | 在列表末尾添加元素     | `(item: T) => void`                            | -                                                                                |\n| pop       | 移除末尾元素           | `() => void`                                   | -                                                                                |\n| unshift   | 在列表起始位置添加元素 | `(item: T) => void`                            | -                                                                                |\n| shift     | 移除起始位置元素       | `() => void`                                   | -                                                                                |\n| sortList  | 校准排序               | `(list: T[]) => T[]`                           | 使用方法详见 [在 antd Form 中使用的另一种写法](#在-antd-form-中使用的另一种写法) |\n\n### 参数\n\n| 参数        | 说明         | 类型  | 默认值 |\n| ----------- | ------------ | ----- | ------ |\n| initialList | 列表的初始值 | `T[]` | `[]`   |\n"
  },
  {
    "path": "packages/hooks/src/useEventEmitter/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { useState } from 'react';\nimport { describe, expect, test } from 'vitest';\nimport useEventEmitter from '../index';\n\ndescribe('useEventEmitter', () => {\n  const setUp = () =>\n    renderHook(() => {\n      const event$ = useEventEmitter<number>();\n      const [count, setCount] = useState(0);\n      event$.useSubscription((val) => {\n        setCount((c) => c + val);\n      });\n      event$.useSubscription((val) => {\n        setCount((c) => c + val + 10);\n      });\n      return {\n        event$,\n        count,\n      };\n    });\n\n  test('emit and subscribe should work', () => {\n    const hook = setUp();\n    act(() => {\n      hook.result.current.event$.emit(1);\n    });\n    expect(hook.result.current.count).toBe(12);\n    act(() => {\n      hook.result.current.event$.emit(2);\n    });\n    expect(hook.result.current.count).toBe(26);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useEventEmitter/demo/demo1.tsx",
    "content": "/**\n * title: Parent component shares a event\n * desc: The parent component creates a `focus$` event emitter, and passes it to its children. When calling `focus$.emit` in MessageBox, InputBox will get notified.\n *\n * title.zh-CN: 父组件向子组件共享事件\n * desc.zh-CN: 父组件创建了一个 `focus$` 事件，并且将它传递给了两个子组件。在 MessageBox 中调用 `focus$.emit` ，InputBox 组件就可以收到通知。\n */\n\nimport { useRef, type FC } from 'react';\nimport { useEventEmitter } from 'ahooks';\nimport { EventEmitter } from 'ahooks/lib/useEventEmitter';\n\nconst MessageBox: FC<{\n  focus$: EventEmitter<void>;\n}> = function (props) {\n  return (\n    <div style={{ paddingBottom: 24 }}>\n      <p>You received a message</p>\n      <button\n        type=\"button\"\n        onClick={() => {\n          props.focus$.emit();\n        }}\n      >\n        Reply\n      </button>\n    </div>\n  );\n};\n\nconst InputBox: FC<{\n  focus$: EventEmitter<void>;\n}> = function (props) {\n  const inputRef = useRef<HTMLInputElement>(null);\n  props.focus$.useSubscription(() => {\n    inputRef.current?.focus();\n  });\n  return (\n    <input ref={inputRef} placeholder=\"Enter reply\" style={{ width: '100%', padding: '4px' }} />\n  );\n};\n\nexport default function () {\n  const focus$ = useEventEmitter();\n  return (\n    <>\n      <MessageBox focus$={focus$} />\n      <InputBox focus$={focus$} />\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/hooks/src/useEventEmitter/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useEventEmitter\n\nSometimes it is difficult to pass events between multiple components. By using EventEmitter, this can be simplified.\n\nTo get an instance of `EventEmitter`, you can call `useEventEmitter` in React components.\n\n```js\nconst event$ = useEventEmitter();\n```\n\n> If the component renders multiple times, the return value of `useEventEmitter` in every render process will stay unchanged and no extra `EventEmitter` instance will be created.\n\nThen we can share `event$` to other components via `props` or `Context`. To push a event, just call the `emit` method of `EventEmitter`. To subscribe to a series of events, call the `useSubscription` method.\n\n```js\nevent$.emit('hello');\n```\n\n```js\nevent$.useSubscription(val => {\n  console.log(val);\n});\n```\n\n> `useSubscription` will automatically register the subscription and unsubscription.\n\nIf you want to let the child component notify the parent component, you can just use `props` to pass a `onEvent` function. And if you want to let the parent component notify the child component, you can use `forwardRef` to retrieve the ref of child component. `useEventEmitter` is most suitable for event management among multiple components or between two components which are far away.\n\n## Examples\n\n### Parent component shares a event\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n### Params\n\n```typescript\nconst result: Result = useEventEmitter<T>();\n```\n\n### Result\n\n| Property        | Description                   | Type                                   |\n| --------------- | ----------------------------- | -------------------------------------- |\n| emit            | Emit a new event.             | `(val: T) => void`                     |\n| useSubscription | Subscribe to a event emitter. | `(callback: (val: T) => void) => void` |\n"
  },
  {
    "path": "packages/hooks/src/useEventEmitter/index.ts",
    "content": "import { useRef, useEffect } from 'react';\n\ntype Subscription<T> = (val: T) => void;\n\nexport class EventEmitter<T> {\n  private subscriptions = new Set<Subscription<T>>();\n\n  emit = (val: T) => {\n    for (const subscription of this.subscriptions) {\n      subscription(val);\n    }\n  };\n\n  useSubscription = (callback: Subscription<T>) => {\n    // eslint-disable-next-line react-hooks/rules-of-hooks\n    const callbackRef = useRef<Subscription<T>>(undefined);\n    callbackRef.current = callback;\n    // eslint-disable-next-line react-hooks/rules-of-hooks\n    useEffect(() => {\n      function subscription(val: T) {\n        if (callbackRef.current) {\n          callbackRef.current(val);\n        }\n      }\n      this.subscriptions.add(subscription);\n      return () => {\n        this.subscriptions.delete(subscription);\n      };\n    }, []);\n  };\n}\n\nfunction useEventEmitter<T = void>() {\n  const ref = useRef<EventEmitter<T>>(undefined);\n  if (!ref.current) {\n    ref.current = new EventEmitter();\n  }\n  return ref.current;\n}\n\nexport default useEventEmitter;\n"
  },
  {
    "path": "packages/hooks/src/useEventEmitter/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useEventEmitter\n\n在多个组件之间进行事件通知有时会让人非常头疼，借助 EventEmitter ，可以让这一过程变得更加简单。\n\n在组件中调用 `useEventEmitter` 可以获得一个 `EventEmitter` 的实例：\n\n```js\nconst event$ = useEventEmitter();\n```\n\n> 在组件多次渲染时，每次渲染调用 `useEventEmitter` 得到的返回值会保持不变，不会重复创建 `EventEmitter` 的实例。\n\n通过 `props` 或者 `Context` ，可以将 `event$` 共享给其他组件。然后在其他组件中，可以调用 `EventEmitter` 的 `emit` 方法，推送一个事件，或是调用 `useSubscription` 方法，订阅事件。\n\n```js\nevent$.emit('hello');\n```\n\n```js\nevent$.useSubscription(val => {\n  console.log(val);\n});\n```\n\n> `useSubscription` 会在组件创建时自动注册订阅，并在组件销毁时自动取消订阅。\n\n对于**子组件**通知**父组件**的情况，我们仍然推荐直接使用 `props` 传递一个 `onEvent` 函数。而对于**父组件**通知**子组件**的情况，可以使用 `forwardRef` 获取子组件的 ref ，再进行子组件的方法调用。 `useEventEmitter` 适合的是在**距离较远**的组件之间进行事件通知，或是在**多个**组件之间共享事件通知。\n\n## 代码演示\n\n### 父组件向子组件共享事件\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst result: Result = useEventEmitter<T>();\n```\n\n### Result\n\n| 参数            | 说明             | 类型                                   |\n| --------------- | ---------------- | -------------------------------------- |\n| emit            | 发送一个事件通知 | `(val: T) => void`                     |\n| useSubscription | 订阅事件         | `(callback: (val: T) => void) => void` |\n"
  },
  {
    "path": "packages/hooks/src/useEventListener/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { afterEach, beforeEach, describe, expect, test } from 'vitest';\nimport useEventListener from '../index';\n\ndescribe('useEventListener', () => {\n  let container: HTMLDivElement;\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n  });\n\n  afterEach(() => {\n    document.body.removeChild(container);\n  });\n\n  test('test on click listener', async () => {\n    let state: number = 0;\n    const onClick = () => {\n      state++;\n    };\n    const { rerender, unmount } = renderHook(() =>\n      useEventListener('click', onClick, { target: () => container }),\n    );\n\n    document.body.click();\n    expect(state).toBe(0);\n    rerender();\n    container.click();\n    expect(state).toBe(1);\n    unmount();\n    document.body.click();\n    expect(state).toBe(1);\n  });\n\n  test('test on event list listener', async () => {\n    let state: number = 0;\n    const onClick = () => {\n      state++;\n    };\n    const onKeydown = () => {\n      state++;\n    };\n    const { rerender, unmount } = renderHook(\n      () => (\n        useEventListener('click', onClick, { target: () => container }),\n        useEventListener('keydown', onKeydown, { target: () => container })\n      ),\n    );\n\n    document.body.click();\n    document.body.dispatchEvent(new KeyboardEvent('keydown'));\n    expect(state).toBe(0);\n    rerender();\n    container.click();\n    container.dispatchEvent(new KeyboardEvent('keydown'));\n    expect(state).toBe(2);\n    unmount();\n    document.body.click();\n    document.body.dispatchEvent(new KeyboardEvent('keydown'));\n    expect(state).toBe(2);\n  });\n\n  test('test \"enable\" parameter', () => {\n    let state = 0;\n    let enable = true;\n    const onClick = () => state++;\n    const { rerender, unmount } = renderHook(() =>\n      useEventListener('click', onClick, { target: () => container, enable }),\n    );\n\n    document.body.click();\n    expect(state).toBe(0);\n    container.click();\n    expect(state).toBe(1);\n\n    enable = false;\n    rerender();\n    container.click();\n    expect(state).toBe(1);\n    unmount();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useEventListener/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Click the button to preview.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 点击按钮查看效果。\n */\n\nimport { useState, useRef } from 'react';\nimport { useEventListener } from 'ahooks';\n\nexport default () => {\n  const [value, setValue] = useState(0);\n  const ref = useRef(null);\n\n  useEventListener(\n    'click',\n    () => {\n      setValue(value + 1);\n    },\n    { target: ref },\n  );\n\n  return (\n    <button ref={ref} type=\"button\">\n      You click {value} times\n    </button>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useEventListener/demo/demo2.tsx",
    "content": "/**\n * title: Listen keydown\n * desc: Press any key to preview.\n *\n * title.zh-CN: 监听 keydown 事件\n * desc.zh-CN: 按下键盘查看效果。\n */\n\nimport { useState } from 'react';\nimport { useEventListener } from 'ahooks';\n\nexport default () => {\n  const [value, setValue] = useState('');\n\n  useEventListener('keydown', (ev) => {\n    setValue(ev.code);\n  });\n\n  return <p>Your press key is {value}</p>;\n};\n"
  },
  {
    "path": "packages/hooks/src/useEventListener/demo/demo3.tsx",
    "content": "/**\n * title: Listen to multiple events.\n * desc: Mouse hover or over the button to preview.\n *\n * title.zh-CN: 监听多个事件\n * desc.zh-CN: 鼠标移入移出按钮查看效果。\n */\n\nimport { useRef, useState } from 'react';\nimport { useEventListener } from 'ahooks';\n\nexport default () => {\n  const ref = useRef(null);\n  const [value, setValue] = useState('');\n\n  useEventListener(\n    ['mouseenter', 'mouseleave'],\n    (ev) => {\n      setValue(ev.type);\n    },\n    { target: ref },\n  );\n\n  return (\n    <button ref={ref} type=\"button\">\n      You Option is {value}\n    </button>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useEventListener/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useEventListener\n\nUse addEventListener elegant by Hook.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Listen for keydown\n\n<code src=\"./demo/demo2.tsx\" />\n\n### Listen for multiple events\n\n<code src=\"./demo/demo3.tsx\" />\n\n## API\n\n```typescript\nuseEventListener(\n  eventName: string,\n  handler: (ev: Event) => void,\n  options?: Options,\n);\n```\n\n### Property\n\n| Property  | Description            | type                   | default |\n| --------- | ---------------------- | ---------------------- | ------- |\n| eventName | Event name             | `string` \\| `string[]` | -       |\n| handler   | Callback function      | `(ev: Event) => void`  | -       |\n| options   | More options(optional) | `Options`              | -       |\n\n### Options\n\n| Property | Description                                                                                                                                                                                                                                     | type                                                                                          | default  |\n| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -------- |\n| target   | DOM element or ref                                                                                                                                                                                                                              | `(() => Element)` \\| `Element` \\| `React.MutableRefObject<Element>` \\| `Window` \\| `Document` | `window` |\n| capture  | Optional, a Boolean indicating that events of this type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree.                                                                    | `boolean`                                                                                     | `false`  |\n| once     | Optional, A Boolean indicating that the listener should be invoked at most once after being added. If true, the listener would be automatically removed when invoked.                                                                           | `boolean`                                                                                     | `false`  |\n| passive  | Optional, A Boolean which, if true, indicates that the function specified by listener will never call preventDefault(). If a passive listener does call preventDefault(), the user agent will do nothing other than generate a console warning. | `boolean`                                                                                     | `false`  |\n| enable   | Optional, Whether to enable listening.                                                                                                                                                                                                          | `boolean`                                                                                     | `true`   |\n"
  },
  {
    "path": "packages/hooks/src/useEventListener/index.ts",
    "content": "import useLatest from '../useLatest';\nimport type { BasicTarget } from '../utils/domTarget';\nimport { getTargetElement } from '../utils/domTarget';\nimport useEffectWithTarget from '../utils/useEffectWithTarget';\n\ntype noop = (...p: any) => void;\n\nexport type Target = BasicTarget<HTMLElement | Element | Window | Document>;\n\ntype Options<T extends Target = Target> = {\n  target?: T;\n  capture?: boolean;\n  once?: boolean;\n  passive?: boolean;\n  enable?: boolean;\n};\n\nfunction useEventListener<K extends keyof HTMLElementEventMap>(\n  eventName: K,\n  handler: (ev: HTMLElementEventMap[K]) => void,\n  options?: Options<HTMLElement>,\n): void;\nfunction useEventListener<K extends keyof ElementEventMap>(\n  eventName: K,\n  handler: (ev: ElementEventMap[K]) => void,\n  options?: Options<Element>,\n): void;\nfunction useEventListener<K extends keyof DocumentEventMap>(\n  eventName: K,\n  handler: (ev: DocumentEventMap[K]) => void,\n  options?: Options<Document>,\n): void;\nfunction useEventListener<K extends keyof WindowEventMap>(\n  eventName: K,\n  handler: (ev: WindowEventMap[K]) => void,\n  options?: Options<Window>,\n): void;\nfunction useEventListener(\n  eventName: string | string[],\n  handler: (event: Event) => void,\n  options?: Options<Window>,\n): void;\nfunction useEventListener(eventName: string | string[], handler: noop, options: Options): void;\n\nfunction useEventListener(eventName: string | string[], handler: noop, options: Options = {}) {\n  const { enable = true } = options;\n\n  const handlerRef = useLatest(handler);\n\n  useEffectWithTarget(\n    () => {\n      if (!enable) {\n        return;\n      }\n\n      const targetElement = getTargetElement(options.target, window);\n      if (!targetElement?.addEventListener) {\n        return;\n      }\n\n      const eventListener = (event: Event) => {\n        return handlerRef.current(event);\n      };\n\n      const eventNameArray = Array.isArray(eventName) ? eventName : [eventName];\n\n      eventNameArray.forEach((event) => {\n        targetElement.addEventListener(event, eventListener, {\n          capture: options.capture,\n          once: options.once,\n          passive: options.passive,\n        });\n      });\n\n      return () => {\n        eventNameArray.forEach((event) => {\n          targetElement.removeEventListener(event, eventListener, {\n            capture: options.capture,\n          });\n        });\n      };\n    },\n    [eventName, options.capture, options.once, options.passive, enable],\n    options.target,\n  );\n}\n\nexport default useEventListener;\n"
  },
  {
    "path": "packages/hooks/src/useEventListener/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useEventListener\n\n优雅的使用 addEventListener。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 监听 keydown 事件\n\n<code src=\"./demo/demo2.tsx\" />\n\n### 监听多个事件\n\n<code src=\"./demo/demo3.tsx\" />\n\n## API\n\n```typescript\nuseEventListener(\n  eventName: string,\n  handler: (ev: Event) => void,\n  options?: Options,\n);\n```\n\n### Params\n\n| 参数      | 说明       | 类型                   | 默认值 |\n| --------- | ---------- | ---------------------- | ------ |\n| eventName | 事件名称   | `string` \\| `string[]` | -      |\n| handler   | 处理函数   | `(ev: Event) => void`  | -      |\n| options   | 设置(可选) | `Options`              | -      |\n\n### Options\n\n| 参数    | 说明                                                                                                                                           | 类型                                                                                          | 默认值   |\n| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -------- |\n| target  | DOM 节点或者 ref                                                                                                                               | `(() => Element)` \\| `Element` \\| `React.MutableRefObject<Element>` \\| `Window` \\| `Document` | `window` |\n| capture | 可选项，listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。                                                                         | `boolean`                                                                                     | `false`  |\n| once    | 可选项，listener 在添加之后最多只调用一次。如果是 true，listener 会在其被调用之后自动移除。                                                    | `boolean`                                                                                     | `false`  |\n| passive | 可选项，设置为 true 时，表示 listener 永远不会调用 preventDefault() 。如果 listener 仍然调用了这个函数，客户端将会忽略它并抛出一个控制台警告。 | `boolean`                                                                                     | `false`  |\n| enable  | 可选项，是否开启监听。                                                                                                                         | `boolean`                                                                                     | `true`   |\n"
  },
  {
    "path": "packages/hooks/src/useEventTarget/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useEventTarget from '../index';\n\ndescribe('useEventTarget', () => {\n  test('should work without initial value', async () => {\n    const hook = renderHook(() => useEventTarget());\n    expect(hook.result.current[0]).toBeUndefined();\n    act(() => {\n      hook.result.current[1].onChange({ target: { value: 'abc' } });\n    });\n    expect(hook.result.current[0]).toBe('abc');\n  });\n\n  test('should work with initial value', async () => {\n    const hook = renderHook(() => useEventTarget({ initialValue: 'abc' }));\n    expect(hook.result.current[0]).toBe('abc');\n    act(() => {\n      hook.result.current[1].onChange({ target: { value: 'def' } });\n    });\n    expect(hook.result.current[0]).toBe('def');\n    act(() => {\n      hook.result.current[1].reset();\n    });\n    expect(hook.result.current[0]).toBe('abc');\n  });\n\n  test('should work with transformer', () => {\n    const hook = renderHook(() =>\n      useEventTarget({\n        transformer: (str: string) => str.toUpperCase(),\n      }),\n    );\n\n    expect(hook.result.current[0]).toBeUndefined();\n    act(() => {\n      hook.result.current[1].onChange({ target: { value: 'def' } });\n    });\n    expect(hook.result.current[0]).toBe('DEF');\n  });\n\n  test('should be able to transform to any type', () => {\n    const hook = renderHook(() =>\n      useEventTarget<string, number>({\n        transformer: (num: number) => String(num),\n      }),\n    );\n    expect(hook.result.current[0]).toBeUndefined();\n    act(() => {\n      hook.result.current[1].onChange({ target: { value: 123 } });\n    });\n    expect(hook.result.current[0]).toBe('123');\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useEventTarget/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Controlled input component，support reset.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 受控的 input，支持 reset。\n */\n\nimport { useEventTarget } from 'ahooks';\n\nexport default () => {\n  const [value, { reset, onChange }] = useEventTarget({ initialValue: 'this is initial value' });\n\n  return (\n    <div>\n      <input value={value} onChange={onChange} style={{ width: 200, marginRight: 20 }} />\n      <button type=\"button\" onClick={reset}>\n        reset\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useEventTarget/demo/demo2.tsx",
    "content": "/**\n * title: Custom transformer function\n * desc: Controlled input component with number input only\n *\n * title.zh-CN: 自定义转换函数\n * desc.zh-CN: 只能输入数字的 input 组件\n */\n\nimport { useEventTarget } from 'ahooks';\n\nexport default () => {\n  const [value, { onChange, reset }] = useEventTarget({\n    initialValue: '',\n    transformer: (val: string) => val.replace(/[^\\d]/g, ''),\n  });\n\n  return (\n    <div>\n      <input\n        value={value}\n        onChange={onChange}\n        style={{ width: 200, marginRight: 20 }}\n        placeholder=\"Please type here\"\n      />\n      <button type=\"button\" onClick={reset}>\n        reset\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useEventTarget/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useEventTarget\n\nA hook that encapsulates `onChange` and `value` logic for form controls that obtains value through `event.target.value`. It also supports custom transformer and reset functionalities.\n\n## Example\n\n### Basic Usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Custom transformer\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst [value, { onChange, reset }] = useEventTarget<T, U>(Options<T, U>);\n```\n\n### Result\n\n| Property | Description                                 | Type                                    |\n| -------- | ------------------------------------------- | --------------------------------------- |\n| value    | component value                             | `T`                                     |\n| onChange | callback when value changes                 | `(e: { target: { value: T } }) => void` |\n| reset    | function to reset the value to initialValue | `() => void`                            |\n\n### Options\n\n| Property     | Description                                | Type              | Default |\n| ------------ | ------------------------------------------ | ----------------- | ------- |\n| initialValue | initial value                              | `T`               | -       |\n| transformer  | custom transform function applied to value | `(value: U) => T` | -       |\n"
  },
  {
    "path": "packages/hooks/src/useEventTarget/index.ts",
    "content": "import { useCallback, useState } from 'react';\nimport useLatest from '../useLatest';\nimport { isFunction } from '../utils';\n\ninterface EventTarget<U> {\n  target: {\n    value: U;\n  };\n}\n\nexport interface Options<T, U> {\n  initialValue?: T;\n  transformer?: (value: U) => T;\n}\n\nfunction useEventTarget<T, U = T>(options?: Options<T, U>) {\n  const { initialValue, transformer } = options || {};\n  const [value, setValue] = useState(initialValue);\n\n  const transformerRef = useLatest(transformer);\n\n  const reset = useCallback(() => setValue(initialValue), []);\n\n  const onChange = useCallback((e: EventTarget<U>) => {\n    const _value = e.target.value;\n    if (isFunction(transformerRef.current)) {\n      return setValue(transformerRef.current(_value));\n    }\n    // no transformer => U and T should be the same\n    return setValue(_value as unknown as T);\n  }, []);\n\n  return [\n    value,\n    {\n      onChange,\n      reset,\n    },\n  ] as const;\n}\n\nexport default useEventTarget;\n"
  },
  {
    "path": "packages/hooks/src/useEventTarget/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useEventTarget\n\n常见表单控件(通过 e.target.value 获取表单值) 的 onChange 跟 value 逻辑封装，支持自定义值转换和重置功能。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 自定义转换函数\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst [value, { onChange, reset }] = useEventTarget<T, U>(Options<T, U>);\n```\n\n### Result\n\n| 参数     | 说明                         | 类型                                    |\n| -------- | ---------------------------- | --------------------------------------- |\n| value    | 表单控件的值                 | `T`                                     |\n| onChange | 表单控件值发生变化时候的回调 | `(e: { target: { value: T } }) => void` |\n| reset    | 重置函数                     | `() => void`                            |\n\n### Options\n\n| 参数         | 说明                         | 类型              | 默认值 |\n| ------------ | ---------------------------- | ----------------- | ------ |\n| initialValue | 可选项, 初始值               | `T`               | -      |\n| transformer  | 可选项，可自定义回调值的转化 | `(value: U) => T` | -      |\n"
  },
  {
    "path": "packages/hooks/src/useExternal/__tests__/index.spec.ts",
    "content": "import { act, fireEvent, renderHook } from '@testing-library/react';\nimport { beforeEach, describe, expect, test, vi } from 'vitest';\nimport useExternal, { type Options } from '../index';\n\nconst setup = (path: string, options?: Options) => renderHook(() => useExternal(path, options));\n\ndescribe('useExternal', () => {\n  beforeEach(() => {\n    document.body.innerHTML = '';\n    document.head.innerHTML = '';\n  });\n\n  test('should load a script', () => {\n    const path = 'https://ahooks.js.org/useExternal/test-external-script.js';\n    const { result } = setup(path, {\n      js: {\n        async: true,\n      },\n    });\n    const script = document.querySelector('script') as HTMLScriptElement;\n    expect(result.current).toBe('loading');\n    act(() => {\n      fireEvent.load(script);\n    });\n    expect(result.current).toBe('ready');\n  });\n\n  test('should load a css', () => {\n    const path = 'https://ahooks.js.org/useExternal/bootstrap-badge.css';\n    const { result } = setup(path, {\n      css: {\n        media: 'all',\n      },\n    });\n    const link = document.querySelector('link') as HTMLLinkElement;\n    expect(result.current).toBe('loading');\n    act(() => {\n      fireEvent.load(link);\n    });\n    expect(result.current).toBe('ready');\n  });\n\n  test('status should be unset without path', () => {\n    const { result } = setup('');\n    expect(result.current).toBe('unset');\n  });\n\n  test('status should be error when load failed', async () => {\n    const { result } = setup('xx.js');\n    const script = document.querySelector('script') as HTMLScriptElement;\n    act(() => {\n      fireEvent.error(script);\n    });\n    expect(result.current).toBe('error');\n  });\n\n  test('should throw error when provide unsupported type', () => {\n    const mockSpy = vi.spyOn(console, 'error').mockImplementationOnce(() => {});\n    setup('ahooks.ts');\n    expect(mockSpy).toBeCalled();\n  });\n\n  test('should not load again when the js exists', () => {\n    const path = 'a.js';\n    const hook1 = setup(path);\n    const script = document.querySelector('script') as HTMLScriptElement;\n    act(() => {\n      fireEvent.load(script);\n    });\n    expect(hook1.result.current).toBe('ready');\n\n    const hook2 = setup(path);\n    expect(hook2.result.current).toBe('ready');\n  });\n\n  test('should not load again when the css exists', () => {\n    const path = 'a.css';\n    const hook1 = setup(path);\n    const link = document.querySelector('link') as HTMLLinkElement;\n    act(() => {\n      fireEvent.load(link);\n    });\n    expect(hook1.result.current).toBe('ready');\n\n    const hook2 = setup(path);\n    expect(hook2.result.current).toBe('ready');\n  });\n\n  test('should remove when not use', () => {\n    const { unmount } = setup('b.js');\n    const script = document.querySelector('script') as HTMLScriptElement;\n    act(() => {\n      fireEvent.load(script);\n    });\n    unmount();\n    expect(document.querySelector('script')).toBeNull();\n  });\n  test('should not remove when keepWhenUnused is true', () => {\n    // https://github.com/alibaba/hooks/discussions/2163\n    const { result, unmount } = setup('b.js', {\n      keepWhenUnused: true,\n    });\n    const script = document.querySelector('script') as HTMLScriptElement;\n    act(() => {\n      fireEvent.load(script);\n    });\n    unmount();\n    expect(result.current).toBe('ready');\n  });\n\n  test('css preload should work in IE Edge', () => {\n    Object.defineProperty(HTMLLinkElement.prototype, 'hideFocus', {\n      value: true,\n    });\n    setup('b.css');\n    const link = document.querySelector('link') as HTMLLinkElement;\n    act(() => {\n      fireEvent.load(link);\n    });\n    expect(link.rel).toBe('preload');\n    expect(link.as).toBe('style');\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useExternal/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Load js file, such as [test-external-script.js](/useExternal/test-external-script.js)\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 加载 js 文件，例如引入 [test-external-script.js](/useExternal/test-external-script.js)\n */\n\nimport { useExternal } from 'ahooks';\n\nexport default () => {\n  const status = useExternal('/useExternal/test-external-script.js', {\n    js: {\n      async: true,\n    },\n  });\n\n  return (\n    <>\n      <p>\n        Status: <b>{status}</b>\n      </p>\n      <p>\n        Response: <i>{status === 'ready' ? (window as any).TEST_SCRIPT?.start() : '-'}</i>\n      </p>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useExternal/demo/demo2.tsx",
    "content": "/**\n * title: Load style dynamically\n * desc: Load css file, such as [bootstrap-badge.css](/useExternal/bootstrap-badge.css)\n *\n * title.zh-CN: 动态加载样式\n * desc.zh-CN: 加载 css 文件，例如引入 [bootstrap-badge.css](/useExternal/bootstrap-badge.css)\n */\n\nimport { useExternal } from 'ahooks';\nimport { useState } from 'react';\n\nexport default () => {\n  const [path, setPath] = useState('/useExternal/bootstrap-badge.css');\n\n  const status = useExternal(path);\n\n  return (\n    <>\n      <p>\n        Status: <b>{status}</b>\n      </p>\n      <div className=\"bd-example\" style={{ wordBreak: 'break-word' }}>\n        <span className=\"badge badge-pill badge-primary\">Primary</span>\n        <span className=\"badge badge-pill badge-secondary\">Secondary</span>\n        <span className=\"badge badge-pill badge-success\">Success</span>\n        <span className=\"badge badge-pill badge-danger\">Danger</span>\n        <span className=\"badge badge-pill badge-warning\">Warning</span>\n        <span className=\"badge badge-pill badge-info\">Info</span>\n        <span className=\"badge badge-pill badge-light\">Light</span>\n        <span className=\"badge badge-pill badge-dark\">Dark</span>\n      </div>\n      <br />\n      <button type=\"button\" style={{ marginRight: 8 }} onClick={() => setPath('')}>\n        unload\n      </button>\n      <button\n        type=\"button\"\n        style={{ marginRight: 8 }}\n        onClick={() => setPath('/useExternal/bootstrap-badge.css')}\n      >\n        load\n      </button>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useExternal/demo/demo3.tsx",
    "content": "/**\n * title: Load style dynamically\n * desc: Load css file, such as [bootstrap-badge.css](/useExternal/bootstrap-badge.css)\n *\n * title.zh-CN: 动态加载样式\n * desc.zh-CN: 加载 css 文件，例如引入 [bootstrap-badge.css](/useExternal/bootstrap-badge.css)\n */\n\nimport { useExternal } from 'ahooks';\nimport { useState } from 'react';\n\nexport default () => {\n  const [path, setPath] = useState('');\n  const status = useExternal(path);\n\n  const [path2, setPath2] = useState('');\n  const status2 = useExternal(path2);\n\n  return (\n    <>\n      <div className=\"bd-example\">\n        <span className=\"badge badge-pill badge-primary\">Primary</span>\n        <span className=\"badge badge-pill badge-secondary\">Secondary</span>\n        <span className=\"badge badge-pill badge-success\">Success</span>\n        <span className=\"badge badge-pill badge-danger\">Danger</span>\n        <span className=\"badge badge-pill badge-warning\">Warning</span>\n        <span className=\"badge badge-pill badge-info\">Info</span>\n        <span className=\"badge badge-pill badge-light\">Light</span>\n        <span className=\"badge badge-pill badge-dark\">Dark</span>\n      </div>\n      <br />\n\n      <h1>第一个</h1>\n      <p>\n        Status: <b>{status}</b>\n      </p>\n      <button type=\"button\" style={{ marginRight: 8 }} onClick={() => setPath('')}>\n        unload\n      </button>\n      <button\n        type=\"button\"\n        style={{ marginRight: 8 }}\n        onClick={() => setPath('/useExternal/bootstrap-badge.css')}\n      >\n        load\n      </button>\n      <hr />\n      <h1>第二个</h1>\n      <p>\n        Status: <b>{status2}</b>\n      </p>\n      <button type=\"button\" style={{ marginRight: 8 }} onClick={() => setPath2('')}>\n        unload\n      </button>\n      <button\n        type=\"button\"\n        style={{ marginRight: 8 }}\n        onClick={() => setPath2('/useExternal/bootstrap-badge.css')}\n      >\n        load\n      </button>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useExternal/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useExternal\n\nDynamically load JS or CSS, useExternal can ensure that the resource are globally unique.\n\n## Example\n\n### Basic Usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Load CSS\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst status = useExternal(path: string, options?: Options);\n```\n\n### Result\n\n| Params | Description                                                                                  | Type     |\n| ------ | -------------------------------------------------------------------------------------------- | -------- |\n| status | The progress of loading the external resources, support `unset`, `loading`, `ready`, `error` | `string` |\n\n### Params\n\n| Params | Description                       | Type     | Default |\n| ------ | --------------------------------- | -------- | ------- |\n| path   | The url of the external resources | `string` | -       |\n\n### Options\n\n| Params         | Description                                                                                                          | Type                | Default |\n| -------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------- | ------- |\n| type           | The type of external resources which need to load, support `js`/`css`, if no type, it will deduced according to path | `string`            | -       |\n| js             | Attributes supported by `script`                                                                                     | `HTMLScriptElement` | -       |\n| css            | Attributes supported by `link`                                                                                       | `HTMLStyleElement`  | -       |\n| keepWhenUnused | Allow resources to remain after they have lost their references                                                      | `boolean`           | `false` |\n"
  },
  {
    "path": "packages/hooks/src/useExternal/index.ts",
    "content": "import { useEffect, useRef, useState } from 'react';\n\ntype JsOptions = {\n  type: 'js';\n  js?: Partial<HTMLScriptElement>;\n  keepWhenUnused?: boolean;\n};\n\ntype CssOptions = {\n  type: 'css';\n  css?: Partial<HTMLStyleElement>;\n  keepWhenUnused?: boolean;\n};\n\ntype DefaultOptions = {\n  type?: never;\n  js?: Partial<HTMLScriptElement>;\n  css?: Partial<HTMLStyleElement>;\n  keepWhenUnused?: boolean;\n};\n\nexport type Options = JsOptions | CssOptions | DefaultOptions;\n\n// {[path]: count}\n// remove external when no used\nconst EXTERNAL_USED_COUNT: Record<string, number> = {};\n\nexport type Status = 'unset' | 'loading' | 'ready' | 'error';\n\ninterface LoadResult {\n  ref: Element;\n  status: Status;\n}\n\ntype LoadExternal = <T>(path: string, props?: Partial<T>) => LoadResult;\n\nconst loadScript: LoadExternal = (path, props = {}) => {\n  const script = document.querySelector(`script[src=\"${path}\"]`);\n\n  if (!script) {\n    const newScript = document.createElement('script');\n    newScript.src = path;\n\n    Object.keys(props).forEach((key) => {\n      (newScript as any)[key] = (props as any)[key];\n    });\n\n    newScript.setAttribute('data-status', 'loading');\n    document.body.appendChild(newScript);\n\n    return {\n      ref: newScript,\n      status: 'loading',\n    };\n  }\n\n  return {\n    ref: script,\n    status: (script.getAttribute('data-status') as Status) || 'ready',\n  };\n};\n\nconst loadCss: LoadExternal = (path, props = {}) => {\n  const css = document.querySelector(`link[href=\"${path}\"]`);\n  if (!css) {\n    const newCss = document.createElement('link');\n\n    newCss.rel = 'stylesheet';\n    newCss.href = path;\n    Object.keys(props).forEach((key) => {\n      (newCss as any)[key] = (props as any)[key];\n    });\n    // IE9+\n    const isLegacyIECss = 'hideFocus' in newCss;\n    // use preload in IE Edge (to detect load errors)\n    if (isLegacyIECss && newCss.relList) {\n      newCss.rel = 'preload';\n      newCss.as = 'style';\n    }\n    newCss.setAttribute('data-status', 'loading');\n    document.head.appendChild(newCss);\n\n    return {\n      ref: newCss,\n      status: 'loading',\n    };\n  }\n\n  return {\n    ref: css,\n    status: (css.getAttribute('data-status') as Status) || 'ready',\n  };\n};\n\nconst useExternal = (path?: string, options?: Options) => {\n  const [status, setStatus] = useState<Status>(path ? 'loading' : 'unset');\n\n  const ref = useRef<Element>(undefined);\n\n  useEffect(() => {\n    if (!path) {\n      setStatus('unset');\n      return;\n    }\n    const pathname = path.replace(/[|#].*$/, '');\n    if (options?.type === 'css' || (!options?.type && /(^css!|\\.css$)/.test(pathname))) {\n      const result = loadCss(path, options?.css);\n      ref.current = result.ref;\n      setStatus(result.status);\n    } else if (options?.type === 'js' || (!options?.type && /(^js!|\\.js$)/.test(pathname))) {\n      const result = loadScript(path, options?.js);\n      ref.current = result.ref;\n      setStatus(result.status);\n    } else {\n      // do nothing\n      console.error(\n        \"Cannot infer the type of external resource, and please provide a type ('js' | 'css'). \" +\n          'Refer to the https://ahooks.js.org/hooks/dom/use-external/#options',\n      );\n    }\n\n    if (!ref.current) {\n      return;\n    }\n\n    if (EXTERNAL_USED_COUNT[path] === undefined) {\n      EXTERNAL_USED_COUNT[path] = 1;\n    } else {\n      EXTERNAL_USED_COUNT[path] += 1;\n    }\n\n    const handler = (event: Event) => {\n      const targetStatus = event.type === 'load' ? 'ready' : 'error';\n      ref.current?.setAttribute('data-status', targetStatus);\n      setStatus(targetStatus);\n    };\n\n    ref.current.addEventListener('load', handler);\n    ref.current.addEventListener('error', handler);\n    return () => {\n      ref.current?.removeEventListener('load', handler);\n      ref.current?.removeEventListener('error', handler);\n\n      EXTERNAL_USED_COUNT[path] -= 1;\n\n      if (EXTERNAL_USED_COUNT[path] === 0 && !options?.keepWhenUnused) {\n        ref.current?.remove();\n      }\n\n      ref.current = undefined;\n    };\n  }, [path]);\n\n  return status;\n};\n\nexport default useExternal;\n"
  },
  {
    "path": "packages/hooks/src/useExternal/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useExternal\n\n动态注入 JS 或 CSS 资源，useExternal 可以保证资源全局唯一。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 加载 CSS\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst status = useExternal(path: string, options?: Options);\n```\n\n### Result\n\n| 参数   | 说明                                                                               | 类型     |\n| ------ | ---------------------------------------------------------------------------------- | -------- |\n| status | 加载状态，`unset`(未设置), `loading`(加载中), `ready`(加载完成), `error`(加载失败) | `string` |\n\n### Params\n\n| 参数 | 说明              | 类型     | 默认值 |\n| ---- | ----------------- | -------- | ------ |\n| path | 外部资源 url 地址 | `string` | -      |\n\n### Options\n\n| 参数           | 说明                                                              | 类型                | 默认值  |\n| -------------- | ----------------------------------------------------------------- | ------------------- | ------- |\n| type           | 需引入外部资源的类型，支持 `js`/`css`，如果不传，则根据 path 推导 | `string`            | -       |\n| js             | `script` 标签支持的属性                                           | `HTMLScriptElement` | -       |\n| css            | `link` 标签支持的属性                                             | `HTMLStyleElement`  | -       |\n| keepWhenUnused | 在不持有资源的引用后，仍然保留资源                                | `boolean`           | `false` |\n"
  },
  {
    "path": "packages/hooks/src/useFavicon/__tests__/index.spec.tsx",
    "content": "import { renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useFavicon from '../index';\n\ndescribe('useFavicon', () => {\n  test('should set the favicon', () => {\n    expect(document.querySelector(\"link[rel*='icon']\")).toBeNull();\n    renderHook(() => useFavicon('favicon.ico'));\n    expect(document.querySelector(\"link[rel*='icon']\")).not.toBeNull();\n  });\n\n  test('should support svg/png/ico/gif', () => {\n    const { rerender } = renderHook((url: string) => useFavicon(url));\n    const suffixes = ['svg', 'png', 'ico', 'gif'] as const;\n    const imgTypeMap = {\n      svg: 'image/svg+xml',\n      ico: 'image/x-icon',\n      gif: 'image/gif',\n      png: 'image/png',\n    } as const;\n    suffixes.forEach((suffix) => {\n      const url = `favicon.${suffix}`;\n      rerender(url);\n      const link = document.querySelector(\"link[rel*='icon']\") as HTMLLinkElement;\n      expect(link.getAttribute('type')).toBe(imgTypeMap[suffix]);\n      expect(link.getAttribute('href')).toBe(url);\n      expect(link.getAttribute('rel')).toBe('shortcut icon');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useFavicon/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Set favicon\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 设置 favicon\n */\n\nimport { useState } from 'react';\nimport { useFavicon } from 'ahooks';\n\nexport const DEFAULT_FAVICON_URL = 'https://ahooks.js.org/simple-logo.svg';\n\nexport const GOOGLE_FAVICON_URL = 'https://www.google.com/favicon.ico';\n\nexport default () => {\n  const [url, setUrl] = useState<string>(DEFAULT_FAVICON_URL);\n\n  useFavicon(url);\n\n  return (\n    <>\n      <p>\n        Current Favicon: <span>{url}</span>\n      </p>\n      <button\n        style={{ marginRight: 16 }}\n        onClick={() => {\n          setUrl(GOOGLE_FAVICON_URL);\n        }}\n      >\n        Change To Google Favicon\n      </button>\n      <button\n        onClick={() => {\n          setUrl(DEFAULT_FAVICON_URL);\n        }}\n      >\n        Back To AHooks Favicon\n      </button>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useFavicon/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useFavicon\n\nA hook that set the favicon of the page.\n\n## Example\n\n### Basic Usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseFavicon(href: string);\n```\n\n### Params\n\n| Params | Description                                  | Type     | Default |\n| ------ | -------------------------------------------- | -------- | ------- |\n| href   | favicon URL, support `svg`/`png`/`ico`/`gif` | `string` | -       |\n\n## FAQ\n\n### It doesn't work in Safari?\n\nSafari cannot set the favicon dynamically.\n\n> Apple intentionally do not want the ability to script favicons. See https://bugs.webkit.org/show_bug.cgi?id=95979#c2\n\nRelated issues: [#2126](https://github.com/alibaba/hooks/issues/2126)\n"
  },
  {
    "path": "packages/hooks/src/useFavicon/index.ts",
    "content": "import { useEffect } from 'react';\n\nconst ImgTypeMap = {\n  SVG: 'image/svg+xml',\n  ICO: 'image/x-icon',\n  GIF: 'image/gif',\n  PNG: 'image/png',\n};\n\ntype ImgTypes = keyof typeof ImgTypeMap;\n\nconst useFavicon = (href: string) => {\n  useEffect(() => {\n    if (!href) {\n      return;\n    }\n\n    const cutUrl = href.split('.');\n\n    const imgSuffix = cutUrl[cutUrl.length - 1].toLocaleUpperCase() as ImgTypes;\n\n    const link =\n      document.querySelector<HTMLLinkElement>(\"link[rel*='icon']\") ||\n      document.createElement('link');\n\n    link.type = ImgTypeMap[imgSuffix];\n    link.href = href;\n    link.rel = 'shortcut icon';\n\n    document.getElementsByTagName('head')[0].appendChild(link);\n  }, [href]);\n};\n\nexport default useFavicon;\n"
  },
  {
    "path": "packages/hooks/src/useFavicon/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useFavicon\n\n设置页面的 favicon。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseFavicon(href: string);\n```\n\n### Params\n\n| 参数 | 说明                                                  | 类型     | 默认值 |\n| ---- | ----------------------------------------------------- | -------- | ------ |\n| href | favicon 地址, 支持 `svg`/`png`/`ico`/`gif` 后缀的图片 | `string` | -      |\n\n## FAQ\n\n### 在 Safari 中不工作？\n\nSafari 无法动态设置 favicon。\n\n> Apple intentionally do not want the ability to script favicons. See https://bugs.webkit.org/show_bug.cgi?id=95979#c2\n\n相关 issue：[#2126](https://github.com/alibaba/hooks/issues/2126)\n"
  },
  {
    "path": "packages/hooks/src/useFocusWithin/__tests__/index.spec.tsx",
    "content": "import { cleanup, fireEvent, render } from '@testing-library/react';\nimport { useRef } from 'react';\nimport { beforeEach, describe, expect, test, vi } from 'vitest';\nimport useFocusWithin, { type Options } from '../index';\n\nconst setup = (options?: Options) => {\n  const TestComponent = () => {\n    const ref = useRef(null);\n    const isFocusWithin = useFocusWithin(ref, options);\n    return (\n      <div ref={ref}>\n        <label>\n          First Name\n          <input />\n        </label>\n        <label>\n          Last Name\n          <input />\n        </label>\n        <p>isFocusWithin: {JSON.stringify(isFocusWithin)}</p>\n      </div>\n    );\n  };\n\n  return render(<TestComponent />);\n};\n\ndescribe('useFocusWithin', () => {\n  beforeEach(() => {\n    cleanup();\n    vi.clearAllMocks();\n  });\n  test('should call onFocus/onBlur', () => {\n    const onFocus = vi.fn();\n    const onBlur = vi.fn();\n    const result = setup({ onFocus, onBlur });\n    fireEvent.focusIn(result.getAllByLabelText('First Name')[0]);\n    expect(onFocus).toBeCalled();\n    fireEvent.focusOut(result.getAllByLabelText('First Name')[0]);\n    expect(onBlur).toBeCalled();\n  });\n\n  test('should call onChange', () => {\n    const onChange = vi.fn();\n    const result = setup({ onChange });\n    fireEvent.focusIn(result.getAllByLabelText('First Name')[0]);\n    expect(onChange).toBeCalledWith(true);\n    fireEvent.focusOut(result.getAllByLabelText('First Name')[0]);\n    expect(onChange).toHaveBeenLastCalledWith(false);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useFocusWithin/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Use ref to set area that needs monitoring. The focus can be switched by click the outside with the mouse, or using keys such as `tab` on the keyboard.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 使用 ref 设置需要监听的区域。可以通过鼠标点击外部区域，或者使用键盘的 `tab` 等按键来切换焦点。\n */\n\nimport { useRef } from 'react';\nimport { useFocusWithin } from 'ahooks';\nimport { message } from 'antd';\n\nexport default () => {\n  const ref = useRef(null);\n  const isFocusWithin = useFocusWithin(ref, {\n    onFocus: () => {\n      message.info('focus');\n    },\n    onBlur: () => {\n      message.info('blur');\n    },\n  });\n  return (\n    <div>\n      <div\n        ref={ref}\n        style={{\n          padding: 16,\n          backgroundColor: isFocusWithin ? 'red' : '',\n          border: '1px solid gray',\n        }}\n      >\n        <label style={{ display: 'block' }}>\n          First Name: <input />\n        </label>\n        <label style={{ display: 'block', marginTop: 16 }}>\n          Last Name: <input />\n        </label>\n      </div>\n      <p>isFocusWithin: {JSON.stringify(isFocusWithin)}</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useFocusWithin/demo/demo2.tsx",
    "content": "/**\n * title: Pass in DOM element\n * desc: Pass in a function that returns the DOM element.\n *\n * title.zh-CN: 传入 DOM 元素\n * desc.zh-CN: 传入 function 并返回一个 dom 元素。\n */\n\nimport { useFocusWithin } from 'ahooks';\n\nexport default () => {\n  const isFocusWithin = useFocusWithin(() => document.getElementById('focus-area'));\n\n  return (\n    <div>\n      <div\n        id=\"focus-area\"\n        style={{\n          padding: 16,\n          backgroundColor: isFocusWithin ? 'red' : '',\n          border: '1px solid gray',\n        }}\n      >\n        <label style={{ display: 'block' }}>\n          First Name: <input />\n        </label>\n        <label style={{ display: 'block', marginTop: 16 }}>\n          Last Name: <input />\n        </label>\n      </div>\n      <p>isFocusWithin: {JSON.stringify(isFocusWithin)}</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useFocusWithin/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useFocusWithin\n\nMonitor whether the current focus is within a certain area, Same as css attribute [:focus-within](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-within).\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Pass in DOM element\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst isFocusWithin = useFocusWithin(\n  target,\n  {\n   onFocus,\n   onBlur,\n   onChange\n  }\n);\n```\n\n### Params\n\n| Property | Description        | Type                                                        | Default |\n| -------- | ------------------ | ----------------------------------------------------------- | ------- |\n| target   | DOM element or ref | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -       |\n| options  | More config        | `Options`                                                   | -       |\n\n### Options\n\n| Property | Description                             | Type                               | Default |\n| -------- | --------------------------------------- | ---------------------------------- | ------- |\n| onFocus  | Callback to be executed on focus        | `(e: FocusEvent) => void`          | -       |\n| onBlur   | Callback to be executed on blur         | `(e: FocusEvent) => void`          | -       |\n| onChange | Callback to be executed on focus change | `(isFocusWithin: boolean) => void` | -       |\n\n### Result\n\n| Property      | Description                              | Type      |\n| ------------- | ---------------------------------------- | --------- |\n| isFocusWithin | Whether the focus is in the current area | `boolean` |\n"
  },
  {
    "path": "packages/hooks/src/useFocusWithin/index.tsx",
    "content": "import { useState } from 'react';\nimport useEventListener from '../useEventListener';\nimport type { BasicTarget } from '../utils/domTarget';\n\nexport interface Options {\n  onFocus?: (e: FocusEvent) => void;\n  onBlur?: (e: FocusEvent) => void;\n  onChange?: (isFocusWithin: boolean) => void;\n}\n\nexport default function useFocusWithin(target: BasicTarget, options?: Options) {\n  const [isFocusWithin, setIsFocusWithin] = useState(false);\n  const { onFocus, onBlur, onChange } = options || {};\n\n  useEventListener(\n    'focusin',\n    (e: FocusEvent) => {\n      if (!isFocusWithin) {\n        onFocus?.(e);\n        onChange?.(true);\n        setIsFocusWithin(true);\n      }\n    },\n    {\n      target,\n    },\n  );\n\n  useEventListener(\n    'focusout',\n    (e: FocusEvent) => {\n      if (isFocusWithin && !(e.currentTarget as Element)?.contains?.(e.relatedTarget as Element)) {\n        onBlur?.(e);\n        onChange?.(false);\n        setIsFocusWithin(false);\n      }\n    },\n    {\n      target,\n    },\n  );\n\n  return isFocusWithin;\n}\n"
  },
  {
    "path": "packages/hooks/src/useFocusWithin/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useFocusWithin\n\n监听当前焦点是否在某个区域之内，同 css 属性 [:focus-within](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-within)。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 传入 DOM 元素\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst isFocusWithin = useFocusWithin(\n  target,\n  {\n   onFocus,\n   onBlur,\n   onChange\n  }\n);\n```\n\n### Params\n\n| 参数    | 说明                  | 类型                                                        | 默认值 |\n| ------- | --------------------- | ----------------------------------------------------------- | ------ |\n| target  | DOM 节点或者 Ref 对象 | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -      |\n| options | 额外的配置项          | `Options`                                                   | -      |\n\n### Options\n\n| 参数     | 说明           | 类型                               | 默认值 |\n| -------- | -------------- | ---------------------------------- | ------ |\n| onFocus  | 获取焦点时触发 | `(e: FocusEvent) => void`          | -      |\n| onBlur   | 失去焦点时触发 | `(e: FocusEvent) => void`          | -      |\n| onChange | 焦点变化时触发 | `(isFocusWithin: boolean) => void` | -      |\n\n### Result\n\n| 参数          | 说明               | 类型      |\n| ------------- | ------------------ | --------- |\n| isFocusWithin | 焦点是否在当前区域 | `boolean` |\n"
  },
  {
    "path": "packages/hooks/src/useFullscreen/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport screenfull from 'screenfull';\nimport { afterAll, afterEach, beforeEach, describe, expect, test, vi } from 'vitest';\nimport type { BasicTarget } from '../../utils/domTarget';\nimport useFullscreen, { type Options } from '../index';\n\n// Mock screenfull\nvi.mock('screenfull', () => ({\n  default: {\n    isEnabled: true,\n    element: null,\n    request: vi.fn(),\n    exit: vi.fn(),\n    on: vi.fn(),\n    off: vi.fn(),\n  },\n}));\n\nconst mockScreenfull = screenfull as any;\n\nlet globalHook: any;\nlet targetEl: any;\nlet changeCallback: any;\n\nconst setup = (target: BasicTarget, options?: Options) => {\n  globalHook = renderHook(() => useFullscreen(target, options));\n  return globalHook;\n};\n\ndescribe('useFullscreen', () => {\n  beforeEach(() => {\n    targetEl = document.createElement('div');\n    document.body.appendChild(targetEl);\n\n    // Reset screenfull mocks\n    mockScreenfull.element = null;\n    mockScreenfull.on.mockImplementation((event: string, callback: any) => {\n      if (event === 'change') {\n        changeCallback = callback;\n      }\n    });\n    mockScreenfull.off.mockImplementation(() => {});\n    mockScreenfull.request.mockImplementation((el: any) => {\n      mockScreenfull.element = el;\n      return Promise.resolve();\n    });\n    mockScreenfull.exit.mockImplementation(() => {\n      mockScreenfull.element = null;\n      return Promise.resolve();\n    });\n\n    vi.clearAllMocks();\n  });\n\n  afterEach(() => {\n    document.body.removeChild(targetEl);\n    globalHook?.unmount();\n    changeCallback = null;\n  });\n\n  afterAll(() => {\n    vi.resetAllMocks();\n  });\n\n  test('enterFullscreen/exitFullscreen should be work', () => {\n    const { result } = setup(targetEl);\n    const { enterFullscreen, exitFullscreen } = result.current[1];\n\n    enterFullscreen();\n    expect(mockScreenfull.request).toBeCalledWith(targetEl);\n\n    act(() => {\n      if (changeCallback) changeCallback();\n    });\n    expect(result.current[0]).toBe(true);\n\n    exitFullscreen();\n    expect(mockScreenfull.exit).toBeCalled();\n\n    act(() => {\n      if (changeCallback) changeCallback();\n    });\n    expect(result.current[0]).toBe(false);\n  });\n\n  test('toggleFullscreen should be work', () => {\n    const { result } = setup(targetEl);\n    const { toggleFullscreen } = result.current[1];\n\n    toggleFullscreen();\n    expect(mockScreenfull.request).toBeCalledWith(targetEl);\n\n    act(() => {\n      if (changeCallback) changeCallback();\n    });\n    expect(result.current[0]).toBe(true);\n\n    toggleFullscreen();\n    expect(mockScreenfull.exit).toBeCalled();\n\n    act(() => {\n      if (changeCallback) changeCallback();\n    });\n    expect(result.current[0]).toBe(false);\n  });\n\n  test('onExit/onEnter should be called', () => {\n    const onExit = vi.fn();\n    const onEnter = vi.fn();\n    const { result } = setup(targetEl, {\n      onExit,\n      onEnter,\n    });\n    const { toggleFullscreen } = result.current[1];\n\n    toggleFullscreen();\n    act(() => {\n      if (changeCallback) changeCallback();\n    });\n    expect(onEnter).toBeCalled();\n\n    toggleFullscreen();\n    act(() => {\n      if (changeCallback) changeCallback();\n    });\n    expect(onExit).toBeCalled();\n  });\n\n  test('onExit/onEnter should not be called', () => {\n    const onExit = vi.fn();\n    const onEnter = vi.fn();\n    const { result } = setup(targetEl, {\n      onExit,\n      onEnter,\n    });\n    const { exitFullscreen, enterFullscreen } = result.current[1];\n\n    // `onExit` should not be called when not full screen\n    exitFullscreen();\n    act(() => {\n      if (changeCallback) changeCallback();\n    });\n    expect(onExit).not.toBeCalled();\n\n    // Enter full screen\n    enterFullscreen();\n    act(() => {\n      if (changeCallback) changeCallback();\n    });\n    expect(onEnter).toBeCalled();\n    onEnter.mockReset();\n\n    // `onEnter` should not be called when full screen\n    enterFullscreen();\n    expect(onEnter).not.toBeCalled();\n  });\n\n  test('pageFullscreen should be work', () => {\n    const PAGE_FULLSCREEN_CLASS_NAME = 'test-page-fullscreen';\n    const PAGE_FULLSCREEN_Z_INDEX = 101;\n    const onExit = vi.fn();\n    const onEnter = vi.fn();\n    const { result } = setup(targetEl, {\n      onExit,\n      onEnter,\n      pageFullscreen: {\n        className: PAGE_FULLSCREEN_CLASS_NAME,\n        zIndex: PAGE_FULLSCREEN_Z_INDEX,\n      },\n    });\n    const { toggleFullscreen } = result.current[1];\n    const getStyleEl = () => targetEl.querySelector('style');\n\n    act(() => toggleFullscreen());\n    expect(result.current[0]).toBe(true);\n    expect(onEnter).toBeCalled();\n    expect(targetEl.classList.contains(PAGE_FULLSCREEN_CLASS_NAME)).toBeTruthy();\n    expect(getStyleEl()).not.toBeNull();\n    expect(getStyleEl()?.textContent).toContain(`z-index: ${PAGE_FULLSCREEN_Z_INDEX}`);\n    expect(getStyleEl()?.getAttribute('id')).toBe(PAGE_FULLSCREEN_CLASS_NAME);\n\n    act(() => toggleFullscreen());\n    expect(result.current[0]).toBe(false);\n    expect(onExit).toBeCalled();\n    expect(targetEl.classList.contains(PAGE_FULLSCREEN_CLASS_NAME)).toBeFalsy();\n    expect(getStyleEl()).toBeNull();\n    expect(getStyleEl()?.textContent).toBeUndefined();\n    expect(getStyleEl()?.getAttribute('id')).toBeUndefined();\n  });\n\n  test('enterFullscreen should not work when target is not element', () => {\n    const onEnter = vi.fn();\n    const { result } = setup(null, { onEnter });\n    const { enterFullscreen } = result.current[1];\n    enterFullscreen();\n    expect(mockScreenfull.request).not.toBeCalled();\n    expect(onEnter).not.toBeCalled();\n  });\n\n  test('should remove event listener when unmount', () => {\n    const { unmount } = setup(targetEl);\n    expect(mockScreenfull.on).toBeCalledWith('change', expect.any(Function));\n\n    unmount();\n    expect(mockScreenfull.off).toBeCalledWith('change', expect.any(Function));\n  });\n\n  test('`isFullscreen` should be false when use `document.exitFullscreen`', () => {\n    const { result } = setup(targetEl);\n    const { enterFullscreen } = result.current[1];\n\n    enterFullscreen();\n    act(() => {\n      if (changeCallback) changeCallback();\n    });\n    expect(result.current[0]).toBe(true);\n\n    // Simulate external exit fullscreen\n    mockScreenfull.element = null;\n    act(() => {\n      if (changeCallback) changeCallback();\n    });\n    expect(result.current[0]).toBe(false);\n  });\n\n  test('mutli element full screen should be correct', () => {\n    const targetEl2 = document.createElement('p');\n    document.body.appendChild(targetEl2);\n\n    // Store separate change callbacks for each hook\n    let changeCallback1: any = null;\n    let changeCallback2: any = null;\n\n    // Override mock to track multiple callbacks\n    mockScreenfull.on.mockImplementation((event: string, callback: any) => {\n      if (event === 'change') {\n        if (!changeCallback1) {\n          changeCallback1 = callback;\n        } else if (!changeCallback2) {\n          changeCallback2 = callback;\n        }\n      }\n    });\n\n    const hook = setup(targetEl);\n    const hook2 = setup(targetEl2);\n\n    // target1 full screen\n    hook.result.current[1].enterFullscreen();\n    expect(mockScreenfull.element).toBe(targetEl);\n    act(() => {\n      if (changeCallback1) changeCallback1();\n      if (changeCallback2) changeCallback2();\n    });\n    expect(hook.result.current[0]).toBe(true);\n\n    // target2 full screen (this should make target1 not fullscreen)\n    hook2.result.current[1].enterFullscreen();\n    expect(mockScreenfull.element).toBe(targetEl2);\n    act(() => {\n      if (changeCallback1) changeCallback1();\n      if (changeCallback2) changeCallback2();\n    });\n    expect(hook.result.current[0]).toBe(false);\n    expect(hook2.result.current[0]).toBe(true);\n\n    // target2 exit full screen (no element is fullscreen now)\n    hook2.result.current[1].exitFullscreen();\n    expect(mockScreenfull.element).toBe(null);\n    act(() => {\n      if (changeCallback1) changeCallback1();\n      if (changeCallback2) changeCallback2();\n    });\n    expect(hook.result.current[0]).toBe(false);\n    expect(hook2.result.current[0]).toBe(false);\n\n    document.body.removeChild(targetEl2);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useFullscreen/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Use ref to set elements that need full screen\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 使用 ref 设置需要全屏的元素\n */\n\nimport { useRef } from 'react';\nimport { useFullscreen } from 'ahooks';\n\nexport default () => {\n  const ref = useRef(null);\n  const [isFullscreen, { enterFullscreen, exitFullscreen, toggleFullscreen }] = useFullscreen(ref);\n  return (\n    <div ref={ref} style={{ background: 'white' }}>\n      <div style={{ marginBottom: 16 }}>{isFullscreen ? 'Fullscreen' : 'Not fullscreen'}</div>\n      <div>\n        <button type=\"button\" onClick={enterFullscreen}>\n          enterFullscreen\n        </button>\n        <button type=\"button\" onClick={exitFullscreen} style={{ margin: '0 8px' }}>\n          exitFullscreen\n        </button>\n        <button type=\"button\" onClick={toggleFullscreen}>\n          toggleFullscreen\n        </button>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useFullscreen/demo/demo2.tsx",
    "content": "/**\n * title: Image full screen\n *\n * title.zh-CN: 图片全屏\n */\n\nimport { useFullscreen } from 'ahooks';\n// @ts-ignore - Image import\nimport img from './react-hooks.jpg';\n\nexport default () => {\n  const [, { enterFullscreen }] = useFullscreen(() => document.getElementById('fullscreen-img'));\n  return (\n    <div style={{ background: 'white' }}>\n      <div style={{ marginBottom: 16 }}>\n        <img id=\"fullscreen-img\" src={img} style={{ width: 320 }} alt=\"\" />\n      </div>\n      <button type=\"button\" onClick={enterFullscreen}>\n        enterFullscreen\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useFullscreen/demo/demo3.tsx",
    "content": "/**\n * title: Page full screen\n *\n * title.zh-CN: 页面全屏\n */\n\nimport { useRef } from 'react';\nimport { useFullscreen } from 'ahooks';\n\nexport default () => {\n  const ref = useRef(null);\n  const [isFullscreen, { toggleFullscreen, enterFullscreen, exitFullscreen }] = useFullscreen(ref, {\n    pageFullscreen: true,\n  });\n\n  return (\n    <div style={{ background: 'white' }}>\n      <div ref={ref} style={{ background: '#4B6BCD', padding: 12 }}>\n        <div style={{ marginBottom: 16 }}>{isFullscreen ? 'Fullscreen' : 'Not fullscreen'}</div>\n        <button type=\"button\" onClick={enterFullscreen}>\n          enterFullscreen\n        </button>\n        <button type=\"button\" onClick={exitFullscreen} style={{ margin: '0 8px' }}>\n          exitFullscreen\n        </button>\n        <button type=\"button\" onClick={toggleFullscreen}>\n          toggleFullscreen\n        </button>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useFullscreen/demo/demo4.tsx",
    "content": "/**\n * title: Coexist with other full screen operations\n * desc: The element's full screen may be modified by other scripts, don't worry, ahooks can work with them.\n *\n * title.zh-CN: 与其它全屏操作共存\n * desc.zh-CN: 元素的全屏情况可能被其它脚本修改，不用担心，ahooks 可以与它们共存。\n */\n\nimport { useRef } from 'react';\nimport { useFullscreen } from 'ahooks';\n\nfunction vanillaToggleFullscreen(element: HTMLElement) {\n  const isFullscreen = !!document.fullscreenElement;\n\n  if (isFullscreen) {\n    document.exitFullscreen();\n  } else {\n    element.requestFullscreen();\n  }\n}\n\nexport default () => {\n  const ref = useRef(null);\n  const [isFullscreen, { toggleFullscreen }] = useFullscreen(ref);\n\n  return (\n    <div ref={ref} style={{ background: 'white' }}>\n      <div style={{ marginBottom: 16 }}>{isFullscreen ? 'Fullscreen' : 'Not fullscreen'}</div>\n      <div>\n        <button style={{ marginRight: '8px' }} onClick={toggleFullscreen}>\n          ahooks toggleFullscreen\n        </button>\n        <button onClick={() => ref.current && vanillaToggleFullscreen(ref.current)}>\n          vanilla toggleFullscreen\n        </button>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useFullscreen/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useFullscreen\n\nmanages DOM full screen.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Image full screen\n\n<code src=\"./demo/demo2.tsx\" />\n\n### Page full screen\n\n<code src=\"./demo/demo3.tsx\" />\n\n### Coexist with other full screen operations\n\n<code src=\"./demo/demo4.tsx\" />\n\n## API\n\n```typescript\nconst [isFullscreen, {\n  enterFullscreen,\n  exitFullscreen,\n  toggleFullscreen,\n  isEnabled,\n}] = useFullScreen(\n  target,\n  options?: Options\n);\n```\n\n### Params\n\n| Property | Description        | Type                                                        | Default |\n| -------- | ------------------ | ----------------------------------------------------------- | ------- |\n| target   | DOM element or ref | `Element` \\| `() => Element` \\| `MutableRefObject<Element>` | -       |\n| options  | Setting            | `Options`                                                   | -       |\n\n### Options\n\n| Property       | Description                                                                                                                   | Type                                                   | Default |\n| -------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ------- |\n| onExit         | Exit full screen trigger                                                                                                      | `() => void`                                           | -       |\n| onEnter        | Enter full screen trigger                                                                                                     | `() => void`                                           | -       |\n| pageFullscreen | Whether to enable full screen of page. If its type is object, it can set `className` and `z-index` of the full screen element | `boolean` \\| `{ className?: string, zIndex?: number }` | `false` |\n\n### Result\n\n| Property         | Description          | Type         |\n| ---------------- | -------------------- | ------------ |\n| isFullscreen     | Is full screen       | `boolean`    |\n| enterFullscreen  | Enter full screen    | `() => void` |\n| exitFullscreen   | Exit full screen     | `() => void` |\n| toggleFullscreen | Toggle full screen   | `() => void` |\n| isEnabled        | Is enable screenfull | `boolean`    |\n"
  },
  {
    "path": "packages/hooks/src/useFullscreen/index.ts",
    "content": "import { useEffect, useState, useRef } from 'react';\nimport screenfull from 'screenfull';\nimport useLatest from '../useLatest';\nimport useMemoizedFn from '../useMemoizedFn';\nimport type { BasicTarget } from '../utils/domTarget';\nimport { getTargetElement } from '../utils/domTarget';\nimport { isBoolean } from '../utils';\n\nexport interface PageFullscreenOptions {\n  className?: string;\n  zIndex?: number;\n}\n\nexport interface Options {\n  onExit?: () => void;\n  onEnter?: () => void;\n  pageFullscreen?: boolean | PageFullscreenOptions;\n}\n\nconst useFullscreen = (target: BasicTarget, options?: Options) => {\n  const { onExit, onEnter, pageFullscreen = false } = options || {};\n  const { className = 'ahooks-page-fullscreen', zIndex = 999999 } =\n    isBoolean(pageFullscreen) || !pageFullscreen ? {} : pageFullscreen;\n\n  const onExitRef = useLatest(onExit);\n  const onEnterRef = useLatest(onEnter);\n\n  // The state of full screen may be changed by other scripts/components,\n  // so the initial value needs to be computed dynamically.\n  const [state, setState] = useState(getIsFullscreen);\n  const stateRef = useRef(getIsFullscreen());\n\n  function getIsFullscreen() {\n    return (\n      screenfull.isEnabled &&\n      !!screenfull.element &&\n      screenfull.element === getTargetElement(target)\n    );\n  }\n\n  const invokeCallback = (fullscreen: boolean) => {\n    if (fullscreen) {\n      onEnterRef.current?.();\n    } else {\n      onExitRef.current?.();\n    }\n  };\n\n  const updateFullscreenState = (fullscreen: boolean) => {\n    // Prevent repeated calls when the state is not changed.\n    if (stateRef.current !== fullscreen) {\n      invokeCallback(fullscreen);\n      setState(fullscreen);\n      stateRef.current = fullscreen;\n    }\n  };\n\n  const onScreenfullChange = () => {\n    const fullscreen = getIsFullscreen();\n\n    updateFullscreenState(fullscreen);\n  };\n\n  const togglePageFullscreen = (fullscreen: boolean) => {\n    const el = getTargetElement(target);\n    if (!el) {\n      return;\n    }\n\n    let styleElem = document.getElementById(className);\n\n    if (fullscreen) {\n      el.classList.add(className);\n\n      if (!styleElem) {\n        styleElem = document.createElement('style');\n        styleElem.setAttribute('id', className);\n        styleElem.textContent = `\n          .${className} {\n            position: fixed; left: 0; top: 0; right: 0; bottom: 0;\n            width: 100% !important; height: 100% !important;\n            z-index: ${zIndex};\n          }`;\n        el.appendChild(styleElem);\n      }\n    } else {\n      el.classList.remove(className);\n\n      if (styleElem) {\n        styleElem.remove();\n      }\n    }\n\n    updateFullscreenState(fullscreen);\n  };\n\n  const enterFullscreen = () => {\n    const el = getTargetElement(target);\n    if (!el) {\n      return;\n    }\n\n    if (pageFullscreen) {\n      togglePageFullscreen(true);\n      return;\n    }\n    if (screenfull.isEnabled) {\n      try {\n        screenfull.request(el);\n      } catch (error) {\n        console.error(error);\n      }\n    }\n  };\n\n  const exitFullscreen = () => {\n    const el = getTargetElement(target);\n    if (!el) {\n      return;\n    }\n\n    if (pageFullscreen) {\n      togglePageFullscreen(false);\n      return;\n    }\n    if (screenfull.isEnabled && screenfull.element === el) {\n      screenfull.exit();\n    }\n  };\n\n  const toggleFullscreen = () => {\n    if (state) {\n      exitFullscreen();\n    } else {\n      enterFullscreen();\n    }\n  };\n\n  useEffect(() => {\n    if (!screenfull.isEnabled || pageFullscreen) {\n      return;\n    }\n\n    screenfull.on('change', onScreenfullChange);\n\n    return () => {\n      screenfull.off('change', onScreenfullChange);\n    };\n  }, []);\n\n  return [\n    state,\n    {\n      enterFullscreen: useMemoizedFn(enterFullscreen),\n      exitFullscreen: useMemoizedFn(exitFullscreen),\n      toggleFullscreen: useMemoizedFn(toggleFullscreen),\n      isEnabled: screenfull.isEnabled,\n    },\n  ] as const;\n};\n\nexport default useFullscreen;\n"
  },
  {
    "path": "packages/hooks/src/useFullscreen/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useFullscreen\n\n管理 DOM 全屏的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 图片全屏\n\n<code src=\"./demo/demo2.tsx\" />\n\n### 页面全屏\n\n<code src=\"./demo/demo3.tsx\" />\n\n### 与其它全屏操作共存\n\n<code src=\"./demo/demo4.tsx\" />\n\n## API\n\n```typescript\nconst [isFullscreen, {\n  enterFullscreen,\n  exitFullscreen,\n  toggleFullscreen,\n  isEnabled,\n}] = useFullscreen(\n  target,\n  options?: Options\n);\n```\n\n### Params\n\n| 参数    | 说明             | 类型                                                        | 默认值 |\n| ------- | ---------------- | ----------------------------------------------------------- | ------ |\n| target  | DOM 节点或者 ref | `Element` \\| `() => Element` \\| `MutableRefObject<Element>` | -      |\n| options | 设置             | `Options`                                                   | -      |\n\n### Options\n\n| 参数           | 说明                                                                   | 类型                                                   | 默认值  |\n| -------------- | ---------------------------------------------------------------------- | ------------------------------------------------------ | ------- |\n| onExit         | 退出全屏触发                                                           | `() => void`                                           | -       |\n| onEnter        | 全屏触发                                                               | `() => void`                                           | -       |\n| pageFullscreen | 是否是页面全屏。当参数类型为对象时，可以设置全屏元素的类名和 `z-index` | `boolean` \\| `{ className?: string, zIndex?: number }` | `false` |\n\n### Result\n\n| 参数             | 说明         | 类型         |\n| ---------------- | ------------ | ------------ |\n| isFullscreen     | 是否全屏     | `boolean`    |\n| enterFullscreen  | 设置全屏     | `() => void` |\n| exitFullscreen   | 退出全屏     | `() => void` |\n| toggleFullscreen | 切换全屏     | `() => void` |\n| isEnabled        | 是否支持全屏 | `boolean`    |\n"
  },
  {
    "path": "packages/hooks/src/useFusionTable/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';\nimport { sleep } from '../../utils/testingHelpers';\nimport useFusionTable from '../index';\n\ntype Result = {\n  total: number;\n  list: any[];\n};\n\nlet count = 0;\nconst total = 40;\nconst getTableData = async ({\n  current,\n  pageSize,\n}: {\n  current: number;\n  pageSize: number;\n}): Promise<Result> => {\n  if (count * current >= total) {\n    return {\n      total,\n      list: [],\n    };\n  }\n  await sleep(1000);\n  count++;\n  const list = new Array(pageSize).fill(1).map((item, i) => {\n    const index = current * pageSize + i;\n    return {\n      id: index,\n      name: 'test',\n    };\n  });\n\n  return {\n    total,\n    list,\n  };\n};\n\nlet values = {};\nconst mockField = {\n  getNames() {\n    return [];\n  },\n  setValues(v: any) {\n    values = v;\n  },\n  getValues() {\n    return values;\n  },\n  resetToDefault() {\n    values = {};\n  },\n  validate(names: any, callback: (err: any, values: any) => void) {\n    callback(null, values);\n  },\n};\n\nconst setup = (service: any, options: any = {}) =>\n  renderHook(() => useFusionTable(service, options));\n\ndescribe('useFusionTable', () => {\n  beforeEach(() => {\n    count = 0;\n    values = {};\n  });\n\n  beforeAll(() => {\n    vi.useFakeTimers();\n  });\n\n  afterAll(() => {\n    vi.useRealTimers();\n  });\n\n  test('should get table & pagination props', async () => {\n    const { result } = setup(getTableData);\n    await act(async () => {\n      vi.runAllTimers();\n    });\n    expect(result.current.tableProps.loading).toBe(true);\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current.tableProps.loading).toBe(false);\n    expect(result.current.tableProps.dataSource).toHaveLength(10);\n    expect(result.current.paginationProps.current).toBe(1);\n    expect(result.current.paginationProps.total).toBe(total);\n  });\n\n  test('should get table data when page change', async () => {\n    const { result } = setup(getTableData);\n    const current = 2;\n    await act(async () => {\n      vi.runAllTimers();\n    });\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    act(() => {\n      result.current.paginationProps.onChange(current);\n    });\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current.paginationProps.current).toBe(current);\n    expect(result.current.paginationProps.total).toBe(total);\n  });\n\n  test('search should work when set field instance', async () => {\n    const { result } = setup(getTableData, { field: mockField });\n    mockField.setValues({\n      name: 'ahooks',\n    });\n\n    result.current.search.submit();\n    await act(async () => {\n      vi.runAllTimers();\n    });\n    expect(result.current.loading).toBe(true);\n    expect(result.current.params[1]).toMatchObject({ name: 'ahooks' });\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current.loading).toBe(false);\n\n    result.current.search.reset();\n    expect(result.current.params[1]).toMatchObject({});\n    await act(async () => {\n      vi.runAllTimers();\n    });\n    expect(result.current.loading).toBe(true);\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current.loading).toBe(false);\n  });\n\n  test('defaultParams should be work', async () => {\n    const { result } = setup(getTableData, {\n      defaultParams: [{ current: 2, pageSize: 20 }],\n    });\n    await act(async () => {\n      vi.runAllTimers();\n    });\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current.tableProps.dataSource).toHaveLength(20);\n    expect(result.current.paginationProps.current).toBe(2);\n  });\n\n  test('cache should be work', async () => {\n    const options = {\n      field: mockField,\n      cacheKey: 'cache',\n      defaultParams: [\n        {\n          current: 2,\n          pageSize: 5,\n        },\n        { name: 'hello', phone: '123' },\n      ],\n    };\n    const hook = setup(getTableData, options);\n    await act(async () => {\n      vi.runAllTimers();\n    });\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n\n    expect(hook.result.current.tableProps.dataSource).toHaveLength(5);\n    expect(hook.result.current.loading).toBe(false);\n\n    hook.unmount();\n    const hook2 = setup(getTableData, options);\n    await act(async () => {\n      vi.runAllTimers();\n    });\n    expect(hook2.result.current.loading).toBe(false);\n    expect(hook2.result.current.tableProps.dataSource).toHaveLength(5);\n  });\n\n  test('onSort should be work', async () => {\n    const { result } = setup(getTableData);\n    act(() => {\n      result.current.tableProps.onSort('dataIndex', 'asc');\n    });\n    expect(result.current.loading).toBe(true);\n    expect(result.current.params[0]?.sorter).toMatchObject({ field: 'dataIndex', order: 'asc' });\n  });\n\n  test('onFilter should be work', async () => {\n    const { result } = setup(getTableData);\n    const filterParams = {\n      version: 3,\n    };\n    act(() => {\n      result.current.tableProps.onFilter(filterParams);\n    });\n    expect(result.current.loading).toBe(true);\n    expect(result.current.params[0]?.filters).toMatchObject(filterParams);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useFusionTable/demo/cache.tsx",
    "content": "import { useState } from 'react';\nimport { Table, Pagination, Field, Form, Input, Button } from '@alifd/next';\nimport { useFusionTable } from 'ahooks';\nimport ReactJson from 'react-json-view';\n\ninterface Item {\n  name: {\n    last: string;\n  };\n  email: string;\n  phone: string;\n  gender: 'male' | 'female';\n}\n\ninterface Result {\n  total: number;\n  list: Item[];\n}\n\nconst getTableData = async (\n  {\n    current,\n    pageSize,\n    filters,\n    sorter,\n  }: { current: number; pageSize: number; filters: any; sorter: any },\n  formData: Object,\n): Promise<Result> => {\n  console.log(sorter, filters);\n\n  let query = `page=${current}&size=${pageSize}`;\n  Object.entries(formData).forEach(([key, value]) => {\n    if (value) {\n      query += `&${key}=${value}`;\n    }\n  });\n\n  return fetch(`https://randomuser.me/api?results=${pageSize}&${query}`)\n    .then((res) => res.json())\n    .then((res) => ({\n      total: 55,\n      list: res.results,\n    }));\n};\n\nconst AppList = () => {\n  const field = Field.useField({} as any);\n\n  const { tableProps, paginationProps, params, search } = useFusionTable(getTableData, {\n    defaultPageSize: 5,\n    field,\n    cacheKey: 'tableProps',\n  });\n\n  const { filters = {} } = params[0] || {};\n  const { type, changeType, submit, reset } = search || {};\n\n  const searchFrom = (\n    <div style={{ marginBottom: 16 }}>\n      <Form\n        field={field}\n        inline\n        style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}\n      >\n        <Form.Item label=\"name:\">\n          <Input name=\"name\" placeholder=\"name\" />\n        </Form.Item>\n        {type === 'advance' && (\n          <>\n            <Form.Item label=\"email:\">\n              <Input name=\"email\" placeholder=\"email\" />\n            </Form.Item>\n            <Form.Item label=\"phone:\">\n              <Input name=\"phone\" placeholder=\"phone\" />\n            </Form.Item>\n          </>\n        )}\n        <Form.Item label=\" \">\n          <Form.Submit type=\"primary\" onClick={submit}>\n            Search\n          </Form.Submit>\n        </Form.Item>\n\n        <Form.Item label=\" \">\n          <Button onClick={reset}>reset</Button>\n        </Form.Item>\n\n        <Form.Item label=\" \">\n          <Button text type=\"primary\" onClick={changeType}>\n            {type === 'simple' ? 'Expand' : 'Close'}\n          </Button>\n        </Form.Item>\n      </Form>\n    </div>\n  );\n\n  return (\n    <div>\n      {searchFrom}\n\n      <Table {...tableProps} filterParams={filters} primaryKey=\"email\">\n        <Table.Column title=\"name\" dataIndex=\"name.last\" width={140} />\n        <Table.Column title=\"email\" dataIndex=\"email\" width={500} />\n        <Table.Column title=\"phone\" sortable dataIndex=\"phone\" width={500} />\n        <Table.Column\n          title=\"gender\"\n          filters={[\n            { label: 'male', value: 'male' },\n            { label: 'female', value: 'female' },\n          ]}\n          dataIndex=\"gender\"\n          width={500}\n        />\n      </Table>\n      <Pagination style={{ marginTop: 16 }} {...paginationProps} />\n      <div style={{ background: '#f5f5f5', padding: 8, marginTop: 16 }}>\n        <p>Current Table:</p>\n        <ReactJson src={params[0]!} collapsed={2} />\n        <p>Current Form:</p>\n        <ReactJson src={params[1]!} collapsed={2} />\n      </div>\n    </div>\n  );\n};\n\nconst Demo = () => {\n  const [show, setShow] = useState(true);\n\n  return (\n    <div>\n      <Button\n        type=\"primary\"\n        warning\n        onClick={() => {\n          setShow(!show);\n        }}\n        style={{ marginBottom: 16 }}\n      >\n        {show ? 'Click to destroy' : 'Click recovery'}\n      </Button>\n      {show && <AppList />}\n    </div>\n  );\n};\n\nexport default Demo;\n"
  },
  {
    "path": "packages/hooks/src/useFusionTable/demo/form.tsx",
    "content": "import { Table, Pagination, Field, Form, Input, Button, Icon } from '@alifd/next';\nimport { useFusionTable } from 'ahooks';\nimport ReactJson from 'react-json-view';\n\ninterface Item {\n  name: {\n    last: string;\n  };\n  email: string;\n  phone: string;\n  gender: 'male' | 'female';\n}\n\ninterface Result {\n  total: number;\n  list: Item[];\n}\n\nconst getTableData = (\n  { current, pageSize }: { current: number; pageSize: number },\n  formData: Object,\n): Promise<Result> => {\n  let query = `page=${current}&size=${pageSize}`;\n  Object.entries(formData).forEach(([key, value]) => {\n    if (value) {\n      query += `&${key}=${value}`;\n    }\n  });\n\n  return fetch(`https://randomuser.me/api?results=${pageSize}&${query}`)\n    .then((res) => res.json())\n    .then((res) => ({\n      total: 55,\n      list: res.results.slice(0, 10),\n    }));\n};\n\nconst AppList = () => {\n  const field = Field.useField({} as any);\n  const { paginationProps, tableProps, search, loading, params } = useFusionTable(getTableData, {\n    field,\n  });\n  const { type, changeType, submit, reset } = search;\n\n  const advanceSearchForm = (\n    <div>\n      <Form\n        inline\n        style={{\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'flex-end',\n        }}\n        field={field}\n      >\n        <Form.Item label=\"name:\">\n          <Input name=\"name\" placeholder=\"name\" />\n        </Form.Item>\n        <Form.Item label=\"email:\">\n          <Input name=\"email\" placeholder=\"email\" />\n        </Form.Item>\n        <Form.Item label=\"phone:\">\n          <Input name=\"phone\" placeholder=\"phone\" />\n        </Form.Item>\n\n        <Form.Item label=\" \">\n          <Form.Submit loading={loading} type=\"primary\" onClick={submit}>\n            Search\n          </Form.Submit>\n        </Form.Item>\n\n        <Form.Item label=\" \">\n          <Button onClick={reset}>reset</Button>\n        </Form.Item>\n\n        <Form.Item label=\" \">\n          <Button text type=\"primary\" onClick={changeType}>\n            Simple Search\n          </Button>\n        </Form.Item>\n      </Form>\n    </div>\n  );\n\n  const searchForm = (\n    <div>\n      <Form\n        inline\n        style={{\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'flex-end',\n        }}\n        field={field}\n      >\n        <Form.Item label=\" \">\n          <Input\n            name=\"name\"\n            innerAfter={<Icon type=\"search\" size=\"xs\" onClick={submit} style={{ margin: 4 }} />}\n            placeholder=\"enter name\"\n            onPressEnter={submit}\n          />\n        </Form.Item>\n\n        <Form.Item label=\" \">\n          <Button text type=\"primary\" onClick={changeType}>\n            Advanced Search\n          </Button>\n        </Form.Item>\n      </Form>\n    </div>\n  );\n\n  return (\n    <>\n      {type === 'simple' ? searchForm : advanceSearchForm}\n      <Table {...tableProps} primaryKey=\"email\">\n        <Table.Column title=\"name\" dataIndex=\"name.last\" width={140} />\n        <Table.Column title=\"email\" dataIndex=\"email\" width={500} />\n        <Table.Column title=\"phone\" dataIndex=\"phone\" width={500} />\n        <Table.Column title=\"gender\" dataIndex=\"gender\" width={500} />\n      </Table>\n      <Pagination style={{ marginTop: 16 }} {...paginationProps} />\n      <div style={{ background: '#f5f5f5', padding: 8, marginTop: 16 }}>\n        <p>Current Table:</p>\n        <ReactJson src={params[0]!} collapsed={2} />\n        <p>Current Form:</p>\n        <ReactJson src={params[1]!} collapsed={2} />\n      </div>\n    </>\n  );\n};\n\nexport default AppList;\n"
  },
  {
    "path": "packages/hooks/src/useFusionTable/demo/init.tsx",
    "content": "import { Button, Field, Form, Icon, Input, Pagination, Select, Table } from '@alifd/next';\nimport { useFusionTable } from 'ahooks';\nimport ReactJson from 'react-json-view';\n\ninterface Item {\n  name: {\n    last: string;\n  };\n  email: string;\n  phone: string;\n  gender: 'male' | 'female';\n}\n\ninterface Result {\n  total: number;\n  list: Item[];\n}\n\nconst getTableData = (\n  { current, pageSize }: { current: number; pageSize: number },\n  formData: Object,\n): Promise<Result> => {\n  let query = `page=${current}&size=${pageSize}`;\n  Object.entries(formData).forEach(([key, value]) => {\n    if (value) {\n      query += `&${key}=${value}`;\n    }\n  });\n\n  return fetch(`https://randomuser.me/api?results=${pageSize}&${query}`)\n    .then((res) => res.json())\n    .then((res) => ({\n      total: 55,\n      list: res.results.slice(0, 10),\n    }));\n};\n\nconst AppList = () => {\n  const field = Field.useField({} as any);\n  const { paginationProps, tableProps, search, loading, params } = useFusionTable(getTableData, {\n    field,\n    defaultParams: [\n      { current: 2, pageSize: 5 },\n      { name: 'hello', email: 'abc@gmail.com', gender: 'female' },\n    ],\n    defaultType: 'advance',\n  });\n  const { type, changeType, submit, reset } = search;\n\n  const advanceSearchForm = (\n    <div>\n      <Form\n        inline\n        style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}\n        field={field}\n      >\n        <Form.Item label=\"name:\">\n          <Input name=\"name\" placeholder=\"name\" />\n        </Form.Item>\n        <Form.Item label=\"email:\">\n          <Input name=\"email\" placeholder=\"email\" />\n        </Form.Item>\n        <Form.Item label=\"phone:\">\n          <Input name=\"phone\" placeholder=\"phone\" />\n        </Form.Item>\n\n        <Form.Item label=\" \">\n          <Form.Submit loading={loading} type=\"primary\" onClick={submit}>\n            Search\n          </Form.Submit>\n        </Form.Item>\n\n        <Form.Item label=\" \">\n          <Button onClick={reset}>reset</Button>\n        </Form.Item>\n\n        <Form.Item label=\" \">\n          <Button text type=\"primary\" onClick={changeType}>\n            Simple Search\n          </Button>\n        </Form.Item>\n      </Form>\n    </div>\n  );\n\n  const searchForm = (\n    <div>\n      <Form\n        inline\n        style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}\n        field={field}\n      >\n        <Form.Item label=\" \">\n          <Select name=\"gender\" defaultValue=\"all\" onChange={submit}>\n            <Select.Option value=\"all\">all</Select.Option>\n            <Select.Option value=\"male\">male</Select.Option>\n            <Select.Option value=\"female\">female</Select.Option>\n          </Select>\n        </Form.Item>\n\n        <Form.Item label=\" \">\n          <Input\n            name=\"name\"\n            innerAfter={<Icon type=\"search\" size=\"xs\" onClick={submit} style={{ margin: 4 }} />}\n            placeholder=\"enter name\"\n            onPressEnter={submit}\n          />\n        </Form.Item>\n\n        <Form.Item label=\" \">\n          <Button text type=\"primary\" onClick={changeType}>\n            Advanced Search\n          </Button>\n        </Form.Item>\n      </Form>\n    </div>\n  );\n\n  return (\n    <>\n      {type === 'simple' ? searchForm : advanceSearchForm}\n      <Table {...tableProps} primaryKey=\"email\">\n        <Table.Column title=\"name\" dataIndex=\"name.last\" width={140} />\n        <Table.Column title=\"email\" dataIndex=\"email\" width={500} />\n        <Table.Column title=\"phone\" dataIndex=\"phone\" width={500} />\n        <Table.Column title=\"gender\" dataIndex=\"gender\" width={500} />\n      </Table>\n      <Pagination style={{ marginTop: 16 }} {...paginationProps} />\n      <div style={{ background: '#f5f5f5', padding: 8, marginTop: 16 }}>\n        <p>Current Table:</p>\n        <ReactJson src={params[0]!} collapsed={2} />\n        <p>Current Form:</p>\n        <ReactJson src={params[1]!} collapsed={2} />\n      </div>\n    </>\n  );\n};\n\nexport default AppList;\n"
  },
  {
    "path": "packages/hooks/src/useFusionTable/demo/table.tsx",
    "content": "import { Pagination, Table } from '@alifd/next';\nimport { useFusionTable } from 'ahooks';\n\ninterface Item {\n  name: {\n    last: string;\n  };\n  email: string;\n  phone: string;\n  gender: 'male' | 'female';\n}\n\ninterface Result {\n  total: number;\n  list: Item[];\n}\n\nconst getTableData = ({\n  current,\n  pageSize,\n}: {\n  current: number;\n  pageSize: number;\n}): Promise<Result> => {\n  const query = `page=${current}&size=${pageSize}`;\n\n  return fetch(`https://randomuser.me/api?results=${pageSize}&${query}`)\n    .then((res) => res.json())\n    .then((res) => ({\n      total: 55,\n      list: res.results.slice(0, 10),\n    }));\n};\n\nconst AppList = () => {\n  const { paginationProps, tableProps } = useFusionTable(getTableData);\n\n  return (\n    <>\n      <Table {...tableProps} primaryKey=\"email\">\n        <Table.Column title=\"name\" dataIndex=\"name.last\" width={140} />\n        <Table.Column title=\"email\" dataIndex=\"email\" width={500} />\n        <Table.Column title=\"phone\" dataIndex=\"phone\" width={500} />\n        <Table.Column title=\"gender\" dataIndex=\"gender\" width={500} />\n      </Table>\n      <Pagination style={{ marginTop: 16 }} {...paginationProps} />\n    </>\n  );\n};\n\nexport default AppList;\n"
  },
  {
    "path": "packages/hooks/src/useFusionTable/demo/validate.tsx",
    "content": "import { Table, Pagination, Field, Form, Input, Icon } from '@alifd/next';\nimport { useFusionTable } from 'ahooks';\nimport ReactJson from 'react-json-view';\n\ninterface Item {\n  name: {\n    last: string;\n  };\n  email: string;\n  phone: string;\n  gender: 'male' | 'female';\n}\n\ninterface Result {\n  total: number;\n  list: Item[];\n}\n\nconst getTableData = (\n  { current, pageSize }: { current: number; pageSize: number },\n  formData: Object,\n): Promise<Result> => {\n  let query = `page=${current}&size=${pageSize}`;\n  Object.entries(formData).forEach(([key, value]) => {\n    if (value) {\n      query += `&${key}=${value}`;\n    }\n  });\n\n  return fetch(`https://randomuser.me/api?results=${pageSize}&${query}`)\n    .then((res) => res.json())\n    .then((res) => ({\n      total: 55,\n      list: res.results.slice(0, 10),\n    }));\n};\n\nconst AppList = () => {\n  const field = Field.useField({} as any);\n  const { paginationProps, tableProps, search, params } = useFusionTable(getTableData, {\n    field,\n    defaultParams: [{ current: 1, pageSize: 10 }, { name: 'hello' }],\n  });\n  const { submit } = search;\n\n  const searchForm = (\n    <div>\n      <Form\n        inline\n        style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}\n        field={field}\n      >\n        <Form.Item label=\" \">\n          <Input\n            name=\"name\"\n            innerAfter={<Icon type=\"search\" size=\"xs\" onClick={submit} style={{ margin: 4 }} />}\n            placeholder=\"enter name\"\n            onPressEnter={submit}\n            {...field.init('name', { rules: [{ required: true }] })}\n          />\n        </Form.Item>\n      </Form>\n    </div>\n  );\n\n  return (\n    <>\n      {searchForm}\n      <Table {...tableProps} primaryKey=\"email\">\n        <Table.Column title=\"name\" dataIndex=\"name.last\" width={140} />\n        <Table.Column title=\"email\" dataIndex=\"email\" width={500} />\n        <Table.Column title=\"phone\" dataIndex=\"phone\" width={500} />\n        <Table.Column title=\"gender\" dataIndex=\"gender\" width={500} />\n      </Table>\n      <Pagination style={{ marginTop: 16 }} {...paginationProps} />\n      <div style={{ background: '#f5f5f5', padding: 8, marginTop: 16 }}>\n        <p>Current Table:</p>\n        <ReactJson src={params[0]!} collapsed={2} />\n        <p>Current Form:</p>\n        <ReactJson src={params[1]!} collapsed={2} />\n      </div>\n    </>\n  );\n};\n\nexport default AppList;\n"
  },
  {
    "path": "packages/hooks/src/useFusionTable/fusionAdapter.ts",
    "content": "import type { AntdFormUtils } from '../useAntdTable/types';\nimport type { Field } from './types';\n\nexport const fieldAdapter = (field: Field) =>\n  ({\n    getFieldInstance: (name: string) => field.getNames().includes(name),\n    setFieldsValue: field.setValues,\n    getFieldsValue: field.getValues,\n    resetFields: field.resetToDefault,\n    validateFields: (fields, callback) => {\n      field.validate(fields, callback);\n    },\n  }) as AntdFormUtils;\n\nexport const resultAdapter = (result: any) => {\n  const tableProps = {\n    dataSource: result.tableProps.dataSource,\n    loading: result.tableProps.loading,\n    onSort: (dataIndex: string, order: string) => {\n      result.tableProps.onChange(\n        { current: result.pagination.current, pageSize: result.pagination.pageSize },\n        result.params[0]?.filters,\n        {\n          field: dataIndex,\n          order,\n        },\n      );\n    },\n    onFilter: (filterParams: Record<string, any>) => {\n      result.tableProps.onChange(\n        { current: result.pagination.current, pageSize: result.pagination.pageSize },\n        filterParams,\n        result.params[0]?.sorter,\n      );\n    },\n  };\n\n  const paginationProps = {\n    onChange: result.pagination.changeCurrent,\n    onPageSizeChange: result.pagination.changePageSize,\n    current: result.pagination.current,\n    pageSize: result.pagination.pageSize,\n    total: result.pagination.total,\n  };\n\n  return {\n    ...result,\n    tableProps,\n    paginationProps,\n  };\n};\n"
  },
  {
    "path": "packages/hooks/src/useFusionTable/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useFusionTable\n\nuseFusionTable encapsulates the commonly used [Fusion Form](https://fusion.design/pc/component/basic/form) and [Fusion Table](https://fusion.design/pc/component/basic/table) data binding logic.\n\n`useFusionTable` is implemented based on `useRequest`. Before using it, you need to understand a few points that are different from `useRequest`:\n\n1. `service` receives two parameters, the first parameter is the paging data `{ current, pageSize, sorter, filters }`, and the second parameter is the form data.\n2. The data structure returned by `service` must be `{ total: number, list: Item[] }`.\n3. Additional `tableProps`、`paginationProps` and `search` fields will be returned to manage tables and forms.\n4. When `refreshDeps` changes, it will reset `current` to the first page and re-initiate the request.\n\n## Examples\n\n### Table management\n\n`useFusionTable` will automatically manage the pagination data of `Table`, you only need to pass the returned `tableProps` and `paginationProps` to the corresponding components.\n\n```tsx | pure\n<Table columns={columns} rowKey=\"email\" {...tableProps} />\n<Pagination {...paginationProps} />\n```\n\n<br />\n\n<code src=\"./demo/table.tsx\" />\n\n### Form and Table data binding\n\nWhen `useFusionTable` receives the `field` instance, it will return a search object to handle form related events.\n\n- `search.type` supports switching between `simple` and `advance`\n- `search.changeType`, switch form type\n- `search.submit` submit form\n- `search.reset` reset the current form\n\nIn the following example, you can experience the data binding between form and table.\n\n<code src=\"./demo/form.tsx\" />\n\n### Default Params\n\n`useFusionTable` sets the initial value through `defaultParams`, `defaultParams` is an array, the first item is paging related parameters, and the second item is form related data. If there is a second value, we will initialize the form for you!\n\nIt should be noted that the initial form data can be filled with all the form data of `simple` and `advance`, and we will help you select the form data of the currently activated type.\n\nThe following example sets paging data and form data during initialization.\n\n<code src=\"./demo/init.tsx\" />\n\n### Form Validation\n\nBefore the form is submitted, we will automatically validate the form data. If the verification fails, the request will not be initiated.\n\n<code src=\"./demo/validate.tsx\" />\n\n### Data Caching\n\nBy setting `cacheKey`, we can apply the data caching for the `Form` and `Table` .\n\n<code src=\"./demo/cache.tsx\" />\n\n## API\n\nAll parameters and returned results of `useRequest` are applicable to `useFusionTable`, so we won't repeat them here.\n\n```typescript\n\ntype Data = { total: number; list: any[] };\ntype Params = [{ current: number; pageSize: number, filter?: any, sorter?: any }, { [key: string]: any }];\n\nconst {\n  ...,\n  tableProps: {\n    dataSource: TData['list'];\n    loading: boolean;\n    onSort: (dataIndex: string, order: string) => void;\n    onFilter: (filterParams: any) => void;\n  };\n  paginationProps: {\n    onChange: (current: number) => void;\n    onPageSizeChange: (size: number) => void;\n    current: number;\n    pageSize: number;\n    total: number;\n  };\n  search: {\n    type: 'simple' | 'advance';\n    changeType: () => void;\n    submit: () => void;\n    reset: () => void;\n  };\n} = useFusionTable<TData extends Data, TParams extends Params>(\n  service: (...args: TParams) => Promise<TData>,\n  {\n    ...,\n    field?: any;\n    defaultType?: 'simple' | 'advance';\n    defaultParams?: TParams,\n    defaultPageSize?: number;\n    refreshDeps?: any[];\n  }\n);\n```\n\n### Result\n\n| Property          | Description                                     | Type                  |\n| ----------------- | ----------------------------------------------- | --------------------- |\n| tableProps        | The data required by the `Table` component      | -                     |\n| paginationProps   | The data required by the `Pagination` component | -                     |\n| search.type       | Current form type                               | `simple` \\| `advance` |\n| search.changeType | Switch form type                                | `() => void`          |\n| search.submit     | Submit form                                     | `() => void`          |\n| search.reset      | Reset the current form                          | `() => void`          |\n\n### Params\n\n| Property        | Description                                                                                | Type                     | Default  |\n| --------------- | ------------------------------------------------------------------------------------------ | ------------------------ | -------- |\n| field           | `Form` instance                                                                            | -                        | -        |\n| defaultType     | Default form type                                                                          | `simple` \\| `advance`    | `simple` |\n| defaultParams   | Default parameters, the first item is paging data, the second item is form data            | `[pagination, formData]` | -        |\n| defaultPageSize | Default page size                                                                          | `number`                 | `10`     |\n| refreshDeps     | Changes in `refreshDeps` will reset current to the first page and re-initiate the request. | `React.DependencyList`   | `[]`     |\n"
  },
  {
    "path": "packages/hooks/src/useFusionTable/index.tsx",
    "content": "import useAntdTable from '../useAntdTable';\nimport type { Data, Params, Service } from '../useAntdTable/types';\nimport { fieldAdapter, resultAdapter } from './fusionAdapter';\nimport type { FusionTableOptions, FusionTableResult } from './types';\n\nconst useFusionTable = <TData extends Data, TParams extends Params>(\n  service: Service<TData, TParams>,\n  options: FusionTableOptions<TData, TParams> = {},\n): FusionTableResult<TData, TParams> => {\n  const ret = useAntdTable<TData, TParams>(service, {\n    ...options,\n    form: options.field ? fieldAdapter(options.field) : undefined,\n  });\n\n  return resultAdapter(ret);\n};\n\nexport default useFusionTable;\n"
  },
  {
    "path": "packages/hooks/src/useFusionTable/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useFusionTable\n\n封装了常用的 [Fusion Form](https://fusion.design/pc/component/basic/form) 与 [Fusion Table](https://fusion.design/pc/component/basic/table) 联动逻辑。\n\n> 🌈「Table 场景解决方案」上线啦！点击图片查看常用的表格场景，区块代码一键下载到本地，快速复用! [使用文档](https://fusion.design/help.html#/dnzud5)\n\n[![](https://img.alicdn.com/tfs/TB1bEbWbQcx_u4jSZFlXXXnUFXa-2326-498.png)](https://fusion.design/pc/block?category=Table)\n\n`useFusionTable` 基于 `useRequest` 实现，在使用之前，你需要了解它与 `useRequest` 不同的几个点：\n\n1. `service` 接收两个参数，第一个参数为分页数据 `{ current, pageSize, sorter, filters }`，第二个参数为表单数据。\n2. `service` 返回的数据结构为 `{ total: number, list: Item[] }`。\n3. 会额外返回 `tableProps`、`paginationProps` 和 `search` 字段，管理表格和表单。\n4. `refreshDeps` 变化，会重置 `current` 到第一页，并重新发起请求。\n\n## 代码演示\n\n### Table 管理\n\n`useFusionTable` 会自动管理 `Table` 分页数据，你只需要把返回的 `tableProps` 与 `paginationProps` 传递给相应组件即可。\n\n```tsx | pure\n<Table columns={columns} rowKey=\"email\" {...tableProps} />\n<Pagination {...paginationProps} />\n```\n\n<br />\n\n<code src=\"./demo/table.tsx\" />\n\n### Form 与 Table 联动\n\n`useFusionTable` 接收 `field` 实例后，会返回 search 对象，用来处理表单相关事件。\n\n- `search.type` 支持 `simple` 和 `advance` 两个表单切换\n- `search.changeType`，切换表单类型\n- `search.submit` 提交表单行为\n- `search.reset` 重置当前表单\n\n以下示例你可以体验表单与表格联动。\n\n<code src=\"./demo/form.tsx\" />\n\n### 初始化数据\n\n`useFusionTable` 通过 `defaultParams` 设置初始化值，`defaultParams` 是一个数组，第一项为分页相关参数，第二项为表单相关数据。如果有第二个值，我们会帮您初始化表单！\n\n需要注意的是，初始化的表单数据可以填写 `simple` 和 `advance` 全量的表单数据，我们会帮您挑选当前激活的类型中的表单数据。\n\n以下示例在初始化时设置了分页数据和表单数据。\n\n<code src=\"./demo/init.tsx\" />\n\n### 表单验证\n\n表单提交之前，我们会自动校验表单数据，如果验证不通过，则不会发起请求。\n\n<code src=\"./demo/validate.tsx\" />\n\n### 数据缓存\n\n通过设置 `cacheKey`，我们可以实现 `Form` 与 `Table` 数据缓存。\n\n<code src=\"./demo/cache.tsx\" />\n\n## API\n\n`useRequest` 所有参数和返回结果均适用于 `useFusionTable`，此处不再赘述。\n\n```typescript\n\ntype Data = { total: number; list: any[] };\ntype Params = [{ current: number; pageSize: number, filter?: any, sorter?: any }, { [key: string]: any }];\n\nconst {\n  ...,\n  tableProps: {\n    dataSource: TData['list'];\n    loading: boolean;\n    onSort: (dataIndex: string, order: string) => void;\n    onFilter: (filterParams: any) => void;\n  };\n  paginationProps: {\n    onChange: (current: number) => void;\n    onPageSizeChange: (size: number) => void;\n    current: number;\n    pageSize: number;\n    total: number;\n  };\n  search: {\n    type: 'simple' | 'advance';\n    changeType: () => void;\n    submit: () => void;\n    reset: () => void;\n  };\n} = useFusionTable<TData extends Data, TParams extends Params>(\n  service: (...args: TParams) => Promise<TData>,\n  {\n    ...,\n    field?: any;\n    defaultType?: 'simple' | 'advance';\n    defaultParams?: TParams,\n    defaultPageSize?: number;\n    refreshDeps?: any[];\n  }\n);\n```\n\n### Result\n\n| 参数              | 说明                                                          | 类型                  |\n| ----------------- | ------------------------------------------------------------- | --------------------- |\n| tableProps        | `Table` 组件需要的数据，直接透传给 `Table` 组件即可           | -                     |\n| paginationProps   | `Pagination` 组件需要的数据，直接透传给 `Pagination` 组件即可 | -                     |\n| search.type       | 当前表单类型                                                  | `simple` \\| `advance` |\n| search.changeType | 切换表单类型                                                  | `() => void`          |\n| search.submit     | 提交表单                                                      | `() => void`          |\n| search.reset      | 重置当前表单                                                  | `() => void`          |\n\n### Params\n\n| 参数            | 说明                                                          | 类型                     | 默认值   |\n| --------------- | ------------------------------------------------------------- | ------------------------ | -------- |\n| field           | `Form` 实例                                                   | -                        | -        |\n| defaultType     | 默认表单类型                                                  | `simple` \\| `advance`    | `simple` |\n| defaultParams   | 默认参数，第一项为分页数据，第二项为表单数据                  | `[pagination, formData]` | -        |\n| defaultPageSize | 默认分页数量                                                  | `number`                 | `10`     |\n| refreshDeps     | `refreshDeps` 变化，会重置 current 到第一页，并重新发起请求。 | `React.DependencyList`   | `[]`     |\n"
  },
  {
    "path": "packages/hooks/src/useFusionTable/types.ts",
    "content": "import type { AntdTableOptions, AntdTableResult, Data, Params } from '../useAntdTable/types';\n\nexport interface Field {\n  getFieldInstance?: (name: string) => Record<string, any>;\n  setValues: (value: Record<string, any>) => void;\n  getValues: (...args: any) => Record<string, any>;\n  reset: (...args: any) => void;\n  validate: (fields: any, callback: (errors: any, values: any) => void) => void;\n  [key: string]: any;\n}\n\nexport interface FusionTableResult<TData extends Data, TParams extends Params>\n  extends Omit<AntdTableResult<TData, TParams>, 'tableProps'> {\n  paginationProps: {\n    onChange: (current: number) => void;\n    onPageSizeChange: (size: number) => void;\n    current: number;\n    pageSize: number;\n    total: number;\n  };\n  tableProps: {\n    dataSource: TData['list'];\n    loading: boolean;\n    onSort: (dataIndex: string, order: string) => void;\n    onFilter: (filterParams: any) => void;\n  };\n  search: {\n    type: 'simple' | 'advance';\n    changeType: () => void;\n    submit: () => void;\n    reset: () => void;\n  };\n}\n\nexport interface FusionTableOptions<TData extends Data, TParams extends Params>\n  extends Omit<AntdTableOptions<TData, TParams>, 'form'> {\n  field?: Field;\n}\n"
  },
  {
    "path": "packages/hooks/src/useGetState/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useGetState from '../index';\n\ndescribe('useGetState', () => {\n  const setUp = <T>(initialValue: T) =>\n    renderHook(() => {\n      const [state, setState, getState] = useGetState<T>(initialValue);\n      return {\n        state,\n        setState,\n        getState,\n      } as const;\n    });\n\n  test('should support initialValue', () => {\n    const hook = setUp(() => 0);\n    expect(hook.result.current.state).toBe(0);\n  });\n\n  test('should support update', () => {\n    const hook = setUp(0);\n    act(() => {\n      hook.result.current.setState(1);\n    });\n    expect(hook.result.current.getState()).toBe(1);\n  });\n\n  test('should getState frozen', () => {\n    const hook = setUp(0);\n    const prevGetState = hook.result.current.getState;\n    act(() => {\n      hook.result.current.setState(1);\n    });\n    expect(hook.result.current.getState).toBe(prevGetState);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useGetState/demo/demo1.tsx",
    "content": "/**\n * title: Open console to view logs\n * desc: The counter prints the value every 3 seconds\n *\n * title.zh-CN: 打开控制台查看输出\n * desc.zh-CN: 计数器每 3 秒打印一次值\n */\n\nimport { useEffect } from 'react';\nimport { useGetState } from 'ahooks';\n\nexport default () => {\n  const [count, setCount, getCount] = useGetState<number>(0);\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      console.log('interval count', getCount());\n    }, 3000);\n\n    return () => {\n      clearInterval(interval);\n    };\n  }, []);\n\n  return <button onClick={() => setCount((count) => count + 1)}>count: {count}</button>;\n};\n"
  },
  {
    "path": "packages/hooks/src/useGetState/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useGetState\n\nAdd a getter method to the return value of `React.useState` to get the latest value\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## TypeScript definition\n\n```typescript\nimport { Dispatch, SetStateAction } from 'react';\ntype GetStateAction<S> = () => S;\n\nfunction useGetState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>, GetStateAction<S>];\nfunction useGetState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>, GetStateAction<S | undefined>];\n```\n\n## API\n\n```typescript\nconst [state, setState, getState] = useGetState<S>(initialState)\n```\n"
  },
  {
    "path": "packages/hooks/src/useGetState/index.ts",
    "content": "import type { Dispatch, SetStateAction } from 'react';\nimport { useState, useCallback } from 'react';\nimport useLatest from '../useLatest';\n\ntype GetStateAction<S> = () => S;\n\nfunction useGetState<S>(\n  initialState: S | (() => S),\n): [S, Dispatch<SetStateAction<S>>, GetStateAction<S>];\nfunction useGetState<S = undefined>(): [\n  S | undefined,\n  Dispatch<SetStateAction<S | undefined>>,\n  GetStateAction<S | undefined>,\n];\nfunction useGetState<S>(initialState?: S) {\n  const [state, setState] = useState(initialState);\n  const stateRef = useLatest(state);\n\n  const getState = useCallback(() => stateRef.current, []);\n\n  return [state, setState, getState];\n}\n\nexport default useGetState;\n"
  },
  {
    "path": "packages/hooks/src/useGetState/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useGetState\n\n给 `React.useState` 增加了一个 getter 方法，以获取当前最新值。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## 类型定义\n\n```typescript\nimport { Dispatch, SetStateAction } from 'react';\ntype GetStateAction<S> = () => S;\n\nfunction useGetState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>, GetStateAction<S>];\nfunction useGetState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>, GetStateAction<S | undefined>];\n```\n\n## API\n\n```typescript\nconst [state, setState, getState] = useGetState<S>(initialState)\n```\n"
  },
  {
    "path": "packages/hooks/src/useHistoryTravel/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useHistoryTravel from '../index';\n\ndescribe('useHistoryTravel', () => {\n  test('should work without initial value', async () => {\n    const hook = renderHook(() => useHistoryTravel());\n    expect(hook.result.current.value).toBeUndefined();\n    act(() => {\n      hook.result.current.setValue('test');\n    });\n    expect(hook.result.current.value).toBe('test');\n  });\n\n  test('should work with null and undefined without initial value', async () => {\n    const nullHook = renderHook(() => useHistoryTravel());\n    expect(nullHook.result.current.value).toBeUndefined();\n    act(() => {\n      nullHook.result.current.setValue(null);\n    });\n    expect(nullHook.result.current.value).toBeNull();\n\n    const undefHook = renderHook(() => useHistoryTravel());\n    expect(undefHook.result.current.value).toBeUndefined();\n    act(() => {\n      undefHook.result.current.setValue('def');\n    });\n    act(() => {\n      undefHook.result.current.setValue(undefined);\n    });\n    expect(undefHook.result.current.value).toBeUndefined();\n    expect(undefHook.result.current.backLength).toBe(2);\n  });\n\n  test('should work with initial value', async () => {\n    const hook = renderHook(() => useHistoryTravel('abc'));\n    expect(hook.result.current.value).toBe('abc');\n    act(() => {\n      hook.result.current.setValue('def');\n    });\n    expect(hook.result.current.value).toBe('def');\n  });\n\n  test('should work with null and undefined with initial value', async () => {\n    const nullHook = renderHook(() => useHistoryTravel<null | string>('abc'));\n    act(() => {\n      nullHook.result.current.setValue(null);\n    });\n    expect(nullHook.result.current.value).toBeNull();\n\n    const undefHook = renderHook(() => useHistoryTravel<undefined | string>('abc'));\n    act(() => {\n      undefHook.result.current.setValue(undefined);\n    });\n    expect(undefHook.result.current.value).toBeUndefined();\n    expect(undefHook.result.current.backLength).toBe(1);\n  });\n\n  test('back and forward should work', () => {\n    const hook = renderHook(() => useHistoryTravel());\n    act(() => {\n      hook.result.current.setValue('ddd');\n    });\n    act(() => {\n      hook.result.current.setValue('abc');\n    });\n    expect(hook.result.current.value).toBe('abc');\n    act(() => {\n      hook.result.current.setValue('def');\n    });\n    expect(hook.result.current.value).toBe('def');\n    act(() => {\n      hook.result.current.back();\n    });\n    expect(hook.result.current.value).toBe('abc');\n    act(() => {\n      hook.result.current.forward();\n    });\n    expect(hook.result.current.value).toBe('def');\n  });\n\n  test('go should work for negative step', () => {\n    const hook = renderHook(() => useHistoryTravel('init'));\n    act(() => {\n      hook.result.current.setValue('abc');\n    });\n    act(() => {\n      hook.result.current.setValue('def');\n    });\n    act(() => {\n      hook.result.current.setValue('hij');\n    });\n    act(() => {\n      hook.result.current.go(-2);\n    });\n    expect(hook.result.current.value).toBe('abc');\n    act(() => {\n      hook.result.current.go(-100);\n    });\n    expect(hook.result.current.value).toBe('init');\n  });\n\n  test('go should work for positive step', () => {\n    const hook = renderHook(() => useHistoryTravel('init'));\n    act(() => {\n      hook.result.current.setValue('abc');\n    });\n    act(() => {\n      hook.result.current.setValue('def');\n    });\n    act(() => {\n      hook.result.current.setValue('hij');\n    });\n    act(() => {\n      hook.result.current.go(-3);\n    });\n    expect(hook.result.current.value).toBe('init');\n    act(() => {\n      hook.result.current.go(2);\n    });\n    expect(hook.result.current.value).toBe('def');\n    act(() => {\n      hook.result.current.go(100);\n    });\n    expect(hook.result.current.value).toBe('hij');\n  });\n\n  test('reset should reset state to initial by default', () => {\n    const hook = renderHook(() => useHistoryTravel('init'));\n    act(() => {\n      hook.result.current.setValue('abc');\n    });\n    act(() => {\n      hook.result.current.setValue('def');\n    });\n    act(() => {\n      hook.result.current.setValue('hij');\n    });\n    act(() => {\n      hook.result.current.go(-1);\n    });\n    expect(hook.result.current.backLength).toBe(2);\n    expect(hook.result.current.forwardLength).toBe(1);\n    act(() => {\n      hook.result.current.reset();\n    });\n    expect(hook.result.current.value).toBe('init');\n    expect(hook.result.current.backLength).toBe(0);\n    expect(hook.result.current.forwardLength).toBe(0);\n  });\n\n  test('reset should reset state to new initial if provided', () => {\n    const hook = renderHook(() => useHistoryTravel('init'));\n    act(() => {\n      hook.result.current.setValue('abc');\n    });\n    act(() => {\n      hook.result.current.setValue('def');\n    });\n    act(() => {\n      hook.result.current.setValue('hij');\n    });\n    act(() => {\n      hook.result.current.go(-1);\n    });\n    expect(hook.result.current.backLength).toBe(2);\n    expect(hook.result.current.forwardLength).toBe(1);\n    act(() => {\n      hook.result.current.reset('new init');\n    });\n    expect(hook.result.current.value).toBe('new init');\n    expect(hook.result.current.backLength).toBe(0);\n    expect(hook.result.current.forwardLength).toBe(0);\n  });\n\n  test('reset new initial value should work with undefined', () => {\n    const hook = renderHook(() => useHistoryTravel('init'));\n    act(() => {\n      hook.result.current.setValue('abc');\n    });\n    act(() => {\n      hook.result.current.setValue('def');\n    });\n    act(() => {\n      hook.result.current.setValue('hij');\n    });\n    act(() => {\n      hook.result.current.go(-1);\n    });\n    expect(hook.result.current.backLength).toBe(2);\n    expect(hook.result.current.forwardLength).toBe(1);\n    act(() => {\n      hook.result.current.reset(undefined);\n    });\n    expect(hook.result.current.value).toBeUndefined();\n    expect(hook.result.current.backLength).toBe(0);\n    expect(hook.result.current.forwardLength).toBe(0);\n  });\n\n  test('should work without max length', async () => {\n    const hook = renderHook(() => useHistoryTravel());\n    expect(hook.result.current.backLength).toBe(0);\n    for (let i = 1; i <= 100; i++) {\n      act(() => {\n        hook.result.current.setValue(i);\n      });\n    }\n    expect(hook.result.current.forwardLength).toBe(0);\n    expect(hook.result.current.backLength).toBe(100);\n    expect(hook.result.current.value).toBe(100);\n  });\n\n  test('should work with max length', async () => {\n    const hook = renderHook(() => useHistoryTravel(0, 10));\n    expect(hook.result.current.backLength).toBe(0);\n    for (let i = 1; i <= 100; i++) {\n      act(() => {\n        hook.result.current.setValue(i);\n      });\n    }\n    expect(hook.result.current.forwardLength).toBe(0);\n    expect(hook.result.current.backLength).toBe(10);\n    expect(hook.result.current.value).toBe(100);\n\n    act(() => {\n      hook.result.current.go(-5);\n    });\n    expect(hook.result.current.forwardLength).toBe(5);\n    expect(hook.result.current.backLength).toBe(5);\n    expect(hook.result.current.value).toBe(95);\n\n    act(() => {\n      hook.result.current.go(5);\n    });\n    expect(hook.result.current.forwardLength).toBe(0);\n    expect(hook.result.current.backLength).toBe(10);\n    expect(hook.result.current.value).toBe(100);\n\n    act(() => {\n      hook.result.current.go(-50);\n    });\n    expect(hook.result.current.forwardLength).toBe(10);\n    expect(hook.result.current.backLength).toBe(0);\n    expect(hook.result.current.value).toBe(90);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useHistoryTravel/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Redo and undo operations，click back and forward after input something.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 撤销跟重做操作，输入内容后，点击 back 和 forward。\n */\n\nimport { useHistoryTravel } from 'ahooks';\nexport default () => {\n  const { value, setValue, backLength, forwardLength, back, forward } = useHistoryTravel<string>();\n\n  return (\n    <div>\n      <input value={value || ''} onChange={(e) => setValue(e.target.value)} />\n      <button disabled={backLength <= 0} onClick={back} style={{ margin: '0 8px' }}>\n        back\n      </button>\n      <button disabled={forwardLength <= 0} onClick={forward}>\n        forward\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useHistoryTravel/demo/demo2.tsx",
    "content": "/**\n * title: TodoList\n * desc: Redo and undo operations\n *\n * title.zh-CN: 可撤销恢复的 Todo List\n * desc.zh-CN: 可以实现撤销恢复等操作。\n */\n\nimport { useHistoryTravel } from 'ahooks';\nimport { useState } from 'react';\n\nexport default () => {\n  const {\n    value = [],\n    setValue,\n    backLength,\n    forwardLength,\n    back,\n    forward,\n    go,\n    reset,\n  } = useHistoryTravel(['do homework']);\n\n  const [inputValue, setInputValue] = useState('');\n  const [step, setStep] = useState(-1);\n\n  const onAdd = () => {\n    setValue([...value, inputValue]);\n    setInputValue('');\n  };\n\n  const onGo = () => {\n    go(step);\n    setStep(0);\n  };\n\n  const onReset = () => {\n    reset();\n    setStep(0);\n    setInputValue('');\n  };\n\n  return (\n    <div>\n      <div style={{ border: '1px solid #ebedf1', padding: 16, marginBottom: 16 }}>\n        <h3>TODO List</h3>\n        <ul>\n          {value.map((it, index) => (\n            <li key={index}>{it}</li>\n          ))}\n        </ul>\n      </div>\n      <div style={{ marginBottom: 16 }}>\n        <input\n          value={inputValue}\n          onChange={(e) => setInputValue(e.target.value)}\n          placeholder=\"Please enter TODO name\"\n          style={{ width: 200, marginRight: 8 }}\n        />\n        <button type=\"button\" onClick={onAdd} style={{ marginRight: 8 }}>\n          Add TODO\n        </button>\n        <button type=\"button\" disabled={backLength <= 0} onClick={back} style={{ marginRight: 8 }}>\n          Undo\n        </button>\n        <button\n          type=\"button\"\n          disabled={forwardLength <= 0}\n          onClick={forward}\n          style={{ marginRight: 8 }}\n        >\n          Redo\n        </button>\n        <button type=\"button\" disabled={!backLength && !forwardLength} onClick={onReset}>\n          Reset\n        </button>\n      </div>\n      <div>\n        <input\n          type=\"number\"\n          value={step}\n          onChange={(e) => setStep(e.target.value as any)}\n          max={forwardLength}\n          min={backLength * -1}\n          style={{ marginRight: 8, width: 60 }}\n        />\n        <button type=\"button\" onClick={onGo}>\n          Go\n        </button>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useHistoryTravel/demo/demo3.tsx",
    "content": "/**\n * title: Limit maximum history length\n * desc: Limit the maximum number of history records to avoid excessive memory consumption.\n *\n * title.zh-CN: 限制历史记录最大长度\n * desc.zh-CN: 限制最大历史记录数量,避免过度占用内存。\n */\n\nimport { useHistoryTravel } from 'ahooks';\nexport default () => {\n  const maxLength = 3;\n  const { value, setValue, backLength, forwardLength, back, forward } = useHistoryTravel<string>(\n    '',\n    maxLength,\n  );\n\n  return (\n    <div>\n      <div>maxLength: {maxLength}</div>\n      <div>backLength: {backLength}</div>\n      <div>forwardLength: {forwardLength}</div>\n      <input value={value || ''} onChange={(e) => setValue(e.target.value)} />\n      <button disabled={backLength <= 0} onClick={back} style={{ margin: '0 8px' }}>\n        back\n      </button>\n      <button disabled={forwardLength <= 0} onClick={forward}>\n        forward\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useHistoryTravel/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useHistoryTravel\n\nA hook to manage state change history. It provides encapsulation methods to travel through the history.\n\n## Examples\n\n### Basic usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Todo List\n\n<code src=\"./demo/demo2.tsx\" />\n\n### Limit maximum history length\n\n<code src=\"./demo/demo3.tsx\" />\n\n## API\n\n```typescript\nconst {\n  value,\n  setValue,\n  backLength,\n  forwardLength,\n  go,\n  back,\n  forward\n} = useHistoryTravel<T>(initialValue?: T, maxLength: number = 0 );\n```\n\n### Params\n\n| Property     | Description                                                                                                               | Type     | Default     |\n| ------------ | ------------------------------------------------------------------------------------------------------------------------- | -------- | ----------- |\n| initialValue | Optional initial value                                                                                                    | `T`      | -           |\n| maxLength    | Optional limit the maximum length of history records. If the maximum length is exceeded, the first record will be deleted | `number` | 0 unlimited |\n\n### Result\n\n| Property      | Description                                                                       | Type                            |\n| ------------- | --------------------------------------------------------------------------------- | ------------------------------- |\n| value         | Current value                                                                     | `T`                             |\n| setValue      | Set value                                                                         | `(value: T) => void`            |\n| backLength    | The length of backward history                                                    | `number`                        |\n| forwardLength | The length of forward history                                                     | `number`                        |\n| go            | Move between the history, move backward on step < 0，and move forward on step > 0 | `(step: number) => void`        |\n| back          | Move one step backward                                                            | `() => void`                    |\n| foward        | Move one step forward                                                             | `() => void`                    |\n| reset         | Reset history to initial value by default or provide a new initial value.         | `(newInitialValue?: T) => void` |\n"
  },
  {
    "path": "packages/hooks/src/useHistoryTravel/index.ts",
    "content": "import { useRef, useState } from 'react';\nimport useMemoizedFn from '../useMemoizedFn';\nimport { isNumber } from '../utils';\n\ninterface IData<T> {\n  present?: T;\n  past: T[];\n  future: T[];\n}\n\nconst dumpIndex = <T>(step: number, arr: T[]) => {\n  let index =\n    step > 0\n      ? step - 1 // move forward\n      : arr.length + step; // move backward\n  if (index >= arr.length - 1) {\n    index = arr.length - 1;\n  }\n  if (index < 0) {\n    index = 0;\n  }\n  return index;\n};\n\nconst split = <T>(step: number, targetArr: T[]) => {\n  const index = dumpIndex(step, targetArr);\n  return {\n    _current: targetArr[index],\n    _before: targetArr.slice(0, index),\n    _after: targetArr.slice(index + 1),\n  };\n};\n\nexport default function useHistoryTravel<T>(initialValue?: T, maxLength: number = 0) {\n  const [history, setHistory] = useState<IData<T | undefined>>({\n    present: initialValue,\n    past: [],\n    future: [],\n  });\n\n  const { present, past, future } = history;\n\n  const initialValueRef = useRef(initialValue);\n\n  const reset = (...params: any[]) => {\n    const _initial = params.length > 0 ? params[0] : initialValueRef.current;\n    initialValueRef.current = _initial;\n\n    setHistory({\n      present: _initial,\n      future: [],\n      past: [],\n    });\n  };\n\n  const updateValue = (val: T) => {\n    const _past = [...past, present];\n    const maxLengthNum = isNumber(maxLength) ? maxLength : Number(maxLength);\n    // maximum number of records exceeded\n    if (maxLengthNum > 0 && _past.length > maxLengthNum) {\n      //delete first\n      _past.splice(0, 1);\n    }\n\n    setHistory({\n      present: val,\n      future: [],\n      past: _past,\n    });\n  };\n\n  const _forward = (step: number = 1) => {\n    if (future.length === 0) {\n      return;\n    }\n    const { _before, _current, _after } = split(step, future);\n    setHistory({\n      past: [...past, present, ..._before],\n      present: _current,\n      future: _after,\n    });\n  };\n\n  const _backward = (step: number = -1) => {\n    if (past.length === 0) {\n      return;\n    }\n\n    const { _before, _current, _after } = split(step, past);\n    setHistory({\n      past: _before,\n      present: _current,\n      future: [..._after, present, ...future],\n    });\n  };\n\n  const go = (step: number) => {\n    const stepNum = isNumber(step) ? step : Number(step);\n    if (stepNum === 0) {\n      return;\n    }\n    if (stepNum > 0) {\n      return _forward(stepNum);\n    }\n    _backward(stepNum);\n  };\n\n  return {\n    value: present,\n    backLength: past.length,\n    forwardLength: future.length,\n    setValue: useMemoizedFn(updateValue),\n    go: useMemoizedFn(go),\n    back: useMemoizedFn(() => {\n      go(-1);\n    }),\n    forward: useMemoizedFn(() => {\n      go(1);\n    }),\n    reset: useMemoizedFn(reset),\n  };\n}\n"
  },
  {
    "path": "packages/hooks/src/useHistoryTravel/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useHistoryTravel\n\n管理状态历史变化记录，方便在历史记录中前进与后退。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 可撤销恢复的 Todo List\n\n<code src=\"./demo/demo2.tsx\" />\n\n### 限制历史记录最大长度\n\n<code src=\"./demo/demo3.tsx\" />\n\n## API\n\n```typescript\nconst {\n  value,\n  setValue,\n  backLength,\n  forwardLength,\n  go,\n  back,\n  forward\n} = useHistoryTravel<T>(initialValue?: T, maxLength: number = 0);\n```\n\n### Params\n\n| 参数         | 说明                                                      | 类型     | 默认值   |\n| ------------ | --------------------------------------------------------- | -------- | -------- |\n| initialValue | 可选，初始值                                              | `any`    | -        |\n| maxLength    | 可选，限制历史记录最大长度,超过最大长度后将删除第一个记录 | `number` | 0 不限制 |\n\n### Result\n\n| 参数          | 说明                                          | 类型                            |\n| ------------- | --------------------------------------------- | ------------------------------- |\n| value         | 当前值                                        | `T`                             |\n| setValue      | 设置 value                                    | `(value: T) => void`            |\n| backLength    | 可回退历史长度                                | `number`                        |\n| forwardLength | 可前进历史长度                                | `number`                        |\n| go            | 前进步数, step < 0 为后退， step > 0 时为前进 | `(step: number) => void`        |\n| back          | 向后回退一步                                  | `() => void`                    |\n| foward        | 向前前进一步                                  | `() => void`                    |\n| reset         | 重置到初始值，或提供一个新的初始值            | `(newInitialValue?: T) => void` |\n"
  },
  {
    "path": "packages/hooks/src/useHover/__tests__/index.spec.tsx",
    "content": "import { act, fireEvent, render, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useHover from '../index';\n\ndescribe('useHover', () => {\n  test('should work', () => {\n    const { getByText } = render(<button>Hover</button>);\n    let trigger = 0;\n    const { result } = renderHook(() =>\n      useHover(getByText('Hover'), {\n        onEnter: () => {\n          trigger++;\n        },\n        onLeave: () => {\n          trigger++;\n        },\n      }),\n    );\n\n    expect(result.current).toBe(false);\n\n    act(() => void fireEvent.mouseEnter(getByText('Hover')));\n    expect(result.current).toBe(true);\n    expect(trigger).toBe(1);\n\n    act(() => void fireEvent.mouseLeave(getByText('Hover')));\n    expect(result.current).toBe(false);\n    expect(trigger).toBe(2);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useHover/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Use ref to set element that needs monitoring.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 使用 ref 设置需要监听的元素。\n */\n\nimport { useRef } from 'react';\nimport { useHover } from 'ahooks';\n\nexport default () => {\n  const ref = useRef(null);\n  const isHovering = useHover(ref);\n  return <div ref={ref}>{isHovering ? 'hover' : 'leaveHover'}</div>;\n};\n"
  },
  {
    "path": "packages/hooks/src/useHover/demo/demo2.tsx",
    "content": "/**\n * title: Pass in DOM element\n * desc: Pass in a function that returns the DOM element.\n *\n * title.zh-CN: 传入 DOM 元素\n * desc.zh-CN: 传入 function 并返回一个 dom 元素。\n */\n\nimport { useHover } from 'ahooks';\n\nexport default () => {\n  const isHovering = useHover(() => document.getElementById('hover-div'), {\n    onEnter: () => {\n      console.log('onEnter');\n    },\n    onLeave: () => {\n      console.log('onLeave');\n    },\n    onChange: (isHover) => {\n      console.log('onChange', isHover);\n    },\n  });\n\n  return <div id=\"hover-div\">{isHovering ? 'hover' : 'leaveHover'}</div>;\n};\n"
  },
  {
    "path": "packages/hooks/src/useHover/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useHover\n\nA hook that tracks whether the element is being hovered.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Pass in DOM element\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```javascript\nconst isHovering = useHover(\n  target,\n  {\n   onEnter,\n   onLeave,\n   onChange\n  }\n);\n```\n\n### Params\n\n| Property | Description        | Type                                                        | Default |\n| -------- | ------------------ | ----------------------------------------------------------- | ------- |\n| target   | DOM element or ref | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -       |\n| options  | More config        | `Options`                                                   | -       |\n\n### Options\n\n| Property | Description                             | Type                            | Default |\n| -------- | --------------------------------------- | ------------------------------- | ------- |\n| onEnter  | Callback to be executed on mouse hover  | `() => void`                    | -       |\n| onLeave  | Callback to be executed on mouse leave  | `() => void`                    | -       |\n| onChange | Callback to be executed on hover change | `(isHovering: boolean) => void` | -       |\n\n### Result\n\n| Property   | Description                          | Type      |\n| ---------- | ------------------------------------ | --------- |\n| isHovering | Whether the element is being hovered | `boolean` |\n"
  },
  {
    "path": "packages/hooks/src/useHover/index.ts",
    "content": "import useBoolean from '../useBoolean';\nimport useEventListener from '../useEventListener';\nimport type { BasicTarget } from '../utils/domTarget';\n\nexport interface Options {\n  onEnter?: () => void;\n  onLeave?: () => void;\n  onChange?: (isHovering: boolean) => void;\n}\n\nexport default (target: BasicTarget, options?: Options): boolean => {\n  const { onEnter, onLeave, onChange } = options || {};\n\n  const [state, { setTrue, setFalse }] = useBoolean(false);\n\n  useEventListener(\n    'mouseenter',\n    () => {\n      onEnter?.();\n      setTrue();\n      onChange?.(true);\n    },\n    {\n      target,\n    },\n  );\n\n  useEventListener(\n    'mouseleave',\n    () => {\n      onLeave?.();\n      setFalse();\n      onChange?.(false);\n    },\n    {\n      target,\n    },\n  );\n\n  return state;\n};\n"
  },
  {
    "path": "packages/hooks/src/useHover/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useHover\n\n监听 DOM 元素是否有鼠标悬停。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 传入 DOM 元素\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```javascript\nconst isHovering = useHover(\n  target,\n  {\n   onEnter,\n   onLeave,\n   onChange\n  }\n);\n```\n\n### Params\n\n| 参数    | 说明                  | 类型                                                        | 默认值 |\n| ------- | --------------------- | ----------------------------------------------------------- | ------ |\n| target  | DOM 节点或者 Ref 对象 | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -      |\n| options | 额外的配置项          | `Options`                                                   | -      |\n\n### Options\n\n| 参数     | 说明                 | 类型                            | 默认值 |\n| -------- | -------------------- | ------------------------------- | ------ |\n| onEnter  | hover 时触发         | `() => void`                    | -      |\n| onLeave  | 取消 hover 时触发    | `() => void`                    | -      |\n| onChange | hover 状态变化时触发 | `(isHovering: boolean) => void` | -      |\n\n### Result\n\n| 参数       | 说明                   | 类型      |\n| ---------- | ---------------------- | --------- |\n| isHovering | 鼠标元素是否处于 hover | `boolean` |\n"
  },
  {
    "path": "packages/hooks/src/useInViewport/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\n\nimport useInViewport from '../index';\n\nconst targetEl = document.createElement('div');\ndocument.body.appendChild(targetEl);\n\nconst observe = vi.fn();\nconst disconnect = vi.fn();\n\nconst mockIntersectionObserver = vi.fn().mockReturnValue({\n  observe,\n  disconnect,\n});\n\nwindow.IntersectionObserver = mockIntersectionObserver;\n\ndescribe('useInViewport', () => {\n  test('should work when target is in viewport', async () => {\n    const { result } = renderHook(() => useInViewport(targetEl));\n    const calls = mockIntersectionObserver.mock.calls;\n    const [onChange] = calls[calls.length - 1];\n\n    act(() => {\n      onChange([\n        {\n          targetEl,\n          isIntersecting: true,\n          intersectionRatio: 0.5,\n        },\n      ]);\n    });\n\n    const [inViewport, ratio] = result.current;\n    expect(inViewport).toBeTruthy();\n    expect(ratio).toBe(0.5);\n  });\n\n  test('should work when target array is in viewport and has a callback', async () => {\n    const targetEls: HTMLDivElement[] = [];\n    const callback = vi.fn();\n    for (let i = 0; i < 2; i++) {\n      const target = document.createElement('div');\n      document.body.appendChild(target);\n      targetEls.push(target);\n    }\n\n    const getValue = (isIntersecting: any, intersectionRatio: any) => ({\n      isIntersecting,\n      intersectionRatio,\n    });\n\n    const { result } = renderHook(() => useInViewport(targetEls, { callback }));\n    const calls = mockIntersectionObserver.mock.calls;\n    const [observerCallback] = calls[calls.length - 1];\n\n    const target = getValue(false, 0);\n    act(() => observerCallback([target]));\n    expect(callback).toHaveBeenCalledWith(target);\n    expect(result.current[0]).toBe(false);\n    expect(result.current[1]).toBe(0);\n\n    const target1 = getValue(true, 0.5);\n    act(() => observerCallback([target1]));\n    expect(callback).toHaveBeenCalledWith(target1);\n    expect(result.current[0]).toBe(true);\n    expect(result.current[1]).toBe(0.5);\n  });\n\n  test('should not work when target is null', async () => {\n    const previousCallsLength = mockIntersectionObserver.mock.calls.length;\n    renderHook(() => useInViewport(null));\n    const currentCallsLength = mockIntersectionObserver.mock.calls.length;\n    expect(currentCallsLength).toBe(previousCallsLength);\n  });\n\n  test('should disconnect when unmount', async () => {\n    mockIntersectionObserver.mockReturnValue({\n      observe: () => null,\n      disconnect,\n    });\n    const { unmount } = renderHook(() => useInViewport(targetEl));\n    unmount();\n    expect(disconnect).toBeCalled();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useInViewport/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Observe if the element is visible.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 监听元素是否在可见区域内\n */\n\nimport { useRef } from 'react';\nimport { useInViewport } from 'ahooks';\n\nexport default () => {\n  const ref = useRef(null);\n  const [inViewport] = useInViewport(ref);\n  return (\n    <div>\n      <div style={{ width: 300, height: 300, overflow: 'scroll', border: '1px solid' }}>\n        scroll here\n        <div style={{ height: 800 }}>\n          <div\n            ref={ref}\n            style={{\n              border: '1px solid',\n              height: 100,\n              width: 100,\n              textAlign: 'center',\n              marginTop: 80,\n            }}\n          >\n            observer dom\n          </div>\n        </div>\n      </div>\n      <div style={{ marginTop: 16, color: inViewport ? '#87d068' : '#f50' }}>\n        inViewport: {inViewport ? 'visible' : 'hidden'}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useInViewport/demo/demo2.tsx",
    "content": "/**\n * title: Observe element visible area ratio\n * desc: Pass in `options.threshold`, you can control the ratio to be triggered when the visible area reach every threshold. <br /> `options.root` can control the parent element, in this example, visible will not change relative to the browser viewport.\n *\n * title.zh-CN: 监听元素可见区域比例\n * desc.zh-CN: 传入 `options.threshold`, 可以控制在可见区域达到该比例时触发 ratio 更新。<br /> `options.root` 可以控制相对父级元素，在这个例子中，不会相对浏览器视窗变化。\n */\n\nimport { useInViewport } from 'ahooks';\n\nexport default () => {\n  const [inViewport, ratio] = useInViewport(() => document.getElementById('children'), {\n    threshold: [0, 0.25, 0.5, 0.75, 1],\n    root: () => document.getElementById('parent'),\n  });\n  return (\n    <div>\n      <div style={{ width: 300, height: 300, overflow: 'scroll', border: '1px solid' }} id=\"parent\">\n        scroll here\n        <div style={{ height: 800 }}>\n          <div\n            id=\"children\"\n            style={{\n              border: '1px solid',\n              height: 100,\n              width: 100,\n              textAlign: 'center',\n              marginTop: 80,\n            }}\n          >\n            observer dom\n          </div>\n        </div>\n      </div>\n      <div style={{ marginTop: 16, color: inViewport ? '#87d068' : '#f50' }}>\n        <p>inViewport: {inViewport ? 'visible' : 'hidden'}</p>\n        <p>ratio: {ratio}</p>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useInViewport/demo/demo3.tsx",
    "content": "/**\n * title: Listening content scrolling selection menu\n * desc: Pass the `callback` that is triggered when the callback of `IntersectionObserver` is called, so you can do some customization.\n *\n * title.zh-CN: 监听内容滚动选中菜单\n * desc.zh-CN: 传入 `callback`, 使得 `IntersectionObserver` 的回调被调用时，用户可以做一些自定义操作。\n */\nimport { useInViewport, useMemoizedFn } from 'ahooks';\nimport { useRef, useState } from 'react';\n\nconst menus = ['menu-1', 'menu-2', 'menu-3'];\nconst content = {\n  'menu-1': 'Content for menus 1',\n  'menu-2': 'Content for menus 2',\n  'menu-3': 'Content for menus 3',\n};\n\nexport default () => {\n  const menuRef = useRef<HTMLDivElement[]>([]);\n\n  const [activeMenu, setActiveMenu] = useState(menus[0]);\n\n  const callback = useMemoizedFn((entry) => {\n    if (entry.isIntersecting) {\n      const active = entry.target.getAttribute('id') || '';\n      setActiveMenu(active);\n    }\n  });\n\n  const handleMenuClick = (index: number) => {\n    const contentEl = document.getElementById('content-scroll');\n    const top = menuRef.current[index]?.offsetTop;\n\n    contentEl?.scrollTo({\n      top,\n      behavior: 'smooth',\n    });\n  };\n\n  useInViewport(menuRef.current, {\n    callback,\n    root: () => document.getElementById('parent-scroll'),\n    rootMargin: '-50% 0px -50% 0px',\n  });\n\n  return (\n    <div\n      id=\"parent-scroll\"\n      style={{ width: 300, height: 300, border: '1px solid', display: 'flex', overflow: 'hidden' }}\n    >\n      <div style={{ width: '30%', backgroundColor: '#f0f0f0' }}>\n        <ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>\n          {menus.map((menu, index) => (\n            <li\n              key={menu}\n              onClick={() => handleMenuClick(index)}\n              style={{\n                padding: '10px',\n                cursor: 'pointer',\n                textAlign: 'center',\n                transition: 'background-color 0.2s ease-in-out',\n                backgroundColor: activeMenu === menu ? '#e0e0e0' : '',\n              }}\n            >\n              {menu}\n            </li>\n          ))}\n        </ul>\n      </div>\n      <div id=\"content-scroll\" style={{ flex: 1, overflowY: 'scroll', position: 'relative' }}>\n        {menus.map((menu, index) => (\n          <div\n            ref={(el: HTMLDivElement) => {\n              menuRef.current[index] = el;\n            }}\n            key={menu}\n            id={menu}\n            style={{\n              display: 'flex',\n              justifyContent: 'center',\n              alignItems: 'center',\n              height: '100%',\n              fontSize: 16,\n            }}\n          >\n            {content[menu as keyof typeof content]}\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useInViewport/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useInViewport\n\nObserve whether the element is in the visible area, and the visible area ratio of the element. More information refer to [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Observe the visible area ratio of element\n\n<code src=\"./demo/demo2.tsx\" />\n\n### Listening content scrolling selection menu\n\n<code src=\"./demo/demo3.tsx\" />\n\n## API\n\n```typescript\ntype Target = Element | (() => Element) | React.MutableRefObject<Element>;\n\nconst [inViewport, ratio] = useInViewport(\n  target: Target | Target[],\n  options?: Options\n);\n```\n\n### Params\n\n| Property | Description                        | Type                     | Default |\n| -------- | ---------------------------------- | ------------------------ | ------- |\n| target   | DOM elements or Ref, support array | `Target` \\| `Target[]`   | -       |\n| options  | Setting                            | `Options` \\| `undefined` | -       |\n\n### Options\n\nMore information refer to [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)\n\n| Property   | Description                                                                                                                                                                       | Type                                                                                 | Default |\n| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------- |\n| threshold  | Either a single number or an array of numbers which indicate at what percentage of the target's visibility the ratio should be executed                                           | `number` \\| `number[]`                                                               | -       |\n| rootMargin | Margin around the root                                                                                                                                                            | `string`                                                                             | -       |\n| root       | The element that is used as the viewport for checking visibility of the target. Must be the ancestor of the target. Defaults to the browser viewport if not specified or if null. | `Element` \\| `Document` \\| `() => (Element/Document)` \\| `MutableRefObject<Element>` | -       |\n| callback   | Triggered when the callback of `IntersectionObserver` is called                                                                                                                   | `(entry: IntersectionObserverEntry) => void`                                         | -       |\n\n### Result\n\n| Property   | Description                                                                              | Type                     |\n| ---------- | ---------------------------------------------------------------------------------------- | ------------------------ |\n| inViewport | Is visible                                                                               | `boolean` \\| `undefined` |\n| ratio      | Current visible ratio, updated every time the node set by `options.threshold` is reached | `number` \\| `undefined`  |\n"
  },
  {
    "path": "packages/hooks/src/useInViewport/index.ts",
    "content": "import 'intersection-observer';\nimport { useState } from 'react';\nimport type { BasicTarget } from '../utils/domTarget';\nimport { getTargetElement } from '../utils/domTarget';\nimport useEffectWithTarget from '../utils/useEffectWithTarget';\n\ntype CallbackType = (entry: IntersectionObserverEntry) => void;\n\nexport interface Options {\n  rootMargin?: string;\n  threshold?: number | number[];\n  root?: BasicTarget<Element>;\n  callback?: CallbackType;\n}\n\nfunction useInViewport(target: BasicTarget | BasicTarget[], options?: Options) {\n  const { callback, ...option } = options || {};\n\n  const [state, setState] = useState<boolean>();\n  const [ratio, setRatio] = useState<number>();\n\n  useEffectWithTarget(\n    () => {\n      const targets = Array.isArray(target) ? target : [target];\n      const els = targets.map((element) => getTargetElement(element)).filter(Boolean);\n\n      if (!els.length) {\n        return;\n      }\n\n      const observer = new IntersectionObserver(\n        (entries) => {\n          for (const entry of entries) {\n            setRatio(entry.intersectionRatio);\n            setState(entry.isIntersecting);\n            callback?.(entry);\n          }\n        },\n        {\n          ...option,\n          root: getTargetElement(options?.root),\n        },\n      );\n\n      els.forEach((el) => observer.observe(el!));\n\n      return () => {\n        observer.disconnect();\n      };\n    },\n    [options?.rootMargin, options?.threshold, callback],\n    target,\n  );\n\n  return [state, ratio] as const;\n}\n\nexport default useInViewport;\n"
  },
  {
    "path": "packages/hooks/src/useInViewport/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useInViewport\n\n观察元素是否在可见区域，以及元素可见比例。更多信息参考 [Intersection Observer API](https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API)。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 监听元素可见区域比例\n\n<code src=\"./demo/demo2.tsx\" />\n\n### 监听内容滚动选中菜单\n\n<code src=\"./demo/demo3.tsx\" />\n\n## API\n\n```typescript\ntype Target = Element | (() => Element) | React.MutableRefObject<Element>;\n\nconst [inViewport, ratio] = useInViewport(\n  target: Target | Target[],\n  options?: Options\n);\n```\n\n### Params\n\n| 参数    | 说明                       | 类型                     | 默认值 |\n| ------- | -------------------------- | ------------------------ | ------ |\n| target  | DOM 节点或者 Ref，支持数组 | `Target` \\| `Target[]`   | -      |\n| options | 设置                       | `Options` \\| `undefined` | -      |\n\n### Options\n\n更多信息参考 [Intersection Observer API](https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API)\n\n| 参数       | 说明                                                                                                          | 类型                                                                                 | 默认值 |\n| ---------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------ |\n| threshold  | 可以是单一的 number 也可以是 number 数组，target 元素和 root 元素相交程度达到该值的时候 ratio 会被更新        | `number` \\| `number[]`                                                               | -      |\n| rootMargin | 根(root)元素的外边距                                                                                          | `string`                                                                             | -      |\n| root       | 指定根(root)元素，用于检查目标的可见性。必须是目标元素的父级元素，如果未指定或者为 null，则默认为浏览器视窗。 | `Element` \\| `Document` \\| `() => (Element/Document)` \\| `MutableRefObject<Element>` | -      |\n| callback   | `IntersectionObserver` 的回调被调用时触发                                                                     | `(entry: IntersectionObserverEntry) => void`                                         | -      |\n\n### Result\n\n| 参数       | 说明                                                        | 类型                     |\n| ---------- | ----------------------------------------------------------- | ------------------------ |\n| inViewport | 是否可见                                                    | `boolean` \\| `undefined` |\n| ratio      | 当前可见比例，在每次到达 `options.threshold` 设置节点时更新 | `number` \\| `undefined`  |\n"
  },
  {
    "path": "packages/hooks/src/useInfiniteScroll/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { useState } from 'react';\nimport { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';\nimport { sleep } from '../../utils/testingHelpers';\nimport useInfiniteScroll from '..';\nimport type { Data, InfiniteScrollOptions, Service } from '../types';\n\nlet count = 0;\nexport async function mockRequest() {\n  await sleep(1000);\n  if (count >= 1) {\n    return { list: [4, 5, 6] };\n  }\n  count++;\n  return {\n    list: [1, 2, 3],\n    nextId: count,\n  };\n}\n\nconst targetEl = document.createElement('div');\n\n// set target property\nfunction setTargetInfo(key: 'scrollTop', value: any) {\n  Object.defineProperty(targetEl, key, {\n    value,\n    configurable: true,\n  });\n}\n\nconst setup = <T extends Data>(service: Service<T>, options?: InfiniteScrollOptions<T>) =>\n  renderHook(() => useInfiniteScroll(service, options));\n\ndescribe('useInfiniteScroll', () => {\n  let mockRaf: ReturnType<typeof vi.spyOn>;\n\n  beforeEach(() => {\n    count = 0;\n  });\n\n  beforeAll(() => {\n    vi.useFakeTimers();\n    // Mock requestAnimationFrame to execute callbacks immediately\n    mockRaf = vi\n      .spyOn(window, 'requestAnimationFrame')\n      .mockImplementation((cb: FrameRequestCallback) => {\n        cb(0);\n        return 0;\n      }) as ReturnType<typeof vi.spyOn>;\n  });\n\n  afterAll(() => {\n    mockRaf.mockRestore();\n    vi.useRealTimers();\n  });\n\n  test('should auto load', async () => {\n    const { result } = setup(mockRequest);\n    expect(result.current.loading).toBe(true);\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current.loading).toBe(false);\n  });\n\n  test('loadMore should be work', async () => {\n    const { result } = setup(mockRequest, { manual: true });\n    const { loadMore, loading } = result.current;\n    expect(loading).toBe(false);\n    act(() => {\n      loadMore();\n    });\n    expect(result.current.loadingMore).toBe(true);\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current.loadingMore).toBe(false);\n  });\n\n  test('noMore should be true when isNoMore is true', async () => {\n    const { result } = setup(mockRequest, {\n      isNoMore: (d) => d?.nextId === undefined,\n    });\n    const { loadMore } = result.current;\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n\n    expect(result.current.noMore).toBe(false);\n    act(() => loadMore());\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current.noMore).toBe(true);\n  });\n\n  test('should auto load when scroll to bottom', async () => {\n    const events: Record<string, any> = {};\n    const mockAddEventListener = vi\n      .spyOn(targetEl, 'addEventListener')\n      .mockImplementation((eventName: string, callback: any) => {\n        events[eventName] = callback;\n      });\n    const { result } = setup(mockRequest, {\n      target: targetEl,\n      isNoMore: (d) => d?.nextId === undefined,\n    });\n    // not work when loading\n    expect(result.current.loading).toBe(true);\n    events['scroll']();\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current.loading).toBe(false);\n    const scrollHeightSpy = vi.spyOn(targetEl, 'scrollHeight', 'get').mockImplementation(() => 150);\n    const clientHeightSpy = vi.spyOn(targetEl, 'clientHeight', 'get').mockImplementation(() => 300);\n    setTargetInfo('scrollTop', 100);\n    act(() => {\n      events['scroll']();\n    });\n    expect(result.current.loadingMore).toBe(true);\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current.loadingMore).toBe(false);\n\n    // not work when no more\n    expect(result.current.noMore).toBe(true);\n    act(() => {\n      events['scroll']();\n    });\n    expect(result.current.loadingMore).toBe(false);\n    // get list by order\n    expect(result.current.data?.list).toMatchObject([1, 2, 3, 4, 5, 6]);\n\n    mockAddEventListener.mockRestore();\n    scrollHeightSpy.mockRestore();\n    clientHeightSpy.mockRestore();\n  });\n\n  test('should auto load when scroll to top', async () => {\n    const events: Record<string, any> = {};\n    const mockAddEventListener = vi\n      .spyOn(targetEl, 'addEventListener')\n      .mockImplementation((eventName: string, callback: any) => {\n        events[eventName] = callback;\n      });\n    // Mock scrollTo using Object.defineProperty\n    Object.defineProperty(targetEl, 'scrollTo', {\n      value: (x: number, y: number) => {\n        setTargetInfo('scrollTop', y);\n      },\n      writable: true,\n    });\n\n    const { result } = setup(mockRequest, {\n      target: targetEl,\n      direction: 'top',\n      isNoMore: (d) => d?.nextId === undefined,\n    });\n    // not work when loading\n    expect(result.current.loading).toBe(true);\n    events['scroll']();\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current.loading).toBe(false);\n\n    // mock first scroll\n    const scrollHeightSpy = vi.spyOn(targetEl, 'scrollHeight', 'get').mockImplementation(() => 150);\n    const clientHeightSpy = vi.spyOn(targetEl, 'clientHeight', 'get').mockImplementation(() => 500);\n    setTargetInfo('scrollTop', 300);\n\n    act(() => {\n      events['scroll']();\n    });\n    // mock scroll upward\n    setTargetInfo('scrollTop', 50);\n\n    act(() => {\n      events['scroll']();\n    });\n\n    expect(result.current.loadingMore).toBe(true);\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n\n    expect(result.current.loadingMore).toBe(false);\n    //reverse order\n    expect(result.current.data?.list).toMatchObject([4, 5, 6, 1, 2, 3]);\n\n    // not work when no more\n    expect(result.current.noMore).toBe(true);\n    act(() => {\n      events['scroll']();\n    });\n    expect(result.current.loadingMore).toBe(false);\n\n    mockAddEventListener.mockRestore();\n    scrollHeightSpy.mockRestore();\n    clientHeightSpy.mockRestore();\n  });\n\n  test('reload should be work', async () => {\n    const fn = vi.fn(() => Promise.resolve({ list: [] }));\n    const { result } = setup(fn);\n    const { reload } = result.current;\n    expect(fn).toBeCalledTimes(1);\n    act(() => reload());\n    expect(fn).toBeCalledTimes(2);\n    await act(async () => {\n      Promise.resolve();\n    });\n  });\n\n  test('reload should be triggered when reloadDeps change', async () => {\n    const fn = vi.fn(() => Promise.resolve({ list: [] }));\n    const { result } = renderHook(() => {\n      const [value, setValue] = useState('');\n      const res = useInfiniteScroll(fn, {\n        reloadDeps: [value],\n      });\n      return {\n        ...res,\n        setValue,\n      };\n    });\n    expect(fn).toBeCalledTimes(1);\n    act(() => {\n      result.current.setValue('ahooks');\n    });\n    expect(fn).toBeCalledTimes(2);\n    await act(async () => {\n      Promise.resolve();\n    });\n  });\n\n  test('reload data should be latest', async () => {\n    let listCount = 5;\n    const mockRequestFn = async () => {\n      await sleep(1000);\n      return {\n        list: Array.from({\n          length: listCount,\n        }).map((_, index) => index + 1),\n        nextId: listCount,\n        hasMore: listCount > 2,\n      };\n    };\n\n    const { result } = setup(mockRequestFn);\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current.data).toMatchObject({ list: [1, 2, 3, 4, 5], nextId: 5 });\n\n    listCount = 3;\n    await act(async () => {\n      result.current.reload();\n      vi.advanceTimersByTime(1000);\n    });\n\n    expect(result.current.data).toMatchObject({ list: [1, 2, 3], nextId: 3 });\n  });\n\n  test('mutate should be work', async () => {\n    const { result } = setup(mockRequest);\n    const { mutate } = result.current;\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current.data).toMatchObject({ list: [1, 2, 3], nextId: 1 });\n    const newData = {\n      list: [1, 2],\n      nextId: 1,\n    };\n    act(() => mutate(newData));\n    expect(result.current.data).toMatchObject(newData);\n  });\n\n  test('cancel should be work', () => {\n    const onSuccess = vi.fn();\n    const { result } = setup(mockRequest, {\n      onSuccess,\n    });\n    const { cancel } = result.current;\n    expect(result.current.loading).toBe(true);\n    act(() => cancel());\n    expect(result.current.loading).toBe(false);\n    expect(onSuccess).not.toBeCalled();\n  });\n\n  test('onBefore/onSuccess/onFinally should be called', async () => {\n    const onBefore = vi.fn();\n    const onSuccess = vi.fn();\n    const onFinally = vi.fn();\n    setup(mockRequest, {\n      onBefore,\n      onSuccess,\n      onFinally,\n    });\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(onBefore).toBeCalled();\n    expect(onSuccess).toBeCalled();\n    expect(onFinally).toBeCalled();\n  });\n\n  test('onError should be called when throw error', async () => {\n    const onError = vi.fn();\n    const mockRequestError = () => {\n      return Promise.reject('error');\n    };\n    setup(mockRequestError, {\n      onError,\n    });\n    await act(async () => {\n      Promise.resolve();\n    });\n    expect(onError).toBeCalled();\n  });\n\n  test('loadMoreAsync should be work', async () => {\n    const { result } = setup(mockRequest, {\n      manual: true,\n    });\n    const { loadMoreAsync } = result.current;\n    act(() => {\n      loadMoreAsync().then((res) => {\n        expect(res).toMatchObject({ list: [1, 2, 3], nextId: 1 });\n        expect(result.current.loading).toBe(false);\n      });\n    });\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n  });\n\n  test('reloadAsync should be work', async () => {\n    const fn = vi.fn(() => Promise.resolve({ list: [] }));\n    const { result } = setup(fn);\n    const { reloadAsync } = result.current;\n    expect(fn).toBeCalledTimes(1);\n\n    act(() => {\n      reloadAsync().then(() => {\n        expect(fn).toBeCalledTimes(2);\n      });\n    });\n    await act(async () => {\n      Promise.resolve();\n    });\n  });\n\n  test('loading should be true when reload after loadMore', async () => {\n    const { result } = setup(mockRequest);\n    expect(result.current.loading).toBeTruthy();\n    const { reload, loadMore } = result.current;\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n\n    expect(result.current.loading).toBeFalsy();\n\n    act(() => {\n      loadMore();\n      reload();\n    });\n    expect(result.current.loading).toBeTruthy();\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n\n    expect(result.current.loading).toBeFalsy();\n  });\n\n  test('loading should be true when reloadAsync after loadMore', async () => {\n    const { result } = setup(mockRequest);\n    expect(result.current.loading).toBeTruthy();\n    const { reloadAsync, loadMore } = result.current;\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n\n    expect(result.current.loading).toBeFalsy();\n\n    act(() => {\n      loadMore();\n      reloadAsync();\n    });\n    expect(result.current.loading).toBeTruthy();\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n\n    expect(result.current.loading).toBeFalsy();\n  });\n\n  test('list can be null or undefined', async () => {\n    // @ts-ignore\n    const { result } = setup(async () => {\n      await sleep(1000);\n      count++;\n      return {\n        list: Math.random() < 0.5 ? null : undefined,\n        nextId: count,\n      };\n    });\n\n    expect(result.current.loading).toBeTruthy();\n\n    const { loadMore } = result.current;\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n\n    expect(result.current.loading).toBeFalsy();\n\n    act(() => {\n      loadMore();\n    });\n  });\n\n  test('error result', async () => {\n    const { result } = setup(async () => {\n      throw new Error('error message');\n    });\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n\n    expect(result.current.error?.message).toBe('error message');\n  });\n\n  test('reloadAsync should reset data and restart from page=1', async () => {\n    const PAGE_SIZE = 2;\n\n    // Vitest 的 mock\n    const getLoadMoreListMock = vi.fn((page: number, pageSize: number) => {\n      const start = (page - 1) * pageSize + 1;\n      const list = Array.from({ length: pageSize }, (_, i) => start + i);\n      return Promise.resolve({ list });\n    });\n\n    const { result } = renderHook(() =>\n      useInfiniteScroll((d) => {\n        const page = d ? Math.ceil(d.list.length / PAGE_SIZE) + 1 : 1;\n        return getLoadMoreListMock(page, PAGE_SIZE);\n      }),\n    );\n\n    await act(async () => {\n      await result.current.loadMoreAsync();\n    });\n    expect(getLoadMoreListMock).toHaveBeenLastCalledWith(1, PAGE_SIZE);\n    expect(result.current.data?.list.length).toBe(2);\n\n    await act(async () => {\n      await result.current.loadMoreAsync();\n    });\n    expect(getLoadMoreListMock).toHaveBeenLastCalledWith(2, PAGE_SIZE);\n    expect(result.current.data?.list.length).toBe(4);\n\n    await act(async () => {\n      await result.current.reloadAsync();\n    });\n    expect(getLoadMoreListMock).toHaveBeenLastCalledWith(1, PAGE_SIZE);\n    expect(result.current.data?.list.length).toBe(2);\n    expect(result.current.data?.list).toEqual([1, 2]);\n  });\n\n  test('service should be called only once when scrolling to bottom multiple times quickly', async () => {\n    const mockService = vi.fn(async () => {\n      await sleep(1000);\n      return { list: [1, 2, 3], nextId: 1 };\n    });\n\n    const events: Record<string, any> = {};\n    const mockAddEventListener = vi\n      .spyOn(targetEl, 'addEventListener')\n      .mockImplementation((eventName: string, callback: any) => {\n        events[eventName] = callback;\n      });\n\n    const scrollHeightSpy = vi.spyOn(targetEl, 'scrollHeight', 'get').mockImplementation(() => 150);\n    const clientHeightSpy = vi.spyOn(targetEl, 'clientHeight', 'get').mockImplementation(() => 100);\n\n    const { result } = setup(mockService, {\n      target: targetEl,\n      isNoMore: (d) => d?.nextId === undefined,\n    });\n\n    // Wait for initial load to complete\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current.loading).toBe(false);\n    expect(mockService).toHaveBeenCalledTimes(1);\n\n    // Set scroll position to bottom (scrollHeight - scrollTop <= clientHeight + threshold)\n    // 150 - 50 = 100 <= 100 + 100 = 200, so it should trigger loadMore\n    setTargetInfo('scrollTop', 50);\n\n    // Trigger scroll event multiple times quickly (before first request completes)\n    act(() => {\n      events['scroll']();\n    });\n\n    // Service should be called once more (total 2 times: initial + loadMore)\n    expect(mockService).toHaveBeenCalledTimes(2);\n\n    // Trigger more scroll events while loading\n    act(() => {\n      events['scroll']();\n    });\n    act(() => {\n      events['scroll']();\n    });\n    act(() => {\n      events['scroll']();\n    });\n\n    // Service should still only be called twice (no additional calls during loading)\n    expect(mockService).toHaveBeenCalledTimes(2);\n\n    mockAddEventListener.mockRestore();\n    scrollHeightSpy.mockRestore();\n    clientHeightSpy.mockRestore();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useInfiniteScroll/demo/default.tsx",
    "content": "import { useInfiniteScroll } from 'ahooks';\n\ninterface Result {\n  list: string[];\n  nextId: string | undefined;\n}\n\nconst resultData = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13'];\n\nfunction getLoadMoreList(nextId: string | undefined, limit: number): Promise<Result> {\n  let start = 0;\n  if (nextId) {\n    start = resultData.findIndex((i) => i === nextId);\n  }\n  const end = start + limit;\n  const list = resultData.slice(start, end);\n  const nId = resultData.length >= end ? resultData[end] : undefined;\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve({\n        list,\n        nextId: nId,\n      });\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const { data, loading, loadMore, loadingMore } = useInfiniteScroll((d) =>\n    getLoadMoreList(d?.nextId, 4),\n  );\n\n  return (\n    <div>\n      {loading ? (\n        <p>loading</p>\n      ) : (\n        <div>\n          {data?.list?.map((item) => (\n            <div key={item} style={{ padding: 12, border: '1px solid #f5f5f5' }}>\n              item-{item}\n            </div>\n          ))}\n        </div>\n      )}\n\n      <div style={{ marginTop: 8 }}>\n        {data?.nextId && (\n          <button type=\"button\" onClick={loadMore} disabled={loadingMore}>\n            {loadingMore ? 'Loading more...' : 'Click to load more'}\n          </button>\n        )}\n\n        {!data?.nextId && <span>No more data</span>}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useInfiniteScroll/demo/mutate.tsx",
    "content": "import { useInfiniteScroll, useRequest } from 'ahooks';\n\ninterface Result {\n  list: string[];\n  nextId: string | undefined;\n}\n\nconst resultData = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13'];\n\nfunction getLoadMoreList(nextId: string | undefined, limit: number): Promise<Result> {\n  let start = 0;\n  if (nextId) {\n    start = resultData.findIndex((i) => i === nextId);\n  }\n  const end = start + limit;\n  const list = resultData.slice(start, end);\n  const nId = resultData.length >= end ? resultData[end] : undefined;\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve({\n        list,\n        nextId: nId,\n      });\n    }, 1000);\n  });\n}\n\nfunction deleteItem(id: string) {\n  return new Promise<void>((resolve) => {\n    setTimeout(() => {\n      resolve();\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const { data, loading, loadMore, loadingMore, mutate } = useInfiniteScroll((d) =>\n    getLoadMoreList(d?.nextId, 4),\n  );\n\n  const {\n    loading: deleteLading,\n    params: deleteParams,\n    run: remove,\n  } = useRequest(deleteItem, {\n    manual: true,\n    onSuccess: (_, [id]) => {\n      if (data) {\n        const index = data.list.findIndex((i) => i === id);\n        data?.list.splice(index, 1);\n        mutate({ ...data });\n      }\n    },\n  });\n\n  return (\n    <div>\n      {loading ? (\n        <p>loading</p>\n      ) : (\n        <div>\n          {data?.list?.map((item) => (\n            <div key={item} style={{ padding: 12, border: '1px solid #f5f5f5' }}>\n              item-{item}\n              <button\n                style={{ marginLeft: 8 }}\n                onClick={() => remove(item)}\n                disabled={deleteLading && deleteParams[0] === item}\n              >\n                Delete\n              </button>\n            </div>\n          ))}\n        </div>\n      )}\n\n      <div style={{ marginTop: 8 }}>\n        {data?.nextId && (\n          <button type=\"button\" onClick={loadMore} disabled={loadingMore}>\n            {loadingMore ? 'Loading more...' : 'Click to load more'}\n          </button>\n        )}\n\n        {!data?.nextId && <span>No more data</span>}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useInfiniteScroll/demo/pagination.tsx",
    "content": "import { useInfiniteScroll } from 'ahooks';\n\ninterface Result {\n  list: string[];\n  total: number;\n}\n\nconst resultData = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13'];\n\nfunction getLoadMoreList(page: number, pageSize: number): Promise<Result> {\n  const start = (page - 1) * pageSize;\n  const end = page * pageSize;\n  const list = resultData.slice(start, end);\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve({\n        list,\n        total: resultData.length,\n      });\n    }, 1000);\n  });\n}\n\nconst PAGE_SIZE = 4;\n\nexport default () => {\n  const { data, loading, loadMore, loadingMore } = useInfiniteScroll((d) => {\n    const page = d ? Math.ceil(d.list.length / PAGE_SIZE) + 1 : 1;\n    return getLoadMoreList(page, PAGE_SIZE);\n  });\n\n  const hasMore = data && data.list.length < data.total;\n\n  return (\n    <div>\n      {loading ? (\n        <p>loading</p>\n      ) : (\n        <div>\n          {data?.list?.map((item) => (\n            <div key={item} style={{ padding: 12, border: '1px solid #f5f5f5' }}>\n              item-{item}\n            </div>\n          ))}\n        </div>\n      )}\n\n      <div style={{ marginTop: 8 }}>\n        {hasMore && (\n          <button type=\"button\" onClick={loadMore} disabled={loadingMore}>\n            {loadingMore ? 'Loading more...' : 'Click to load more'}\n          </button>\n        )}\n\n        {!hasMore && <span>No more data</span>}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useInfiniteScroll/demo/reload.tsx",
    "content": "import { useState } from 'react';\nimport { useInfiniteScroll } from 'ahooks';\n\ninterface Result {\n  list: string[];\n  nextId: string | undefined;\n}\n\nconst resultData = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13'];\n\nfunction getLoadMoreList(\n  nextId: string | undefined,\n  limit: number,\n  keyword: string,\n): Promise<Result> {\n  let start = 0;\n  if (nextId) {\n    start = resultData.findIndex((i) => i === nextId);\n  }\n  const end = start + limit;\n  const list = resultData.slice(start, end);\n  const nId = resultData.length >= end ? resultData[end] : undefined;\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve({\n        list,\n        nextId: nId,\n      });\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const [keyword, setKeyword] = useState('');\n\n  const { data, loading, loadMore, loadingMore, reload } = useInfiniteScroll((d) =>\n    getLoadMoreList(d?.nextId, 4, keyword),\n  );\n\n  return (\n    <div>\n      <div style={{ marginBottom: 16 }}>\n        <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />\n        <button style={{ marginLeft: 8 }} onClick={reload}>\n          Filter\n        </button>\n      </div>\n      {loading ? (\n        <p>loading</p>\n      ) : (\n        <div>\n          {data?.list?.map((item) => (\n            <div key={item} style={{ padding: 12, border: '1px solid #f5f5f5' }}>\n              item-{item}\n            </div>\n          ))}\n        </div>\n      )}\n\n      <div style={{ marginTop: 8 }}>\n        {data?.nextId && (\n          <button type=\"button\" onClick={loadMore} disabled={loadingMore}>\n            {loadingMore ? 'Loading more...' : 'Click to load more'}\n          </button>\n        )}\n\n        {!data?.nextId && <span>No more data</span>}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useInfiniteScroll/demo/scroll.tsx",
    "content": "import { useRef } from 'react';\nimport { useInfiniteScroll } from 'ahooks';\n\ninterface Result {\n  list: string[];\n  nextId: string | undefined;\n}\n\nconst resultData = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13'];\n\nfunction getLoadMoreList(nextId: string | undefined, limit: number): Promise<Result> {\n  let start = 0;\n  if (nextId) {\n    start = resultData.findIndex((i) => i === nextId);\n  }\n  const end = start + limit;\n  const list = resultData.slice(start, end);\n  const nId = resultData.length >= end ? resultData[end] : undefined;\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve({\n        list,\n        nextId: nId,\n      });\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const ref = useRef<HTMLDivElement>(null);\n\n  const { data, loading, loadMore, loadingMore, noMore } = useInfiniteScroll(\n    (d) => getLoadMoreList(d?.nextId, 4),\n    {\n      target: ref,\n      isNoMore: (d) => d?.nextId === undefined,\n    },\n  );\n\n  return (\n    <div ref={ref} style={{ height: 150, overflow: 'auto', border: '1px solid', padding: 12 }}>\n      {loading ? (\n        <p>loading</p>\n      ) : (\n        <div>\n          {data?.list?.map((item) => (\n            <div key={item} style={{ padding: 12, border: '1px solid #f5f5f5' }}>\n              item-{item}\n            </div>\n          ))}\n        </div>\n      )}\n\n      <div style={{ marginTop: 8 }}>\n        {!noMore && (\n          <button type=\"button\" onClick={loadMore} disabled={loadingMore}>\n            {loadingMore ? 'Loading more...' : 'Click to load more'}\n          </button>\n        )}\n\n        {noMore && <span>No more data</span>}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useInfiniteScroll/demo/scrollTop.tsx",
    "content": "import { useRef } from 'react';\nimport { useInfiniteScroll } from 'ahooks';\n\ninterface Result {\n  list: string[];\n  nextId: string | undefined;\n}\n\nconst resultData = [\n  '15',\n  '14',\n  '13',\n  '12',\n  '11',\n  '10',\n  '9',\n  '8',\n  '7',\n  '6',\n  '5',\n  '4',\n  '3',\n  '2',\n  '1',\n  '0',\n];\n\nfunction getLoadMoreList(nextId: string | undefined, limit: number): Promise<Result> {\n  let start = 0;\n  if (nextId) {\n    start = resultData.findIndex((i) => i === nextId);\n  }\n  const end = start + limit;\n  const list = resultData.slice(start, end).reverse();\n  const nId = resultData.length >= end ? resultData[end] : undefined;\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve({\n        list,\n        nextId: nId,\n      });\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const ref = useRef<HTMLDivElement>(null);\n  const isFirstIn = useRef(true);\n\n  const { data, loading, loadMore, loadingMore, noMore } = useInfiniteScroll(\n    (d) => getLoadMoreList(d?.nextId, 5),\n    {\n      target: ref,\n      direction: 'top',\n      threshold: 0,\n      isNoMore: (d) => d?.nextId === undefined,\n      onSuccess() {\n        if (isFirstIn.current) {\n          isFirstIn.current = false;\n          setTimeout(() => {\n            const el = ref.current;\n            if (el) {\n              el.scrollTo(0, 999999);\n            }\n          });\n        }\n      },\n    },\n  );\n\n  return (\n    <div ref={ref} style={{ height: 150, overflow: 'auto', border: '1px solid', padding: 12 }}>\n      {loading ? (\n        <p>loading</p>\n      ) : (\n        <div>\n          <div style={{ marginBottom: 10 }}>\n            {!noMore && (\n              <button type=\"button\" onClick={loadMore} disabled={loadingMore}>\n                {loadingMore ? 'Loading more...' : 'Click to load more'}\n              </button>\n            )}\n\n            {noMore && <span>No more data</span>}\n          </div>\n          {data?.list?.map((item) => (\n            <div key={item} style={{ padding: 12, border: '1px solid #f5f5f5' }}>\n              item-{item}\n            </div>\n          ))}\n        </div>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useInfiniteScroll/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useInfiniteScroll\n\nuseInfiniteScroll encapsulates the common infinite scroll logic.\n\n```js\nconst { data, loading, loadingMore, loadMore } = useInfiniteScroll(service);\n```\n\nThe first parameter `service` of useInfiniteScroll is an asynchronous function. The input and output parameters of this function have the following conventions:\n\n1. The data returned by `service` must contain a `list` array, the type is `{ list: any[], ...rest }`\n2. The input parameter of `service` is the latest merged `data`\n\nIf the data returned for the first request is `{ list: [1, 2, 3], nextId: 4 }`, the data returned for the second time is `{ list: [4, 5, 6], nextId: 7 }` , then we will automatically merge `list`, and the merged `data` will be `{ list: [1, 2, 3, 4, 5, 6], nextId: 7 }`.\n\n## Basic usage\n\nIn the first example, we demonstrate the implementation of a most basic infinite scroll.\n\n<code src=\"./demo/default.tsx\" />\n\n## Pagination\n\nIn the data fixation scenario, we sometimes use `page` and `pageSize` to request new data.\n\n<code src=\"./demo/pagination.tsx\" />\n\n## Scrolling to automatically load\n\nIn the infinite scrolling scenario, the most common case is to automatically load when scrolling to the bottom. By configuring the following properties, you can achieve scrolling to automatically load.\n\n- `options.target` specifies the parent element, The parent element needs to set a fixed height and support internal scrolling\n- `options.isNoMore` determines if there is no more data\n- `options.direction` determines the direction of scrolling, the default is `bottom`\n\nthe scroll to bottom demo\n<code src=\"./demo/scroll.tsx\" />\n\nthe scroll to top demo\n<code src=\"./demo/scrollTop.tsx\" />\n\n## Data reset\n\nThe data can be reset by `reload`. The following example shows that after the `filter` changes, the data is reset to the first page.\n\n<code src=\"./demo/reload.tsx\" />\n\nThe above code can be implemented with `reloadDeps` syntax sugar. When `reloadDeps` changes, `reload` will be triggered automatically.\n\n```ts\nconst result = useInfiniteScroll(service, {\n  reloadDeps: [keyword]\n});\n```\n\n## Data mutation\n\nWith `mutate`, we can directly modify the current `data`. The following example demonstrates deleting a record from the data.\n\n<code src=\"./demo/mutate.tsx\" />\n\n## API\n\n```ts\nexport type Data = { list: any[];[key: string]: any; };\nexport type Service<TData extends Data> = (currentData?: TData) => Promise<TData>;\n\nconst {\n  data: TData;\n  loading: boolean;\n  loadingMore: boolean;\n  error?: Error;\n  noMore: boolean;\n  loadMore: () => void;\n  loadMoreAsync: () => Promise<TData>;\n  reload: () => void;\n  reloadAsync: () => Promise<TData>;\n  cancel: () => void;\n  mutate: (data?: TData) => void;\n} = useInfiniteScroll<TData extends Data>(\n  service: (currentData?: TData) => Promise<TData>,\n  {\n    target?: BasicTarget;\n    isNoMore?: (data?: TData) => boolean;\n    threshold?: number;\n    manual?: boolean;\n    reloadDeps?: DependencyList;\n    onBefore?: () => void;\n    onSuccess?: (data: TData) => void;\n    onError?: (e: Error) => void;\n    onFinally?: (data?: TData, e?: Error) => void;\n  }\n);\n```\n\n### Result\n\n| Property      | Description                                                                                                                                       | Type                     |\n| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |\n| data          | The data returned by the service, where the `list` attribute is the aggregated data                                                               | `TData` \\| `undefined`   |\n| loading       | Is the first request in progress                                                                                                                  | `boolean`                |\n| loadingMore   | Is more data request in progress                                                                                                                  | `boolean`                |\n| noMore        | Whether there is no more data, it will take effect after configuring `options.isNoMore`                                                           | `boolean`                |\n| error         | Request error message                                                                                                                             | `Error`                  |\n| loadMore      | Load more data, it will automatically catch the exception, and handle it through `options.onError`                                                | `() => void`             |\n| loadMoreAsync | Load more data, which is consistent with the behavior of `loadMore`, but returns Promise, so you need to handle the exception yourself            | `() => Promise<TData>`   |\n| reload        | Load the first page of data, it will automatically catch the exception, and handle it through `options.onError`                                   | `() => void`             |\n| reloadAsync   | Load the first page of data, which is consistent with the behavior of `reload`, but returns Promise, so you need to handle the exception yourself | `() => Promise<TData>`   |\n| mutate        | Modify `data` directly                                                                                                                            | `(data?: TData) => void` |\n| cancel        | Ignore the current promise response                                                                                                               | `() => void`             |\n\n### Options\n\n| Property   | Description                                                                                                                                                                                                                                          | Type                                                        | Default  |\n| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -------- |\n| target     | specifies the parent element. If it exists, it will trigger the `loadMore` when scrolling to the bottom. Needs to work with `isNoMore` to know when there is no more data to load. **when target is document, it is defined as the entire viewport** | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -        |\n| isNoMore   | determines if there is no more data, the input parameter is the latest merged `data`                                                                                                                                                                 | `(data?: TData) => boolean`                                 | -        |\n| threshold  | The pixel threshold to the bottom for the scrolling to load                                                                                                                                                                                          | `number`                                                    | `100`    |\n| direction  | The direction of the scrolling                                                                                                                                                                                                                       | `bottom` \\|`top`                                            | `bottom` |\n| reloadDeps | When the content of the array changes, `reload` will be triggered                                                                                                                                                                                    | `any[]`                                                     | -        |\n| manual     | <ul><li> The default is `false`. That is, the service is automatically executed during initialization. </li><li>If set to `true`, you need to manually call `run` or `runAsync` to trigger execution </li></ul>                                      | `boolean`                                                   | `false`  |\n| onBefore   | Triggered before service execution                                                                                                                                                                                                                   | `() => void`                                                | -        |\n| onSuccess  | Triggered when service resolve                                                                                                                                                                                                                       | `(data: TData) => void`                                     | -        |\n| onError    | Triggered when service reject                                                                                                                                                                                                                        | `(e: Error) => void`                                        | -        |\n| onFinally  | Triggered when service execution is complete                                                                                                                                                                                                         | `(data?: TData, e?: Error) => void`                         | -        |\n"
  },
  {
    "path": "packages/hooks/src/useInfiniteScroll/index.tsx",
    "content": "import { useMemo, useRef, useState } from 'react';\nimport useEventListener from '../useEventListener';\nimport useMemoizedFn from '../useMemoizedFn';\nimport useRequest from '../useRequest';\nimport useUpdateEffect from '../useUpdateEffect';\nimport { getTargetElement } from '../utils/domTarget';\nimport { getClientHeight, getScrollHeight, getScrollTop } from '../utils/rect';\nimport type { Data, InfiniteScrollOptions, Service } from './types';\n\nconst useInfiniteScroll = <TData extends Data>(\n  service: Service<TData>,\n  options: InfiniteScrollOptions<TData> = {},\n) => {\n  const {\n    target,\n    isNoMore,\n    threshold = 100,\n    direction = 'bottom',\n    reloadDeps = [],\n    manual,\n    onBefore,\n    onSuccess,\n    onError,\n    onFinally,\n  } = options;\n\n  const [finalData, setFinalData] = useState<TData>();\n  const [loadingMore, setLoadingMore] = useState(false);\n  const isScrollToTop = direction === 'top';\n  // lastScrollTop is used to determine whether the scroll direction is up or down\n  const lastScrollTop = useRef<number>(undefined);\n  // scrollBottom is used to record the distance from the bottom of the scroll bar\n  const scrollBottom = useRef<number>(0);\n\n  const noMore = useMemo(() => {\n    if (!isNoMore) {\n      return false;\n    }\n    return isNoMore(finalData);\n  }, [finalData]);\n\n  const { loading, error, run, runAsync, cancel } = useRequest(\n    async (lastData?: TData) => {\n      const currentData = await service(lastData);\n      return { currentData, lastData };\n    },\n    {\n      manual,\n      onFinally: (_, d, e) => {\n        setLoadingMore(false);\n        onFinally?.(d?.currentData, e);\n      },\n      onBefore: () => onBefore?.(),\n      onSuccess: (d) => {\n        if (!d.lastData) {\n          setFinalData({\n            ...d.currentData,\n            list: [...(d.currentData.list ?? [])],\n          });\n        } else {\n          setFinalData({\n            ...d.currentData,\n            list: isScrollToTop\n              ? [...d.currentData.list, ...(d.lastData.list ?? [])]\n              : [...(d.lastData.list ?? []), ...d.currentData.list],\n          });\n        }\n\n        setTimeout(() => {\n          // use requestAnimationFrame to ensure the scroll position is updated (To ensure compatibility react 19)\n          requestAnimationFrame(() => {\n            if (isScrollToTop) {\n              let el = getTargetElement(target);\n              el = el === document ? document.documentElement : el;\n              if (el) {\n                const scrollHeight = getScrollHeight(el);\n                (el as Element).scrollTo(0, scrollHeight - scrollBottom.current);\n              }\n            } else {\n              // eslint-disable-next-line @typescript-eslint/no-use-before-define\n              scrollMethod();\n            }\n          });\n        });\n\n        onSuccess?.(d.currentData);\n      },\n      onError: (e) => onError?.(e),\n    },\n  );\n\n  const loadMore = useMemoizedFn(() => {\n    if (noMore) {\n      return;\n    }\n    setLoadingMore(true);\n    run(finalData);\n  });\n\n  const runAsyncForCurrent = async (data?: TData) => {\n    const res = await runAsync(data);\n    return res.currentData;\n  };\n\n  const loadMoreAsync = useMemoizedFn(() => {\n    if (noMore) {\n      return Promise.reject();\n    }\n    setLoadingMore(true);\n    return runAsyncForCurrent(finalData);\n  });\n\n  const reload = () => {\n    setLoadingMore(false);\n    return run();\n  };\n\n  const reloadAsync = () => {\n    setLoadingMore(false);\n    return runAsyncForCurrent();\n  };\n\n  const scrollMethod = () => {\n    const el = getTargetElement(target);\n    if (!el) {\n      return;\n    }\n\n    const targetEl = el === document ? document.documentElement : el;\n    const scrollTop = getScrollTop(targetEl);\n    const scrollHeight = getScrollHeight(targetEl);\n    const clientHeight = getClientHeight(targetEl);\n\n    if (isScrollToTop) {\n      if (\n        lastScrollTop.current !== undefined &&\n        lastScrollTop.current > scrollTop &&\n        scrollTop <= threshold\n      ) {\n        loadMore();\n      }\n      lastScrollTop.current = scrollTop;\n      scrollBottom.current = scrollHeight - scrollTop;\n    } else if (scrollHeight - scrollTop <= clientHeight + threshold) {\n      loadMore();\n    }\n  };\n\n  useEventListener(\n    'scroll',\n    () => {\n      if (loading || loadingMore) {\n        return;\n      }\n      scrollMethod();\n    },\n    { target },\n  );\n\n  useUpdateEffect(() => {\n    run();\n  }, [...reloadDeps]);\n\n  return {\n    data: finalData,\n    loading: !loadingMore && loading,\n    error,\n    loadingMore,\n    noMore,\n\n    loadMore,\n    loadMoreAsync,\n    reload: useMemoizedFn(reload),\n    reloadAsync: useMemoizedFn(reloadAsync),\n    mutate: setFinalData,\n    cancel,\n  };\n};\n\nexport default useInfiniteScroll;\n"
  },
  {
    "path": "packages/hooks/src/useInfiniteScroll/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useInfiniteScroll\n\nuseInfiniteScroll 封装了常见的无限滚动逻辑。\n\n```js\nconst { data, loading, loadingMore, loadMore } = useInfiniteScroll(service);\n```\n\nuseInfiniteScroll 的第一个参数 `service` 是一个异步函数，对这个函数的入参和出参有如下约定：\n\n1. `service` 返回的数据必须包含 `list` 数组，类型为 `{ list: any[], ...rest }`\n2. `service` 的入参为整合后的最新 `data`\n\n假如第一次请求返回数据为 `{ list: [1, 2, 3], nextId: 4 }`, 第二次返回的数据为 `{ list: [4, 5, 6], nextId: 7 }`, 则我们会自动合并 `list`，整合后的 `data` 为 `{ list: [1, 2, 3, 4, 5, 6], nextId: 7 }`。\n\n## 基础用法\n\n第一个例子我们演示最基本的无限滚动写法。\n\n<code src=\"./demo/default.tsx\" />\n\n## 分页\n\n在数据固定场景下，我们有时候会用 `page` 和 `pageSize` 来请求新的分页数据。\n\n<code src=\"./demo/pagination.tsx\" />\n\n## 滚动自动加载\n\n在无限滚动场景中，我们最常见的是滚动到底部时自动加载。通过配置以下几个属性，即可实现滚动自动加载。\n\n- `options.target` 指定父级元素（父级元素需设置固定高度，且支持内部滚动）\n- `options.isNoMore` 判断是不是没有更多数据了\n- `options.direction` 滚动的方向，默认为向下滚动\n\n向下滚动示例\n<code src=\"./demo/scroll.tsx\" />\n\n向上滚动示例\n<code src=\"./demo/scrollTop.tsx\" />\n\n## 数据重置\n\n通过 `reload` 即可实现数据重置，下面示例我们演示在 `filter` 变化后，重置数据到第一页。\n\n<code src=\"./demo/reload.tsx\" />\n\n以上代码可以通过 `reloadDeps` 语法糖实现，当 `reloadDeps` 变化时，会自动触发 `reload`。\n\n```ts\nconst result = useInfiniteScroll(service, {\n  reloadDeps: [keyword]\n});\n```\n\n## 数据突变\n\n通过 `mutate`，我们可以直接修改当前 `data`。下面示例演示了删除某条数据。\n\n<code src=\"./demo/mutate.tsx\" />\n\n## API\n\n```ts\nexport type Data = { list: any[];[key: string]: any; };\nexport type Service<TData extends Data> = (currentData?: TData) => Promise<TData>;\n\nconst {\n  data: TData;\n  loading: boolean;\n  loadingMore: boolean;\n  error?: Error;\n  noMore: boolean;\n  loadMore: () => void;\n  loadMoreAsync: () => Promise<TData>;\n  reload: () => void;\n  reloadAsync: () => Promise<TData>;\n  cancel: () => void;\n  mutate: (data?: TData) => void;\n} = useInfiniteScroll<TData extends Data>(\n  service: (currentData?: TData) => Promise<TData>,\n  {\n    target?: BasicTarget;\n    isNoMore?: (data?: TData) => boolean;\n    threshold?: number;\n    manual?: boolean;\n    reloadDeps?: DependencyList;\n    onBefore?: () => void;\n    onSuccess?: (data: TData) => void;\n    onError?: (e: Error) => void;\n    onFinally?: (data?: TData, e?: Error) => void;\n  }\n);\n```\n\n### Result\n\n| 参数          | 说明                                                                       | 类型                     |\n| ------------- | -------------------------------------------------------------------------- | ------------------------ |\n| data          | service 返回的数据，其中的 `list` 属性为聚合后数据                         | `TData` \\| `undefined`   |\n| loading       | 是否正在进行首次请求                                                       | `boolean`                |\n| loadingMore   | 是否正在进行更多数据请求                                                   | `boolean`                |\n| noMore        | 是否没有更多数据了，配置 `options.isNoMore` 后生效                         | `boolean`                |\n| error         | 请求错误消息                                                               | `Error`                  |\n| loadMore      | 加载更多数据，会自动捕获异常，通过 `options.onError` 处理                  | `() => void`             |\n| loadMoreAsync | 加载更多数据，与 `loadMore` 行为一致，但返回的是 Promise，需要自行处理异常 | `() => Promise<TData>`   |\n| reload        | 加载第一页数据，会自动捕获异常，通过 `options.onError` 处理                | `() => void`             |\n| reloadAsync   | 加载第一页数据，与 `reload` 行为一致，但返回的是 Promise，需要自行处理异常 | `() => Promise<TData>`   |\n| mutate        | 直接修改 `data`                                                            | `(data?: TData) => void` |\n| cancel        | 忽略当前 Promise 的响应                                                    | `() => void`             |\n\n### Options\n\n| 参数       | 说明                                                                                                                                                             | 类型                                                        | 默认值   |\n| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -------- |\n| target     | 父级容器，如果存在，则在滚动到底部时，自动触发 `loadMore`。需要配合 `isNoMore` 使用，以便知道什么时候到最后一页了。 **当 target 为 document 时，定义为整个视口** | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -        |\n| isNoMore   | 是否有最后一页的判断逻辑，入参为当前聚合后的 `data`                                                                                                              | `(data?: TData) => boolean`                                 | -        |\n| threshold  | 下拉自动加载，距离底部距离阈值                                                                                                                                   | `number`                                                    | `100`    |\n| direction  | 滚动的方向                                                                                                                                                       | `bottom` \\| `top`                                           | `bottom` |\n| reloadDeps | 变化后，会自动触发 `reload`                                                                                                                                      | `any[]`                                                     | -        |\n| manual     | <ul><li> 默认 `false`。 即在初始化时自动执行 service。</li><li>如果设置为 `true`，则需要手动调用 `reload` 或 `reloadAsync` 触发执行。 </li></ul>                 | `boolean`                                                   | `false`  |\n| onBefore   | service 执行前触发                                                                                                                                               | `() => void`                                                | -        |\n| onSuccess  | service resolve 时触发                                                                                                                                           | `(data: TData) => void`                                     | -        |\n| onError    | service reject 时触发                                                                                                                                            | `(e: Error) => void`                                        | -        |\n| onFinally  | service 执行完成时触发                                                                                                                                           | `(data?: TData, e?: Error) => void`                         | -        |\n"
  },
  {
    "path": "packages/hooks/src/useInfiniteScroll/types.ts",
    "content": "import type { DependencyList } from 'react';\nimport type { BasicTarget } from '../utils/domTarget';\n\nexport type Data = { list: any[]; [key: string]: any };\n\nexport type Service<TData extends Data> = (currentData?: TData) => Promise<TData>;\n\nexport interface InfiniteScrollResult<TData extends Data> {\n  data: TData;\n  loading: boolean;\n  loadingMore: boolean;\n  error?: Error;\n  noMore: boolean;\n\n  loadMore: () => void;\n  loadMoreAsync: () => Promise<TData>;\n  reload: () => void;\n  reloadAsync: () => Promise<TData>;\n  cancel: () => void;\n  mutate: (data?: TData) => void;\n}\n\nexport interface InfiniteScrollOptions<TData extends Data> {\n  target?: BasicTarget<Element | Document>;\n  isNoMore?: (data?: TData) => boolean;\n  threshold?: number;\n  direction?: 'bottom' | 'top';\n\n  manual?: boolean;\n  reloadDeps?: DependencyList;\n\n  onBefore?: () => void;\n  onSuccess?: (data: TData) => void;\n  onError?: (e: Error) => void;\n  onFinally?: (data?: TData, e?: Error) => void;\n}\n"
  },
  {
    "path": "packages/hooks/src/useInterval/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\nimport useInterval from '../index';\n\ninterface ParamsObj {\n  fn: (...arg: any) => any;\n  delay: number | undefined;\n  options?: { immediate: boolean };\n}\n\nconst setUp = ({ fn, delay, options }: ParamsObj) =>\n  renderHook(() => useInterval(fn, delay, options));\n\ndescribe('useInterval', () => {\n  vi.useFakeTimers();\n  vi.spyOn(global, 'clearInterval');\n\n  test('interval should work', () => {\n    const callback = vi.fn();\n    setUp({ fn: callback, delay: 20 });\n    expect(callback).not.toBeCalled();\n    vi.advanceTimersByTime(70);\n    expect(callback).toHaveBeenCalledTimes(3);\n  });\n\n  test('interval should stop', () => {\n    const callback = vi.fn();\n\n    setUp({ fn: callback, delay: undefined });\n    vi.advanceTimersByTime(50);\n    expect(callback).toHaveBeenCalledTimes(0);\n\n    setUp({ fn: callback, delay: -2 });\n    vi.advanceTimersByTime(50);\n    expect(callback).toHaveBeenCalledTimes(0);\n  });\n\n  test('immediate in options should work', () => {\n    const callback = vi.fn();\n    setUp({ fn: callback, delay: 20, options: { immediate: true } });\n    expect(callback).toBeCalled();\n    expect(callback).toHaveBeenCalledTimes(1);\n    vi.advanceTimersByTime(50);\n    expect(callback).toHaveBeenCalledTimes(3);\n  });\n\n  test('interval should be clear', () => {\n    const callback = vi.fn();\n    const hook = setUp({ fn: callback, delay: 20 });\n\n    expect(callback).not.toBeCalled();\n\n    hook.result.current();\n    vi.advanceTimersByTime(70);\n    // not to be called\n    expect(callback).toHaveBeenCalledTimes(0);\n    expect(clearInterval).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useInterval/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Execute once per 1000ms.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 每1000ms，执行一次\n */\n\nimport { useState } from 'react';\nimport { useInterval } from 'ahooks';\n\nexport default () => {\n  const [count, setCount] = useState(0);\n\n  useInterval(() => {\n    setCount(count + 1);\n  }, 1000);\n\n  return <div>count: {count}</div>;\n};\n"
  },
  {
    "path": "packages/hooks/src/useInterval/demo/demo2.tsx",
    "content": "/**\n * title: Advanced usage\n * desc: Modify the delay to realize the timer interval change and pause.\n *\n * title.zh-CN: 进阶使用\n * desc.zh-CN: 动态修改 delay 以实现定时器间隔变化与暂停。\n */\n\nimport { useState } from 'react';\nimport { useInterval } from 'ahooks';\n\nexport default () => {\n  const [count, setCount] = useState(0);\n  const [interval, setInterval] = useState<number | undefined>(1000);\n\n  const clear = useInterval(() => {\n    setCount(count + 1);\n  }, interval);\n\n  return (\n    <div>\n      <p> count: {count} </p>\n      <p style={{ marginTop: 16 }}> interval: {interval} </p>\n      <button\n        onClick={() => setInterval((t) => (!!t ? t + 1000 : 1000))}\n        style={{ marginRight: 8 }}\n      >\n        interval + 1000\n      </button>\n      <button\n        style={{ marginRight: 8 }}\n        onClick={() => {\n          setInterval(1000);\n        }}\n      >\n        reset interval\n      </button>\n      <button onClick={clear}>clear</button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useInterval/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useInterval\n\nA hook that handles the `setInterval` timer function.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Advanced usage\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nuseInterval(\n  fn: () => void,\n  delay?: number | undefined,\n  options?: Options\n): fn: () => void;\n```\n\n### Params\n\n| Property | Description                                                                                                                                                   | Type                    |\n| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |\n| fn       | The function to be executed every `delay` milliseconds.                                                                                                       | `() => void`            |\n| delay    | The time in milliseconds, the timer should delay in between executions of the specified function. The timer will be cancelled if delay is set to `undefined`. | `number` \\| `undefined` |\n| options  | Config of the interval behavior.                                                                                                                              | `Options`               |\n\n### Options\n\n| Property  | Description                                                            | Type      | Default |\n| --------- | ---------------------------------------------------------------------- | --------- | ------- |\n| immediate | Whether the function should be executed immediately on first execution | `boolean` | `false` |\n\n### Result\n\n| Property      | Description    | Type         |\n| ------------- | -------------- | ------------ |\n| clearInterval | clear interval | `() => void` |\n"
  },
  {
    "path": "packages/hooks/src/useInterval/index.ts",
    "content": "import { useCallback, useEffect, useRef } from 'react';\nimport useMemoizedFn from '../useMemoizedFn';\nimport { isNumber } from '../utils';\n\nconst useInterval = (fn: () => void, delay?: number, options: { immediate?: boolean } = {}) => {\n  const timerCallback = useMemoizedFn(fn);\n  const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n  const clear = useCallback(() => {\n    if (timerRef.current) {\n      clearInterval(timerRef.current);\n    }\n  }, []);\n\n  useEffect(() => {\n    if (!isNumber(delay) || delay < 0) {\n      return;\n    }\n    if (options.immediate) {\n      timerCallback();\n    }\n    timerRef.current = setInterval(timerCallback, delay);\n    return clear;\n  }, [delay, options.immediate]);\n\n  return clear;\n};\n\nexport default useInterval;\n"
  },
  {
    "path": "packages/hooks/src/useInterval/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useInterval\n\n一个可以处理 setInterval 的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 进阶使用\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nuseInterval(\n  fn: () => void,\n  delay?: number | undefined,\n  options?: Options\n): fn: () => void;\n```\n\n### Params\n\n| 参数    | 说明                                            | 类型                    |\n| ------- | ----------------------------------------------- | ----------------------- |\n| fn      | 要定时调用的函数                                | `() => void`            |\n| delay   | 间隔时间，当设置值为 `undefined` 时会停止计时器 | `number` \\| `undefined` |\n| options | 配置计时器的行为                                | `Options`               |\n\n### Options\n\n| 参数      | 说明                     | 类型      | 默认值  |\n| --------- | ------------------------ | --------- | ------- |\n| immediate | 是否在首次渲染时立即执行 | `boolean` | `false` |\n\n### Result\n\n| 参数          | 说明       | 类型         |\n| ------------- | ---------- | ------------ |\n| clearInterval | 清除定时器 | `() => void` |\n"
  },
  {
    "path": "packages/hooks/src/useIsomorphicLayoutEffect/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\nimport useIsomorphicLayoutEffect from '../index';\n\ndescribe('useIsomorphicLayoutEffect', () => {\n  const callback = vi.fn();\n  const { result } = renderHook(() => useIsomorphicLayoutEffect(callback));\n\n  test('cheak return value', () => {\n    expect(result.current).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useIsomorphicLayoutEffect/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useIsomorphicLayoutEffect\n\nIn SSR mode, the following warning will appear when useLayoutEffect is used\n\n> ⚠️ Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://fb.me/react-uselayouteffect-ssr for common fixes.\n\nTo avoid this warning, useIsomorphicLayoutEffect can be used instead of useLayoutEffect.\n\nThe source code of useIsomorphicLayoutEffect is:\n\n```javascript\nconst useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : noop;\n```\n\nReturn useLayoutEffect for browser environment and a no-op in non-browser environments to avoid SSR warnings.\n\nFor more information, please refer to [useLayoutEffect and SSR](https://medium.com/@alexandereardon/uselayouteffect-and-ssr-192986cdcf7a)\n"
  },
  {
    "path": "packages/hooks/src/useIsomorphicLayoutEffect/index.ts",
    "content": "import { useLayoutEffect } from 'react';\nimport isBrowser from '../utils/isBrowser';\nimport noop from '../utils/noop';\n\nconst useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : noop;\n\nexport default useIsomorphicLayoutEffect;\n"
  },
  {
    "path": "packages/hooks/src/useIsomorphicLayoutEffect/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useIsomorphicLayoutEffect\n\n在 SSR 模式下，使用 useLayoutEffect 时，会出现以下警告\n\n> ⚠️ Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://fb.me/react-uselayouteffect-ssr for common fixes.\n\n为了避免该警告，可以使用 useIsomorphicLayoutEffect 代替 useLayoutEffect。\n\nuseIsomorphicLayoutEffect 源码如下：\n\n```js\nconst useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : noop;\n```\n\n在浏览器环境返回 useLayoutEffect，在非浏览器环境返回空函数以避免 SSR 警告。\n\n更多信息可以参考 [useLayoutEffect and SSR](https://medium.com/@alexandereardon/uselayouteffect-and-ssr-192986cdcf7a)\n"
  },
  {
    "path": "packages/hooks/src/useKeyPress/__tests__/index.spec.tsx",
    "content": "import { fireEvent, renderHook } from '@testing-library/react';\nimport { afterEach, describe, expect, test, vi } from 'vitest';\nimport useKeyPress from '../index';\n\nconst callback = vi.fn();\n\nafterEach(() => {\n  callback.mockClear();\n});\n\ndescribe('useKeyPress ', () => {\n  test('test single key', async () => {\n    const { unmount } = renderHook(() => useKeyPress(['c'], callback));\n    fireEvent.keyDown(document, { key: 'c', keyCode: 67 });\n    expect(callback.mock.calls.length).toBe(1);\n    unmount();\n  });\n\n  test('test standard key aliases', async () => {\n    const { unmount } = renderHook(() => useKeyPress(['arrowleft', 'escape'], callback));\n    fireEvent.keyDown(document, { key: 'ArrowLeft', keyCode: 37 });\n    fireEvent.keyDown(document, { key: 'Escape', keyCode: 27 });\n    expect(callback.mock.calls.length).toBe(2);\n    unmount();\n  });\n\n  test('test standard vs legacy key aliases', async () => {\n    const aliasCallback = vi.fn();\n    const { unmount } = renderHook(() =>\n      useKeyPress(\n        [\n          'control',\n          'ctrl',\n          'escape',\n          'esc',\n          'arrowleft',\n          'leftarrow',\n          'spacebar',\n          'space',\n          'contextmenu',\n          'selectkey',\n          'pause',\n          'pausebreak',\n        ],\n        aliasCallback,\n      ),\n    );\n\n    fireEvent.keyDown(document, { key: 'Control', keyCode: 17, ctrlKey: true });\n    fireEvent.keyDown(document, { key: 'Escape', keyCode: 27 });\n    fireEvent.keyDown(document, { key: 'ArrowLeft', keyCode: 37 });\n    fireEvent.keyDown(document, { key: ' ', keyCode: 32 });\n    fireEvent.keyDown(document, { key: 'ContextMenu', keyCode: 93 });\n    fireEvent.keyDown(document, { key: 'Pause', keyCode: 19 });\n\n    // each event should match once (first alias hit)\n    expect(aliasCallback.mock.calls.length).toBe(6);\n    unmount();\n  });\n\n  test('test modifier key', async () => {\n    const { unmount } = renderHook(() => useKeyPress(['ctrl'], callback));\n    fireEvent.keyDown(document, { key: 'ctrl', keyCode: 17, ctrlKey: true });\n    expect(callback.mock.calls.length).toBe(1);\n    unmount();\n  });\n\n  test('test combination keys', async () => {\n    const hook1 = renderHook(() => useKeyPress(['shift.c'], callback));\n    const hook2 = renderHook(() => useKeyPress(['shift'], callback));\n    const hook3 = renderHook(() => useKeyPress(['c'], callback));\n\n    fireEvent.keyDown(document, { key: 'c', shiftKey: true, keyCode: 67 });\n\n    expect(callback.mock.calls.length).toBe(3);\n    hook1.unmount();\n    hook2.unmount();\n    hook3.unmount();\n  });\n\n  test('test combination keys by exact match', async () => {\n    const callbackShift = vi.fn();\n    const callbackC = vi.fn();\n    const callbackMulti = vi.fn();\n    const hook1 = renderHook(() => useKeyPress(['shift.c'], callback, { exactMatch: true }));\n    const hook2 = renderHook(() => useKeyPress(['shift'], callbackShift, { exactMatch: true }));\n    const hook3 = renderHook(() => useKeyPress(['c'], callbackC, { exactMatch: true }));\n    const hook4 = renderHook(() => useKeyPress(['ctrl.shift.c'], callbackMulti));\n\n    fireEvent.keyDown(document, { key: 'c', shiftKey: true, keyCode: 67 });\n    /**\n     * 只有 shift.c 才会触发，shift 和 c 都不应该触发\n     */\n    expect(callback.mock.calls.length).toBe(1);\n    expect(callbackShift.mock.calls.length).toBe(0);\n    expect(callbackC.mock.calls.length).toBe(0);\n\n    callback.mockClear();\n    fireEvent.keyDown(document, { key: 'c', ctrlKey: true, shiftKey: true, keyCode: 67 });\n    expect(callbackMulti.mock.calls.length).toBe(1);\n    expect(callback.mock.calls.length).toBe(0);\n    expect(callbackC.mock.calls.length).toBe(0);\n\n    hook1.unmount();\n    hook2.unmount();\n    hook3.unmount();\n    hook4.unmount();\n  });\n\n  test('test multiple keys', async () => {\n    const { unmount } = renderHook(() => useKeyPress(['0', 65], callback));\n    fireEvent.keyDown(document, { key: '0', keyCode: 48 });\n    fireEvent.keyDown(document, { key: 'a', keyCode: 65 });\n    expect(callback.mock.calls.length).toBe(2);\n    unmount();\n  });\n\n  test('meta key should be work in keyup event', async () => {\n    renderHook(() =>\n      useKeyPress(['meta'], callback, {\n        events: ['keyup'],\n      }),\n    );\n\n    fireEvent.keyUp(document, { key: 'meta', keyCode: 91, metaKey: false });\n    expect(callback).toBeCalled();\n  });\n\n  test('test `keyFilter` function parameter', async () => {\n    const callback1 = vi.fn();\n    const callback2 = vi.fn();\n\n    // all keys can trigger callback\n    const hook1 = renderHook(() => useKeyPress(() => true, callback1));\n    fireEvent.keyDown(document, { key: '0', keyCode: 48 });\n    fireEvent.keyDown(document, { key: 'a', keyCode: 65 });\n    expect(callback1.mock.calls.length).toBe(2);\n\n    // only some keys can trigger callback\n    const hook2 = renderHook(() => useKeyPress((e) => ['0', 'meta'].includes(e.key), callback2));\n    fireEvent.keyDown(document, { key: '0', keyCode: 48 });\n    fireEvent.keyDown(document, { key: '1', keyCode: 49 });\n    fireEvent.keyDown(document, { key: 'ctrl', keyCode: 17, ctrlKey: true });\n    fireEvent.keyDown(document, { key: 'meta', keyCode: 91, metaKey: true });\n    expect(callback2.mock.calls.length).toBe(2);\n\n    hook1.unmount();\n    hook2.unmount();\n  });\n\n  test('test key in `eventHandler` parameter', async () => {\n    let pressedKey;\n    const KEYS = ['c', 'shift.c', 'shift.ctrl.c'];\n    const callbackKey = (e: any, key: any) => {\n      pressedKey = key;\n    };\n\n    // test `exactMatch: true` props\n    const hook1 = renderHook(() => useKeyPress(KEYS, callbackKey, { exactMatch: true }));\n    fireEvent.keyDown(document, { key: 'c', keyCode: 67 });\n    expect(pressedKey).toBe('c');\n    fireEvent.keyDown(document, { key: 'c', keyCode: 67, shiftKey: true });\n    expect(pressedKey).toBe('shift.c');\n    fireEvent.keyDown(document, { key: 'c', keyCode: 67, shiftKey: true, ctrlKey: true });\n    expect(pressedKey).toBe('shift.ctrl.c');\n\n    // test `exactMatch: false`(default) props\n    const hook2 = renderHook(() => useKeyPress(KEYS, callbackKey));\n    fireEvent.keyDown(document, { key: 'c', keyCode: 67 });\n    expect(pressedKey).toBe('c');\n    fireEvent.keyDown(document, { key: 'c', keyCode: 67, shiftKey: true });\n    expect(pressedKey).toBe('c');\n    fireEvent.keyDown(document, { key: 'c', keyCode: 67, shiftKey: true, ctrlKey: true });\n    expect(pressedKey).toBe('c');\n\n    hook2.unmount();\n    hook1.unmount();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useKeyPress/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Supported keyCode and alias in keyboard events, pressing ArrowUp or ArrowDown to show effect.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 支持键盘事件中的 keyCode 和别名，请按 ArrowUp 或 ArrowDown 键进行演示。\n */\n\nimport { useState } from 'react';\nimport { useKeyPress } from 'ahooks';\n\nexport default () => {\n  const [counter, setCounter] = useState(0);\n\n  useKeyPress('uparrow', () => {\n    setCounter((s) => s + 1);\n  });\n\n  // keyCode value for ArrowDown\n  useKeyPress(40, () => {\n    setCounter((s) => s - 1);\n  });\n\n  return (\n    <div>\n      <p>Try pressing the following: </p>\n      <div>1. Press ArrowUp by key to increase</div>\n      <div>2. Press ArrowDown by keyCode to decrease</div>\n      <div>\n        counter: <span style={{ color: '#f00' }}>{counter}</span>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useKeyPress/demo/demo2.tsx",
    "content": "/**\n * title: Use key aliases\n * desc: Support using key aliases. Please refer to the [document](#remarks) below.\n *\n * title.zh-CN: 使用别名\n * desc.zh-CN: 支持使用别名，更多内容请[查看备注](#remarks)。\n */\n\nimport { useState } from 'react';\nimport { useKeyPress } from 'ahooks';\n\nexport default () => {\n  const [counter, setCounter] = useState(0);\n\n  useKeyPress('leftarrow', () => {\n    setCounter((s) => s - 1);\n  });\n\n  useKeyPress('rightarrow', () => {\n    setCounter((s) => s + 1);\n  });\n\n  return (\n    <div>\n      <p>Try pressing the following: </p>\n      <div>1. Press ArrowLeft to decrease</div>\n      <div>2. Press ArrowRight to increase</div>\n      <div>\n        counter: <span style={{ color: '#f00' }}>{counter}</span>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useKeyPress/demo/demo3.tsx",
    "content": "import { useKeyPress } from 'ahooks';\nimport { useState } from 'react';\n\nexport default () => {\n  const [num, setNum] = useState<string>();\n  const [key, setKey] = useState<string>();\n\n  const filterKey = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];\n  useKeyPress(filterKey, (event) => {\n    setNum(event.key);\n  });\n\n  // a s d f, Backspace, 8\n  useKeyPress([65, 83, 68, 70, 8, '8'], (event) => {\n    setKey(event.key);\n  });\n\n  return (\n    <div>\n      <p>Try pressing the following: </p>\n      <div>\n        1. Number key [0-9]: <span style={{ color: '#f00' }}>{num}</span>\n      </div>\n      <div>\n        2. Press key [a, s, d, f, Backspace, 8]: <span style={{ color: '#f00' }}>{key}</span>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useKeyPress/demo/demo4.tsx",
    "content": "/**\n * title: Advanced\n * desc: Supports receiving a Boolean callback function to handle preprocessing operations.\n *\n * title.zh-CN: 进阶使用\n * desc.zh-CN: 支持接收一个返回 boolean 的回调函数，自己处理逻辑。\n */\n\nimport { useState } from 'react';\nimport { useKeyPress } from 'ahooks';\n\nexport default () => {\n  const [key, setKey] = useState<string>();\n  const filterKey = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];\n  useKeyPress(\n    (event) => !filterKey.includes(event.key),\n    (event) => {\n      if (event.type === 'keyup') {\n        setKey(event.key);\n      }\n    },\n    {\n      events: ['keydown', 'keyup'],\n    },\n  );\n\n  return (\n    <div>\n      Pressing key except number key：<span style={{ color: '#f00' }}>{key}</span>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useKeyPress/demo/demo5.tsx",
    "content": "/**\n * title: Custom DOM\n * desc: By default, listen for events on the window. You can also pass in a DOM to set listen area. such as the common listening for input box events.\n *\n * title.zh-CN: 自定义 DOM\n * desc.zh-CN: |\n *  默认监听挂载在 window 上的事件，你也可以传入 DOM 指定监听区域。\n *\n *  如常见的监听输入框事件，支持多种 DOM 指定方式。\n */\n\nimport { useState, useRef } from 'react';\nimport { useKeyPress } from 'ahooks';\n\nexport default () => {\n  const inputRef = useRef(null);\n\n  const [text, setText] = useState('');\n  const [textRef, setTextRef] = useState('');\n  const [textSync, setTextSync] = useState('');\n  useKeyPress(\n    'enter',\n    (event: any) => {\n      const { value } = event.target;\n      setText(value);\n    },\n    {\n      events: ['keyup'],\n      target: () => document.getElementById('input'),\n    },\n  );\n\n  useKeyPress(\n    'enter',\n    (event: any) => {\n      const { value } = event.target;\n      setTextRef(value);\n    },\n    {\n      target: inputRef,\n    },\n  );\n\n  // Make sure the DOM exists\n  useKeyPress(\n    () => true,\n    (event: any) => {\n      const { value } = event.target;\n      setTextSync(value);\n    },\n    {\n      events: ['keyup'],\n      target: document.getElementById('input2'),\n    },\n  );\n\n  return (\n    <div>\n      <div>\n        <p>Input and pressing enter: {text}</p>\n        <input id=\"input\" style={{ width: 300, marginRight: 24 }} />\n      </div>\n      <div style={{ marginTop: 24 }}>\n        <p>Input and pressing enter: {textRef}</p>\n        <input ref={inputRef} style={{ width: 300, marginRight: 24 }} />\n      </div>\n      <div style={{ marginTop: 24 }}>\n        <p>Input after enter change: {textSync}</p>\n        <input id=\"input2\" style={{ width: 300, marginRight: 24 }} />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useKeyPress/demo/demo6.tsx",
    "content": "import { CheckOutlined } from '@ant-design/icons';\nimport { useKeyPress } from 'ahooks';\nimport { useState } from 'react';\n\nexport default () => {\n  const [state, setState] = useState<number>();\n\n  useKeyPress(['shift.c'], () => {\n    setState(1);\n  });\n\n  useKeyPress(['meta'], () => {\n    setState(2);\n  });\n\n  useKeyPress('ctrl.alt.c', () => {\n    setState(3);\n  });\n\n  useKeyPress('ctrl.enter', () => {\n    setState(4);\n  });\n\n  // Attention: event.key === '0'\n  useKeyPress('ctrl.alt.0', () => {\n    setState(5);\n  });\n\n  return (\n    <div>\n      <p>Try pressing the following: </p>\n      <div>\n        1. Modifier key [shift.c]: {state === 1 && <CheckOutlined style={{ color: '#f00' }} />}\n      </div>\n      <div>\n        2. Modifier key [meta]: {state === 2 && <CheckOutlined style={{ color: '#f00' }} />}\n      </div>\n      <div>\n        3. Modifier key [ctrl.alt.c]: {state === 3 && <CheckOutlined style={{ color: '#f00' }} />}\n      </div>\n      <div>\n        4. Modifier key [ctrl.enter]: {state === 4 && <CheckOutlined style={{ color: '#f00' }} />}\n      </div>\n      <div>\n        5. Modifier key [ctrl.alt.0]: {state === 5 && <CheckOutlined style={{ color: '#f00' }} />}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useKeyPress/demo/demo7.tsx",
    "content": "/**\n * title: Exact match\n * desc: Enable exact matching by setting `exactMatch`. For example, press [shift + c], will not trigger [c].\n *\n * title.zh-CN: 精确匹配\n * desc.zh-CN: 通过配置 `exactMatch`, 开启精确匹配。比如按 [shift + c] ，不会触发 [c]。\n */\n\nimport { CheckOutlined } from '@ant-design/icons';\nimport { useKeyPress } from 'ahooks';\nimport { useState } from 'react';\n\nexport default () => {\n  const [state, setState] = useState<number>();\n\n  useKeyPress(['shift.c'], () => {\n    setState(1);\n  });\n\n  useKeyPress(\n    ['c'],\n    () => {\n      setState(2);\n    },\n    {\n      exactMatch: true,\n    },\n  );\n\n  return (\n    <div>\n      <p>Try pressing the following: </p>\n      <div>\n        1. Modifier key [shift.c]: {state === 1 && <CheckOutlined style={{ color: '#f00' }} />}\n      </div>\n      <div>2. Modifier key [c]: {state === 2 && <CheckOutlined style={{ color: '#f00' }} />}</div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useKeyPress/demo/demo8.tsx",
    "content": "/**\n * title: Get the trigger key\n * desc: Multiple shortcuts are registered by a hook, each corresponding to a different logic.\n *\n * title.zh-CN: 获取触发的按键\n * desc.zh-CN: 单个 hook 注册多个快捷键，每个快捷键对应不同逻辑。\n */\n\nimport { useState } from 'react';\nimport { useKeyPress } from 'ahooks';\n\nexport default () => {\n  const [count, setCount] = useState<number>(0);\n\n  const keyCallbackMap = {\n    w: () => {\n      setCount((prev) => prev + 1);\n    },\n    s: () => {\n      setCount((prev) => prev - 1);\n    },\n    'shift.c': () => {\n      setCount(0);\n    },\n  };\n\n  useKeyPress(['w', 's', 'shift.c'], (e, key) => {\n    keyCallbackMap[key as keyof typeof keyCallbackMap]();\n  });\n\n  return (\n    <div>\n      <p>Try pressing the following: </p>\n      <div>1. Press [w] to increase</div>\n      <div>2. Press [s] to decrease</div>\n      <div>3. Press [shift.c] to reset</div>\n      <p>\n        counter: <span style={{ color: '#f00' }}>{count}</span>\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useKeyPress/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useKeyPress\n\nListen for the keyboard press, support key combinations, and support alias.\n\n## Examples\n\n### Basic usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Combination keys\n\n<code src=\"./demo/demo6.tsx\" />\n\n### Exact match\n\n<code src=\"./demo/demo7.tsx\"/>\n\n### Multiple keys\n\n<code src=\"./demo/demo3.tsx\" />\n\n### Get the trigger key\n\n<code src=\"./demo/demo8.tsx\" />\n\n### Custom method\n\n<code src=\"./demo/demo4.tsx\" />\n\n### Custom DOM\n\n<code src=\"./demo/demo5.tsx\" />\n\n## API\n\n```typescript\ntype KeyType = number | string;\ntype KeyFilter = KeyType | KeyType[] | ((event: KeyboardEvent) => boolean);\n\nuseKeyPress(\n  keyFilter: KeyFilter,\n  eventHandler: (event: KeyboardEvent, key: KeyType) => void,\n  options?: Options\n);\n```\n\n### Params\n\n| Property     | Description                                                      | Type                                                            | Default |\n| ------------ | ---------------------------------------------------------------- | --------------------------------------------------------------- | ------- |\n| keyFilter    | Support keyCode、alias、combination keys、array、custom function | `KeyType` \\| `KeyType[]` \\| `(event: KeyboardEvent) => boolean` | -       |\n| eventHandler | Callback function                                                | `(event: KeyboardEvent, key: KeyType) => void`                  | -       |\n| options      | Advanced options                                                 | `Options`                                                       | -       |\n\n### Options\n\n| Property   | Description                                                                                                                                    | Type                                                        | Default       |\n| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------- |\n| events     | Trigger Events                                                                                                                                 | `('keydown' \\| 'keyup')[]`                                  | `['keydown']` |\n| target     | DOM element or ref                                                                                                                             | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -             |\n| exactMatch | Exact match. If set `true`, the event will only be trigger when the keys match exactly. For example, pressing [shift + c] will not trigger [c] | `boolean`                                                   | `false`       |\n| useCapture | to block events bubbling                                                                                                                       | `boolean`                                                   | `false`       |\n\n## Remarks\n\n1. Supports part of standard browser key values. Reference: [MDN keyboard key values](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values). Full alias list refers to [code](https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useKeyPress/index.ts#L21)\n2. Modifier keys\n\n```text\nctrl\nalt\nshift\nmeta\n```\n"
  },
  {
    "path": "packages/hooks/src/useKeyPress/index.ts",
    "content": "import useLatest from '../useLatest';\nimport { isFunction, isNumber, isString } from '../utils';\nimport type { BasicTarget } from '../utils/domTarget';\nimport { getTargetElement } from '../utils/domTarget';\nimport useDeepCompareEffectWithTarget from '../utils/useDeepCompareWithTarget';\nimport isAppleDevice from '../utils/isAppleDevice';\n\nexport type KeyType = number | string;\nexport type KeyPredicate = (event: KeyboardEvent) => KeyType | boolean | undefined;\nexport type KeyFilter = KeyType | KeyType[] | ((event: KeyboardEvent) => boolean);\nexport type KeyEvent = 'keydown' | 'keyup';\n\nexport type Target = BasicTarget<HTMLElement | Document | Window>;\n\nexport type Options = {\n  events?: KeyEvent[];\n  target?: Target;\n  exactMatch?: boolean;\n  useCapture?: boolean;\n};\n\n// 键盘事件 keyCode 别名\nconst aliasKeyCodeMap = {\n  '0': 48,\n  '1': 49,\n  '2': 50,\n  '3': 51,\n  '4': 52,\n  '5': 53,\n  '6': 54,\n  '7': 55,\n  '8': 56,\n  '9': 57,\n  backspace: 8,\n  tab: 9,\n  enter: 13,\n  shift: 16,\n  // Legacy alias kept for compatibility; standard name is \"control\".\n  ctrl: 17,\n  control: 17,\n  alt: 18,\n  // Legacy alias kept for compatibility; standard name is \"pause\".\n  pausebreak: 19,\n  pause: 19,\n  capslock: 20,\n  // Legacy alias kept for compatibility; standard name is \"escape\".\n  esc: 27,\n  escape: 27,\n  // Legacy alias kept for compatibility; standard name is \"spacebar\" (non-standard but widely used).\n  space: 32,\n  spacebar: 32,\n  pageup: 33,\n  pagedown: 34,\n  end: 35,\n  home: 36,\n  // Legacy aliases kept for compatibility; standard names are \"arrowleft/arrowup/arrowright/arrowdown\".\n  leftarrow: 37,\n  arrowleft: 37,\n  uparrow: 38,\n  arrowup: 38,\n  rightarrow: 39,\n  arrowright: 39,\n  downarrow: 40,\n  arrowdown: 40,\n  insert: 45,\n  delete: 46,\n  a: 65,\n  b: 66,\n  c: 67,\n  d: 68,\n  e: 69,\n  f: 70,\n  g: 71,\n  h: 72,\n  i: 73,\n  j: 74,\n  k: 75,\n  l: 76,\n  m: 77,\n  n: 78,\n  o: 79,\n  p: 80,\n  q: 81,\n  r: 82,\n  s: 83,\n  t: 84,\n  u: 85,\n  v: 86,\n  w: 87,\n  x: 88,\n  y: 89,\n  z: 90,\n  leftwindowkey: 91,\n  rightwindowkey: 92,\n  meta: isAppleDevice ? [91, 93] : [91, 92],\n  // Legacy alias kept for compatibility; standard name is \"contextmenu\".\n  selectkey: 93,\n  contextmenu: 93,\n  numpad0: 96,\n  numpad1: 97,\n  numpad2: 98,\n  numpad3: 99,\n  numpad4: 100,\n  numpad5: 101,\n  numpad6: 102,\n  numpad7: 103,\n  numpad8: 104,\n  numpad9: 105,\n  multiply: 106,\n  add: 107,\n  subtract: 109,\n  decimalpoint: 110,\n  divide: 111,\n  f1: 112,\n  f2: 113,\n  f3: 114,\n  f4: 115,\n  f5: 116,\n  f6: 117,\n  f7: 118,\n  f8: 119,\n  f9: 120,\n  f10: 121,\n  f11: 122,\n  f12: 123,\n  numlock: 144,\n  scrolllock: 145,\n  semicolon: 186,\n  equalsign: 187,\n  comma: 188,\n  dash: 189,\n  period: 190,\n  forwardslash: 191,\n  graveaccent: 192,\n  openbracket: 219,\n  backslash: 220,\n  closebracket: 221,\n  singlequote: 222,\n};\n\n// 修饰键\nconst modifierKey = {\n  ctrl: (event: KeyboardEvent) => event.ctrlKey,\n  shift: (event: KeyboardEvent) => event.shiftKey,\n  alt: (event: KeyboardEvent) => event.altKey,\n  meta: (event: KeyboardEvent) => {\n    if (event.type === 'keyup') {\n      return aliasKeyCodeMap.meta.includes(event.keyCode);\n    }\n    return event.metaKey;\n  },\n};\n\n// 判断合法的按键类型\nfunction isValidKeyType(value: unknown): value is string | number {\n  return isString(value) || isNumber(value);\n}\n\n// 根据 event 计算激活键数量\nfunction countKeyByEvent(event: KeyboardEvent) {\n  const countOfModifier = Object.keys(modifierKey).reduce((total, key) => {\n    if ((modifierKey as any)[key](event)) {\n      return total + 1;\n    }\n\n    return total;\n  }, 0);\n\n  // 16 17 18 91 92 是修饰键的 keyCode，如果 keyCode 是修饰键，那么激活数量就是修饰键的数量，如果不是，那么就需要 +1\n  return [16, 17, 18, 91, 92].includes(event.keyCode) ? countOfModifier : countOfModifier + 1;\n}\n\n/**\n * 判断按键是否激活\n * @param [event: KeyboardEvent]键盘事件\n * @param [keyFilter: any] 当前键\n * @returns string | number | boolean\n */\nfunction genFilterKey(event: KeyboardEvent, keyFilter: KeyType, exactMatch: boolean) {\n  // 浏览器自动补全 input 的时候，会触发 keyDown、keyUp 事件，但此时 event.key 等为空\n  if (!event.key) {\n    return false;\n  }\n\n  // 数字类型直接匹配事件的 keyCode\n  if (isNumber(keyFilter)) {\n    return event.keyCode === keyFilter ? keyFilter : false;\n  }\n\n  // 字符串依次判断是否有组合键\n  const genArr = keyFilter.split('.');\n  let genLen = 0;\n\n  for (const key of genArr) {\n    // 组合键\n    const genModifier = (modifierKey as any)[key];\n    // keyCode 别名\n    const aliasKeyCode: number | number[] = (aliasKeyCodeMap as any)[key.toLowerCase()];\n\n    if ((genModifier && genModifier(event)) || (aliasKeyCode && aliasKeyCode === event.keyCode)) {\n      genLen++;\n    }\n  }\n\n  /**\n   * 需要判断触发的键位和监听的键位完全一致，判断方法就是触发的键位里有且等于监听的键位\n   * genLen === genArr.length 能判断出来触发的键位里有监听的键位\n   * countKeyByEvent(event) === genArr.length 判断出来触发的键位数量里有且等于监听的键位数量\n   * 主要用来防止按组合键其子集也会触发的情况，例如监听 ctrl+a 会触发监听 ctrl 和 a 两个键的事件。\n   */\n  if (exactMatch) {\n    return genLen === genArr.length && countKeyByEvent(event) === genArr.length ? keyFilter : false;\n  }\n  return genLen === genArr.length ? keyFilter : false;\n}\n\n/**\n * 键盘输入预处理方法\n * @param [keyFilter: any] 当前键\n * @returns () => Boolean\n */\nfunction genKeyFormatter(keyFilter: KeyFilter, exactMatch: boolean): KeyPredicate {\n  if (isFunction(keyFilter)) {\n    return keyFilter;\n  }\n  if (isValidKeyType(keyFilter)) {\n    return (event: KeyboardEvent) => genFilterKey(event, keyFilter, exactMatch);\n  }\n  if (Array.isArray(keyFilter)) {\n    return (event: KeyboardEvent) =>\n      keyFilter.find((item) => genFilterKey(event, item, exactMatch));\n  }\n  return () => Boolean(keyFilter);\n}\n\nconst defaultEvents: KeyEvent[] = ['keydown'];\n\nfunction useKeyPress(\n  keyFilter: KeyFilter,\n  eventHandler: (event: KeyboardEvent, key: KeyType) => void,\n  option?: Options,\n) {\n  const { events = defaultEvents, target, exactMatch = false, useCapture = false } = option || {};\n  const eventHandlerRef = useLatest(eventHandler);\n  const keyFilterRef = useLatest(keyFilter);\n\n  useDeepCompareEffectWithTarget(\n    () => {\n      const el = getTargetElement(target, window);\n      if (!el) {\n        return;\n      }\n\n      const callbackHandler = (event: Event) => {\n        const keyEvent = event as KeyboardEvent;\n        const genGuard = genKeyFormatter(keyFilterRef.current, exactMatch);\n        const keyGuard = genGuard(keyEvent);\n        const firedKey = isValidKeyType(keyGuard) ? keyGuard : keyEvent.key;\n\n        if (keyGuard) {\n          return eventHandlerRef.current?.(keyEvent, firedKey);\n        }\n      };\n\n      for (const eventName of events) {\n        el?.addEventListener?.(eventName, callbackHandler, useCapture);\n      }\n      return () => {\n        for (const eventName of events) {\n          el?.removeEventListener?.(eventName, callbackHandler, useCapture);\n        }\n      };\n    },\n    [events],\n    target,\n  );\n}\n\nexport default useKeyPress;\n"
  },
  {
    "path": "packages/hooks/src/useKeyPress/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useKeyPress\n\n监听键盘按键，支持组合键，支持按键别名。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 监听组合按键\n\n<code src=\"./demo/demo6.tsx\" />\n\n### 精确匹配\n\n<code src=\"./demo/demo7.tsx\" />\n\n### 监听多个按键\n\n<code src=\"./demo/demo3.tsx\" />\n\n### 获取触发的按键\n\n<code src=\"./demo/demo8.tsx\" />\n\n### 自定义监听方式\n\n<code src=\"./demo/demo4.tsx\" />\n\n### 自定义 DOM\n\n<code src=\"./demo/demo5.tsx\" />\n\n## API\n\n```typescript\ntype KeyType = number | string;\ntype KeyFilter = KeyType | KeyType[] | ((event: KeyboardEvent) => boolean);\n\nuseKeyPress(\n  keyFilter: KeyFilter,\n  eventHandler: (event: KeyboardEvent, key: KeyType) => void,\n  options?: Options\n);\n```\n\n### Params\n\n| 参数         | 说明                                         | 类型                                                            | 默认值 |\n| ------------ | -------------------------------------------- | --------------------------------------------------------------- | ------ |\n| keyFilter    | 支持 keyCode、别名、组合键、数组、自定义函数 | `KeyType` \\| `KeyType[]` \\| `(event: KeyboardEvent) => boolean` | -      |\n| eventHandler | 回调函数                                     | `(event: KeyboardEvent, key: KeyType) => void`                  | -      |\n| options      | 可选配置项                                   | `Options`                                                       | -      |\n\n### Options\n\n| 参数       | 说明                                                                                        | 类型                                                        | 默认值        |\n| ---------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------- |\n| events     | 触发事件                                                                                    | `('keydown' \\| 'keyup')[]`                                  | `['keydown']` |\n| target     | DOM 节点或者 ref                                                                            | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -             |\n| exactMatch | 精确匹配。如果开启，则只有在按键完全匹配的情况下触发事件。比如按键 [shift + c] 不会触发 [c] | `boolean`                                                   | `false`       |\n| useCapture | 是否阻止事件冒泡                                                                            | `boolean`                                                   | `false`       |\n\n## Remarks\n\n1. 支持部分浏览器标准按键值。参考文档：[MDN 键值表](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)。完整列表见 [代码](https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useKeyPress/index.ts#L21)\n2. 修饰键\n\n```text\nctrl\nalt\nshift\nmeta\n```\n"
  },
  {
    "path": "packages/hooks/src/useLatest/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useLatest from '../index';\n\nconst setUp = (val: any) => renderHook((state) => useLatest(state), { initialProps: val });\n\ndescribe('useLatest', () => {\n  test('useLatest with basic variable should work', async () => {\n    const { result, rerender } = setUp(0);\n\n    rerender(1);\n    expect(result.current.current).toBe(1);\n\n    rerender(2);\n    expect(result.current.current).toBe(2);\n\n    rerender(3);\n    expect(result.current.current).toBe(3);\n  });\n\n  test('useLatest with reference variable should work', async () => {\n    const val1 = {};\n    const { result, rerender } = setUp(val1);\n\n    expect(result.current.current).toBe(val1);\n\n    const val2: any[] = [];\n    rerender(val2);\n    expect(result.current.current).toBe(val2);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useLatest/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: useLatest always returns the latest value\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: useLatest 返回的永远是最新值\n */\n\nimport { useState, useEffect } from 'react';\nimport { useLatest } from 'ahooks';\n\nexport default () => {\n  const [count, setCount] = useState(0);\n  const [count2, setCount2] = useState(0);\n\n  const latestCountRef = useLatest(count);\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setCount(latestCountRef.current + 1);\n    }, 1000);\n    return () => clearInterval(interval);\n  }, []);\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setCount2(count2 + 1);\n    }, 1000);\n    return () => clearInterval(interval);\n  }, []);\n\n  return (\n    <>\n      <p>count(useLatest): {count}</p>\n      <p>count(default): {count2}</p>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useLatest/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useLatest\n\nA Hook that returns the latest value, effectively avoiding the closure problem.\n\n## Examples\n\n### Basic usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst latestValueRef = useLatest<T>(value: T): MutableRefObject<T>;\n```\n"
  },
  {
    "path": "packages/hooks/src/useLatest/index.ts",
    "content": "import { useRef } from 'react';\n\nfunction useLatest<T>(value: T) {\n  const ref = useRef(value);\n  ref.current = value;\n\n  return ref;\n}\n\nexport default useLatest;\n"
  },
  {
    "path": "packages/hooks/src/useLatest/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useLatest\n\n返回当前最新值的 Hook，可以避免闭包问题。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst latestValueRef = useLatest<T>(value: T): MutableRefObject<T>;\n```\n"
  },
  {
    "path": "packages/hooks/src/useLocalStorageState/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport type { Options } from '../../createUseStorageState';\nimport useLocalStorageState from '../index';\n\ndescribe('useLocalStorageState', () => {\n  const setUp = <T>(key: string, value: T, options?: Options<T>) =>\n    renderHook(() => {\n      const [state, setState] = useLocalStorageState<T>(key, {\n        defaultValue: value,\n        ...options,\n      });\n      return {\n        state,\n        setState,\n      } as const;\n    });\n\n  test('getKey should work', () => {\n    const LOCAL_STORAGE_KEY = 'test-key';\n    const hook = setUp(LOCAL_STORAGE_KEY, 'A');\n    expect(hook.result.current.state).toBe('A');\n    act(() => {\n      hook.result.current.setState('B');\n    });\n    expect(hook.result.current.state).toBe('B');\n    const anotherHook = setUp(LOCAL_STORAGE_KEY, 'A');\n    expect(anotherHook.result.current.state).toBe('B');\n    act(() => {\n      anotherHook.result.current.setState('C');\n    });\n    expect(anotherHook.result.current.state).toBe('C');\n    expect(hook.result.current.state).toBe('B');\n  });\n\n  test('should support object', () => {\n    const LOCAL_STORAGE_KEY = 'test-object-key';\n    const hook = setUp<{ name: string }>(LOCAL_STORAGE_KEY, {\n      name: 'A',\n    });\n    expect(hook.result.current.state).toEqual({ name: 'A' });\n    act(() => {\n      hook.result.current.setState({ name: 'B' });\n    });\n    expect(hook.result.current.state).toEqual({ name: 'B' });\n    const anotherHook = setUp(LOCAL_STORAGE_KEY, {\n      name: 'C',\n    });\n    expect(anotherHook.result.current.state).toEqual({ name: 'B' });\n    act(() => {\n      anotherHook.result.current.setState({\n        name: 'C',\n      });\n    });\n    expect(anotherHook.result.current.state).toEqual({ name: 'C' });\n    expect(hook.result.current.state).toEqual({ name: 'B' });\n  });\n\n  test('should support number', () => {\n    const LOCAL_STORAGE_KEY = 'test-number-key';\n    const hook = setUp(LOCAL_STORAGE_KEY, 1);\n    expect(hook.result.current.state).toBe(1);\n    act(() => {\n      hook.result.current.setState(2);\n    });\n    expect(hook.result.current.state).toBe(2);\n    const anotherHook = setUp(LOCAL_STORAGE_KEY, 3);\n    expect(anotherHook.result.current.state).toBe(2);\n    act(() => {\n      anotherHook.result.current.setState(3);\n    });\n    expect(anotherHook.result.current.state).toBe(3);\n    expect(hook.result.current.state).toBe(2);\n  });\n\n  test('should support boolean', () => {\n    const LOCAL_STORAGE_KEY = 'test-boolean-key';\n    const hook = setUp(LOCAL_STORAGE_KEY, true);\n    expect(hook.result.current.state).toBe(true);\n    act(() => {\n      hook.result.current.setState(false);\n    });\n    expect(hook.result.current.state).toBe(false);\n    const anotherHook = setUp(LOCAL_STORAGE_KEY, true);\n    expect(anotherHook.result.current.state).toBe(false);\n    act(() => {\n      anotherHook.result.current.setState(true);\n    });\n    expect(anotherHook.result.current.state).toBe(true);\n    expect(hook.result.current.state).toBe(false);\n  });\n\n  test('should support null', () => {\n    const LOCAL_STORAGE_KEY = 'test-boolean-key-with-null';\n    const hook = setUp<boolean | null>(LOCAL_STORAGE_KEY, false);\n    expect(hook.result.current.state).toBe(false);\n    act(() => {\n      hook.result.current.setState(null);\n    });\n    expect(hook.result.current.state).toBeNull();\n    const anotherHook = setUp(LOCAL_STORAGE_KEY, false);\n    expect(anotherHook.result.current.state).toBeNull();\n  });\n\n  test('should support function updater', () => {\n    const LOCAL_STORAGE_KEY = 'test-func-updater';\n    const hook = setUp<string | null>(LOCAL_STORAGE_KEY, 'hello world');\n    expect(hook.result.current.state).toBe('hello world');\n    act(() => {\n      hook.result.current.setState((state) => `${state}, zhangsan`);\n    });\n    expect(hook.result.current.state).toBe('hello world, zhangsan');\n  });\n\n  test('should sync state when changes', async () => {\n    const LOCAL_STORAGE_KEY = 'test-sync-state';\n    const hook = setUp(LOCAL_STORAGE_KEY, 'foo', { listenStorageChange: true });\n    const anotherHook = setUp(LOCAL_STORAGE_KEY, 'bar', {\n      listenStorageChange: true,\n    });\n\n    expect(hook.result.current.state).toBe('foo');\n    expect(anotherHook.result.current.state).toBe('bar');\n\n    act(() => hook.result.current.setState('baz'));\n    expect(hook.result.current.state).toBe('baz');\n    expect(anotherHook.result.current.state).toBe('baz');\n\n    act(() => anotherHook.result.current.setState('qux'));\n    expect(hook.result.current.state).toBe('qux');\n    expect(anotherHook.result.current.state).toBe('qux');\n  });\n\n  test('should not rerender when setting the same reference', () => {\n    const LOCAL_STORAGE_KEY = 'test-same-reference';\n    const value = {\n      name: 'A',\n    };\n    let renderCount = 0;\n\n    const hook = renderHook(() => {\n      renderCount += 1;\n      const [state, setState] = useLocalStorageState(LOCAL_STORAGE_KEY, {\n        defaultValue: value,\n        listenStorageChange: true,\n      });\n\n      return {\n        state,\n        setState,\n      } as const;\n    });\n\n    expect(renderCount).toBe(1);\n    expect(hook.result.current.state).toBe(value);\n\n    act(() => {\n      hook.result.current.setState((prev) => prev!);\n    });\n\n    expect(renderCount).toBe(1);\n    expect(hook.result.current.state).toBe(value);\n    expect(localStorage.getItem(LOCAL_STORAGE_KEY)).toBeNull();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useLocalStorageState/demo/demo1.tsx",
    "content": "/**\n * title: Store state into localStorage\n * desc: Refresh this page and you will get the state from localStorage.\n *\n * title.zh-CN: 将 state 存储在 localStorage 中\n * desc.zh-CN: 刷新页面后，可以看到输入框中的内容被从 localStorage 中恢复了。\n */\n\nimport { useLocalStorageState } from 'ahooks';\n\nexport default function () {\n  const [message, setMessage] = useLocalStorageState<string | undefined>(\n    'use-local-storage-state-demo1',\n    {\n      defaultValue: 'Hello~',\n    },\n  );\n\n  return (\n    <>\n      <input\n        value={message || ''}\n        placeholder=\"Please enter some words...\"\n        onChange={(e) => setMessage(e.target.value)}\n      />\n      <button style={{ margin: '0 8px' }} type=\"button\" onClick={() => setMessage('Hello~')}>\n        Reset\n      </button>\n      <button type=\"button\" onClick={() => setMessage(undefined)}>\n        Clear\n      </button>\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/hooks/src/useLocalStorageState/demo/demo2.tsx",
    "content": "/**\n * title: Store complex types such as array or object\n * desc: useLocalStorageState will do the serialization and deserialization work automatically.\n *\n * title.zh-CN: 存储数组或对象等复杂类型\n * desc.zh-CN: useLocalStorageState 会自动处理序列化和反序列化的操作。\n */\n\nimport { useLocalStorageState } from 'ahooks';\n\nconst defaultArray = ['a', 'e', 'i', 'o', 'u'];\n\nexport default function () {\n  const [value, setValue] = useLocalStorageState('use-local-storage-state-demo2', {\n    defaultValue: defaultArray,\n  });\n\n  return (\n    <>\n      <p>{value?.join('-')}</p>\n      <button\n        type=\"button\"\n        style={{ marginRight: '16px' }}\n        onClick={() => setValue([...value, Math.random().toString(36).slice(-1)])}\n      >\n        push random\n      </button>\n      <button type=\"button\" onClick={() => setValue(defaultArray)}>\n        reset\n      </button>\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/hooks/src/useLocalStorageState/demo/demo3.tsx",
    "content": "/**\n * title: Custom serialization and deserialization functions\n * desc: You may not need the default `JSON.stringify/JSON.parse` to serialize string.\n *\n * title.zh-CN: 自定义序列化和反序列化函数\n * desc.zh-CN: 对于普通的字符串，可能你不需要默认的 `JSON.stringify/JSON.parse` 来序列化。\n */\n\nimport { useLocalStorageState } from 'ahooks';\n\nexport default function () {\n  const [message, setMessage] = useLocalStorageState<string | undefined>(\n    'use-local-storage-state-demo3',\n    {\n      defaultValue: 'Hello~',\n      serializer: (v) => v ?? '',\n      deserializer: (v) => v,\n    },\n  );\n\n  return (\n    <>\n      <input\n        value={message || ''}\n        placeholder=\"Please enter some words...\"\n        onChange={(e) => setMessage(e.target.value)}\n      />\n      <button style={{ margin: '0 8px' }} type=\"button\" onClick={() => setMessage('Hello~')}>\n        Reset\n      </button>\n      <button type=\"button\" onClick={() => setMessage(undefined)}>\n        Clear\n      </button>\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/hooks/src/useLocalStorageState/demo/demo4.tsx",
    "content": "/**\n * title: Sync state with localStorage\n * desc: When the stored value changes, all `useLocalStorageState` with the same `key` will synchronize their states, including different tabs of the same browser (try to open two tabs of this page, clicking a button on one page will automatically update the \"count\" on the other page).\n *\n * title.zh-CN: 将 state 与 localStorage 保持同步\n * desc.zh-CN: 存储值变化时，所有 `key` 相同的 `useLocalStorageState` 会同步状态，包括同一浏览器不同 tab 之间（尝试打开两个此页面，点击其中一个页面的按钮，另一个页面的 count 会自动更新）\n */\n\nimport { useLocalStorageState } from 'ahooks';\n\nexport default function () {\n  return (\n    <>\n      <Counter />\n      <Counter />\n    </>\n  );\n}\n\nfunction Counter() {\n  const [count, setCount] = useLocalStorageState('use-local-storage-state-demo4', {\n    defaultValue: 0,\n    listenStorageChange: true,\n  });\n\n  return (\n    <div style={{ marginBottom: '8px' }}>\n      <button style={{ marginRight: '8px' }} onClick={() => setCount(count! + 1)}>\n        count: {count}\n      </button>\n      <button onClick={() => setCount(0)}>Clear</button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/hooks/src/useLocalStorageState/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useLocalStorageState\n\nA Hook that store state into localStorage.\n\n## Examples\n\n### Store state into localStorage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Store complex types of data\n\n<code src=\"./demo/demo2.tsx\" />\n\n### Custom serialization and deserialization functions\n\n<code src=\"./demo/demo3.tsx\" />\n\n### Sync state with localStorage\n\n<code src=\"./demo/demo4.tsx\" />\n\n## API\n\nIf you want to delete this record from localStorage, you can use `setState()` or `setState(undefined)`.\n\n```typescript\ntype SetState<S> = S | ((prevState?: S) => S);\n\ninterface Options<T> {\n  defaultValue?: T | (() => T);\n  listenStorageChange?: boolean;\n  serializer?: (value: T) => string;\n  deserializer?: (value: string) => T;\n  onError?: (error: unknown) => void;\n}\n\nconst [state, setState] = useLocalStorageState<T>(\n  key: string,\n  options: Options<T>\n): [T?, (value?: SetState<T>) => void];\n```\n\n### Result\n\n| Property | Description                 | Type                            |\n| -------- | --------------------------- | ------------------------------- |\n| state    | Local `localStorage` value  | `T`                             |\n| setState | Update `localStorage` value | `(value?: SetState<T>) => void` |\n\n### Options\n\n| Property            | Description                                                                                                                                                                                             | Type                       | Default                       |\n| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ----------------------------- |\n| defaultValue        | Default value                                                                                                                                                                                           | `any \\| (() => any)`       | -                             |\n| listenStorageChange | Whether to listen storage changes. If `true`, when the stored value changes, all `useLocalStorageState` with the same `key` will synchronize their states, including different tabs of the same browser | `boolean`                  | `false`                       |\n| serializer          | Custom serialization method                                                                                                                                                                             | `(value: any) => string`   | `JSON.stringify`              |\n| deserializer        | Custom deserialization method                                                                                                                                                                           | `(value: string) => any`   | `JSON.parse`                  |\n| onError             | On error callback                                                                                                                                                                                       | `(error: unknown) => void` | `(e) => { console.error(e) }` |\n\n## Remark\n\nuseLocalStorageState will call `serializer` before write data to localStorage, and call `deserializer` once after read data.\n"
  },
  {
    "path": "packages/hooks/src/useLocalStorageState/index.ts",
    "content": "import { createUseStorageState } from '../createUseStorageState';\nimport isBrowser from '../utils/isBrowser';\n\nconst useLocalStorageState = createUseStorageState(() => (isBrowser ? localStorage : undefined));\n\nexport default useLocalStorageState;\n"
  },
  {
    "path": "packages/hooks/src/useLocalStorageState/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useLocalStorageState\n\n将状态存储在 localStorage 中的 Hook 。\n\n## 代码演示\n\n### 将 state 存储在 localStorage 中\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 存储复杂类型数据\n\n<code src=\"./demo/demo2.tsx\" />\n\n### 自定义序列化和反序列化函数\n\n<code src=\"./demo/demo3.tsx\" />\n\n### 将 state 与 localStorage 保持同步\n\n<code src=\"./demo/demo4.tsx\" />\n\n## API\n\n如果想从 localStorage 中删除这条数据，可以使用 `setState()` 或 `setState(undefined)` 。\n\n```typescript\ntype SetState<S> = S | ((prevState?: S) => S);\n\ninterface Options<T> {\n  defaultValue?: T | (() => T);\n  listenStorageChange?: boolean;\n  serializer?: (value: T) => string;\n  deserializer?: (value: string) => T;\n  onError?: (error: unknown) => void;\n}\n\nconst [state, setState] = useLocalStorageState<T>(\n  key: string,\n  options: Options<T>\n): [T?, (value?: SetState<T>) => void];\n```\n\n### Result\n\n| 参数     | 说明                   | 类型                            |\n| -------- | ---------------------- | ------------------------------- |\n| state    | 本地 `localStorage` 值 | `T`                             |\n| setState | 设置 `localStorage` 值 | `(value?: SetState<T>) => void` |\n\n### Options\n\n| 参数                | 说明                                                                                                                              | 类型                       | 默认值                        |\n| ------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ----------------------------- |\n| defaultValue        | 默认值                                                                                                                            | `any \\| (() => any)`       | -                             |\n| listenStorageChange | 是否监听存储变化。如果是 `true`，当存储值变化时，所有 `key` 相同的 `useLocalStorageState` 会同步状态，包括同一浏览器不同 tab 之间 | `boolean`                  | `false`                       |\n| serializer          | 自定义序列化方法                                                                                                                  | `(value: any) => string`   | `JSON.stringify`              |\n| deserializer        | 自定义反序列化方法                                                                                                                | `(value: string) => any`   | `JSON.parse`                  |\n| onError             | 错误回调函数                                                                                                                      | `(error: unknown) => void` | `(e) => { console.error(e) }` |\n\n## 备注\n\nuseLocalStorageState 在往 localStorage 写入数据前，会先调用一次 `serializer`，在读取数据之后，会先调用一次 `deserializer`。\n"
  },
  {
    "path": "packages/hooks/src/useLockFn/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { useCallback, useRef, useState } from 'react';\nimport { describe, expect, test } from 'vitest';\nimport { sleep } from '../../utils/testingHelpers';\nimport useLockFn from '../index';\n\ndescribe('useLockFn', () => {\n  const setUp = (): any =>\n    renderHook(() => {\n      const [tag, updateTag] = useState(false);\n      const countRef = useRef(0);\n      const persistFn = useCallback(\n        async (step: number) => {\n          countRef.current += step;\n          await sleep(50);\n        },\n        [tag],\n      );\n      const locked = useLockFn(persistFn);\n\n      return {\n        locked,\n        countRef,\n        updateTag: () => updateTag(true),\n      };\n    });\n\n  test('should work', async () => {\n    const hook = setUp();\n    const { locked, countRef } = hook.result.current;\n    locked(1);\n    expect(countRef.current).toBe(1);\n    locked(2);\n    expect(countRef.current).toBe(1);\n    await sleep(30);\n    locked(3);\n    expect(countRef.current).toBe(1);\n    await sleep(30);\n    locked(4);\n    expect(countRef.current).toBe(5);\n    locked(5);\n    expect(countRef.current).toBe(5);\n  });\n\n  test('should same', () => {\n    const hook = setUp();\n    const preLocked = hook.result.current.locked;\n    hook.rerender();\n    expect(hook.result.current.locked).toEqual(preLocked);\n    act(hook.result.current.updateTag);\n    expect(hook.result.current.locked).not.toEqual(preLocked);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useLockFn/demo/demo1.tsx",
    "content": "/**\n * title: Prevent duplicated submits\n * desc: Before the `submit` function finishes, the other click actions will be ignored.\n *\n * title.zh-CN: 防止重复提交\n * desc.zh-CN: 在 `submit` 函数执行完成前，其余的点击动作都会被忽略。\n */\n\nimport { useLockFn } from 'ahooks';\nimport { message } from 'antd';\nimport { useState } from 'react';\n\nfunction mockApiRequest() {\n  return new Promise<void>((resolve) => {\n    setTimeout(() => {\n      resolve();\n    }, 2000);\n  });\n}\n\nexport default () => {\n  const [count, setCount] = useState(0);\n\n  const submit = useLockFn(async () => {\n    message.info('Start to submit');\n    await mockApiRequest();\n    setCount((val) => val + 1);\n    message.success('Submit finished');\n  });\n\n  return (\n    <>\n      <p>Submit count: {count}</p>\n      <button onClick={submit}>Submit</button>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useLockFn/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useLockFn\n\nAdd lock to an async function to prevent parallel executions.\n\n## Examples\n\n### Basic usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nfunction useLockFn<P extends any[] = any[], V = any>(\n  fn: (...args: P) => Promise<V>\n): fn: (...args: P) => Promise<V | undefined>;\n```\n\n### Result\n\n| Property | Description                  | Type                               |\n| -------- | ---------------------------- | ---------------------------------- |\n| fn       | The async function with lock | `(...args: any[]) => Promise<any>` |\n\n### Params\n\n| Property | Description       | Type                               | Default |\n| -------- | ----------------- | ---------------------------------- | ------- |\n| fn       | An async function | `(...args: any[]) => Promise<any>` | -       |\n"
  },
  {
    "path": "packages/hooks/src/useLockFn/index.ts",
    "content": "import { useRef, useCallback } from 'react';\n\nfunction useLockFn<P extends any[] = any[], V = any>(fn: (...args: P) => Promise<V>) {\n  const lockRef = useRef(false);\n\n  return useCallback(\n    async (...args: P) => {\n      if (lockRef.current) {\n        return;\n      }\n      lockRef.current = true;\n      try {\n        const ret = await fn(...args);\n        return ret;\n      } catch (e) {\n        throw e;\n      } finally {\n        lockRef.current = false;\n      }\n    },\n    [fn],\n  );\n}\n\nexport default useLockFn;\n"
  },
  {
    "path": "packages/hooks/src/useLockFn/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useLockFn\n\n用于给一个异步函数增加竞态锁，防止并发执行。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nfunction useLockFn<P extends any[] = any[], V = any>(\n  fn: (...args: P) => Promise<V>\n): fn: (...args: P) => Promise<V | undefined>;\n```\n\n### Result\n\n| 参数 | 说明               | 类型                               |\n| ---- | ------------------ | ---------------------------------- |\n| fn   | 增加了竞态锁的函数 | `(...args: any[]) => Promise<any>` |\n\n### Params\n\n| 参数 | 说明                 | 类型                               | 默认值 |\n| ---- | -------------------- | ---------------------------------- | ------ |\n| fn   | 需要增加竞态锁的函数 | `(...args: any[]) => Promise<any>` | -      |\n"
  },
  {
    "path": "packages/hooks/src/useLongPress/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';\nimport type { Options } from '../index';\nimport useLongPress from '../index';\n\nconst mockCallback = vi.fn();\nconst mockClickCallback = vi.fn();\nconst mockLongPressEndCallback = vi.fn();\n\nlet events: Record<string, any> = {};\nconst mockTarget = {\n  addEventListener: vi.fn((event, callback) => {\n    events[event] = callback;\n  }),\n  removeEventListener: vi.fn((event) => {\n    Reflect.deleteProperty(events, event);\n  }),\n};\n\nconst setup = (onLongPress: any, target: any, options?: Options) =>\n  renderHook(() => useLongPress(onLongPress, target, options));\n\ndescribe('useLongPress', () => {\n  beforeEach(() => {\n    vi.useFakeTimers();\n    vi.clearAllMocks();\n  });\n\n  afterEach(() => {\n    events = {};\n    vi.useRealTimers();\n    vi.clearAllMocks();\n  });\n\n  test('longPress callback correct', () => {\n    setup(mockCallback, mockTarget, {\n      onClick: mockClickCallback,\n      onLongPressEnd: mockLongPressEndCallback,\n    });\n    expect(mockTarget.addEventListener).toBeCalled();\n    events['mousedown']();\n    vi.advanceTimersByTime(350);\n    events['mouseleave']();\n    expect(mockCallback).toBeCalledTimes(1);\n    expect(mockLongPressEndCallback).toBeCalledTimes(1);\n    expect(mockClickCallback).toBeCalledTimes(0);\n  });\n\n  test('click callback correct', () => {\n    setup(mockCallback, mockTarget, {\n      onClick: mockClickCallback,\n      onLongPressEnd: mockLongPressEndCallback,\n    });\n    expect(mockTarget.addEventListener).toBeCalled();\n    events['mousedown']();\n    events['mouseup']();\n    events['mousedown']();\n    events['mouseup']();\n    expect(mockCallback).toBeCalledTimes(0);\n    expect(mockLongPressEndCallback).toBeCalledTimes(0);\n    expect(mockClickCallback).toBeCalledTimes(2);\n  });\n\n  test('longPress and click callback correct', () => {\n    setup(mockCallback, mockTarget, {\n      onClick: mockClickCallback,\n      onLongPressEnd: mockLongPressEndCallback,\n    });\n    expect(mockTarget.addEventListener).toBeCalled();\n    events['mousedown']();\n    vi.advanceTimersByTime(350);\n    events['mouseup']();\n    events['mousedown']();\n    events['mouseup']();\n    expect(mockCallback).toBeCalledTimes(1);\n    expect(mockLongPressEndCallback).toBeCalledTimes(1);\n    expect(mockClickCallback).toBeCalledTimes(1);\n  });\n\n  test('onLongPress should not be called when over the threshold', () => {\n    const { unmount } = setup(mockCallback, mockTarget, {\n      moveThreshold: {\n        x: 30,\n        y: 20,\n      },\n    });\n    expect(events['mousemove']).toBeDefined();\n    events['mousedown'](new MouseEvent('mousedown'));\n    events['mousemove'](\n      new MouseEvent('mousemove', {\n        clientX: 40,\n        clientY: 10,\n      }),\n    );\n    vi.advanceTimersByTime(320);\n    expect(mockCallback).not.toBeCalled();\n\n    unmount();\n    expect(events['mousemove']).toBeUndefined();\n  });\n\n  test(`should not work when target don't support addEventListener method`, () => {\n    Object.defineProperty(mockTarget, 'addEventListener', {\n      get() {\n        return false;\n      },\n    });\n\n    setup(() => {}, mockTarget);\n    expect(Object.keys(events)).toHaveLength(0);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useLongPress/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Please keep pressing button to show effects.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 请长按按钮查看效果。\n */\n\nimport { useState, useRef } from 'react';\nimport { useLongPress } from 'ahooks';\n\nexport default () => {\n  const [counter, setCounter] = useState(0);\n  const ref = useRef<HTMLButtonElement>(null);\n\n  useLongPress(() => setCounter((s) => s + 1), ref);\n\n  return (\n    <div>\n      <button ref={ref} type=\"button\">\n        Press me\n      </button>\n      <p>counter: {counter}</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useLongPress/demo/demo2.tsx",
    "content": "import { useRef, useState } from 'react';\nimport { useLongPress } from 'ahooks';\n\nexport default () => {\n  const [pressCounter, setPressCounter] = useState(0);\n  const [clickCounter, setClickCounter] = useState(0);\n\n  const ref = useRef<HTMLButtonElement>(null);\n\n  useLongPress(() => setPressCounter((s) => s + 1), ref, {\n    onClick: () => setClickCounter((s) => s + 1),\n  });\n\n  return (\n    <div>\n      <button ref={ref} type=\"button\">\n        Press me\n      </button>\n      <p>pressCounter: {pressCounter}</p>\n      <p>clickCounter: {clickCounter}</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useLongPress/demo/demo3.tsx",
    "content": "/**\n * title: Basic usage\n * desc: After the movement threshold is exceeded, the long press event will not be triggered\n *\n * title.zh-CN: 超出移动阈值\n * desc.zh-CN: 超出移动阈值之后，长按事件将不会触发\n */\nimport { useRef, useState } from 'react';\nimport { useLongPress } from 'ahooks';\n\nexport default () => {\n  const [pressCounter, setPressCounter] = useState(0);\n\n  const ref = useRef<HTMLButtonElement>(null);\n\n  useLongPress(() => setPressCounter((s) => s + 1), ref, {\n    moveThreshold: { x: 30 },\n  });\n\n  return (\n    <div>\n      <button ref={ref} type=\"button\">\n        Press me\n      </button>\n      <p>counter: {pressCounter}</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useLongPress/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useLongPress\n\nListen for the long press event of the target element.\n\n## Examples\n\n### Basic usage\n\n<code src=\"./demo/demo1.tsx\"/>\n\n### Listen for click and long press events at the same time\n\n<code src=\"./demo/demo2.tsx\"/>\n\n### Move threshold\n\n<code src=\"./demo/demo3.tsx\"/>\n\n## API\n\n```typescript\nuseLongPress(\n  onLongPress: (event: MouseEvent | TouchEvent) => void,\n  target: Target,\n  options: {\n    delay?: number;\n    moveThreshold?: { x?: number; y?: number };\n    onClick?: (event: MouseEvent | TouchEvent) => void;\n    onLongPressEnd?: (event: MouseEvent | TouchEvent) => void;\n  }\n);\n```\n\n### Params\n\n| Property    | Description                  | Type                                                        | Default |\n| ----------- | ---------------------------- | ----------------------------------------------------------- | ------- |\n| onLongPress | Trigger function             | `(event: MouseEvent \\| TouchEvent) => void`                 | -       |\n| target      | DOM node or Ref              | `Element` \\| `() => Element` \\| `MutableRefObject<Element>` | -       |\n| options     | Optional configuration items | `Options`                                                   | `{}`    |\n\n### Options\n\n| Property       | Description                                                                         | Type                                        | Default |\n| -------------- | ----------------------------------------------------------------------------------- | ------------------------------------------- | ------- |\n| delay          | Long press time                                                                     | `number`                                    | `300`   |\n| moveThreshold  | Move threshold after press. If exceeded, the long press function won't be triggered | `{ x?: number; y?: number }`                | -       |\n| onClick        | Click event                                                                         | `(event: MouseEvent \\| TouchEvent) => void` | -       |\n| onLongPressEnd | Long press end event                                                                | `(event: MouseEvent \\| TouchEvent) => void` | -       |\n\n### Remark\n\nPlease refer to: https://stackoverflow.com/a/11237968 to disable the ability to long press to select text on the phone\n"
  },
  {
    "path": "packages/hooks/src/useLongPress/index.ts",
    "content": "import { useRef } from 'react';\nimport useLatest from '../useLatest';\nimport type { BasicTarget } from '../utils/domTarget';\nimport { getTargetElement } from '../utils/domTarget';\nimport useEffectWithTarget from '../utils/useEffectWithTarget';\n\ntype EventType = MouseEvent | TouchEvent;\nexport interface Options {\n  delay?: number;\n  moveThreshold?: { x?: number; y?: number };\n  onClick?: (event: EventType) => void;\n  onLongPressEnd?: (event: EventType) => void;\n}\n\nfunction useLongPress(\n  onLongPress: (event: EventType) => void,\n  target: BasicTarget,\n  { delay = 300, moveThreshold, onClick, onLongPressEnd }: Options = {},\n) {\n  const onLongPressRef = useLatest(onLongPress);\n  const onClickRef = useLatest(onClick);\n  const onLongPressEndRef = useLatest(onLongPressEnd);\n\n  const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);\n\n  const isTriggeredRef = useRef(false);\n  const pervPositionRef = useRef({ x: 0, y: 0 });\n  const mousePressed = useRef(false);\n  const touchPressed = useRef(false);\n  const hasMoveThreshold = !!(\n    (moveThreshold?.x && moveThreshold.x > 0) ||\n    (moveThreshold?.y && moveThreshold.y > 0)\n  );\n\n  useEffectWithTarget(\n    () => {\n      const targetElement = getTargetElement(target);\n      if (!targetElement?.addEventListener) {\n        return;\n      }\n\n      const overThreshold = (event: EventType) => {\n        const { clientX, clientY } = getClientPosition(event);\n        const offsetX = Math.abs(clientX - pervPositionRef.current.x);\n        const offsetY = Math.abs(clientY - pervPositionRef.current.y);\n\n        return !!(\n          (moveThreshold?.x && offsetX > moveThreshold.x) ||\n          (moveThreshold?.y && offsetY > moveThreshold.y)\n        );\n      };\n\n      function getClientPosition(event: EventType) {\n        if ('TouchEvent' in window && event instanceof TouchEvent) {\n          return {\n            clientX: event.touches[0].clientX,\n            clientY: event.touches[0].clientY,\n          };\n        }\n        if (event instanceof MouseEvent) {\n          return {\n            clientX: event.clientX,\n            clientY: event.clientY,\n          };\n        }\n\n        return { clientX: 0, clientY: 0 };\n      }\n\n      const createTimer = (event: EventType) => {\n        timerRef.current = setTimeout(() => {\n          onLongPressRef.current(event);\n          isTriggeredRef.current = true;\n        }, delay);\n      };\n\n      const onTouchStart = (event: TouchEvent) => {\n        if (touchPressed.current) {\n          return;\n        }\n        touchPressed.current = true;\n\n        if (hasMoveThreshold) {\n          const { clientX, clientY } = getClientPosition(event);\n          pervPositionRef.current.x = clientX;\n          pervPositionRef.current.y = clientY;\n        }\n        createTimer(event);\n      };\n\n      const onMouseDown = (event: MouseEvent) => {\n        if ((event as any)?.sourceCapabilities?.firesTouchEvents) {\n          return;\n        }\n\n        mousePressed.current = true;\n\n        if (hasMoveThreshold) {\n          pervPositionRef.current.x = event.clientX;\n          pervPositionRef.current.y = event.clientY;\n        }\n        createTimer(event);\n      };\n\n      const onMove = (event: EventType) => {\n        if (timerRef.current && overThreshold(event)) {\n          clearTimeout(timerRef.current);\n          timerRef.current = undefined;\n        }\n      };\n\n      const onTouchEnd = (event: TouchEvent) => {\n        if (!touchPressed.current) {\n          return;\n        }\n        touchPressed.current = false;\n\n        if (timerRef.current) {\n          clearTimeout(timerRef.current);\n          timerRef.current = undefined;\n        }\n\n        if (isTriggeredRef.current) {\n          onLongPressEndRef.current?.(event);\n        } else if (onClickRef.current) {\n          onClickRef.current(event);\n        }\n        isTriggeredRef.current = false;\n      };\n\n      const onMouseUp = (event: MouseEvent) => {\n        if ((event as any)?.sourceCapabilities?.firesTouchEvents) {\n          return;\n        }\n        if (!mousePressed.current) {\n          return;\n        }\n        mousePressed.current = false;\n\n        if (timerRef.current) {\n          clearTimeout(timerRef.current);\n          timerRef.current = undefined;\n        }\n\n        if (isTriggeredRef.current) {\n          onLongPressEndRef.current?.(event);\n        } else if (onClickRef.current) {\n          onClickRef.current(event);\n        }\n        isTriggeredRef.current = false;\n      };\n\n      const onMouseLeave = (event: MouseEvent) => {\n        if (!mousePressed.current) {\n          return;\n        }\n        mousePressed.current = false;\n\n        if (timerRef.current) {\n          clearTimeout(timerRef.current);\n          timerRef.current = undefined;\n        }\n        if (isTriggeredRef.current) {\n          onLongPressEndRef.current?.(event);\n          isTriggeredRef.current = false;\n        }\n      };\n\n      targetElement.addEventListener('mousedown', onMouseDown as EventListener);\n      targetElement.addEventListener('mouseup', onMouseUp as EventListener);\n      targetElement.addEventListener('mouseleave', onMouseLeave as EventListener);\n      targetElement.addEventListener('touchstart', onTouchStart as EventListener);\n      targetElement.addEventListener('touchend', onTouchEnd as EventListener);\n\n      if (hasMoveThreshold) {\n        targetElement.addEventListener('mousemove', onMove as EventListener);\n        targetElement.addEventListener('touchmove', onMove as EventListener);\n      }\n\n      return () => {\n        if (timerRef.current) {\n          clearTimeout(timerRef.current);\n          isTriggeredRef.current = false;\n        }\n\n        targetElement.removeEventListener('mousedown', onMouseDown as EventListener);\n        targetElement.removeEventListener('mouseup', onMouseUp as EventListener);\n        targetElement.removeEventListener('mouseleave', onMouseLeave as EventListener);\n        targetElement.removeEventListener('touchstart', onTouchStart as EventListener);\n        targetElement.removeEventListener('touchend', onTouchEnd as EventListener);\n\n        if (hasMoveThreshold) {\n          targetElement.removeEventListener('mousemove', onMove as EventListener);\n          targetElement.removeEventListener('touchmove', onMove as EventListener);\n        }\n      };\n    },\n    [],\n    target,\n  );\n}\n\nexport default useLongPress;\n"
  },
  {
    "path": "packages/hooks/src/useLongPress/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useLongPress\n\n监听目标元素的长按事件。\n\n## 代码演示\n\n### 基本使用\n\n<code src=\"./demo/demo1.tsx\"/>\n\n### 同时监听点击和长按事件\n\n<code src=\"./demo/demo2.tsx\"/>\n\n### 移动阈值\n\n<code src=\"./demo/demo3.tsx\"/>\n\n## API\n\n```typescript\nuseLongPress(\n  onLongPress: (event: MouseEvent | TouchEvent) => void,\n  target: Target,\n  options: {\n    delay?: number;\n    moveThreshold?: { x?: number; y?: number };\n    onClick?: (event: MouseEvent | TouchEvent) => void;\n    onLongPressEnd?: (event: MouseEvent | TouchEvent) => void;\n  }\n);\n```\n\n### Params\n\n| 参数        | 说明             | 类型                                                        | 默认值 |\n| ----------- | ---------------- | ----------------------------------------------------------- | ------ |\n| onLongPress | 触发函数         | `(event: MouseEvent \\| TouchEvent) => void`                 | -      |\n| target      | DOM 节点或者 Ref | `Element` \\| `() => Element` \\| `MutableRefObject<Element>` | -      |\n| options     | 可选配置项       | `Options`                                                   | `{}`   |\n\n### Options\n\n| 参数           | 说明                                 | 类型                                        | 默认值 |\n| -------------- | ------------------------------------ | ------------------------------------------- | ------ |\n| delay          | 长按时间                             | `number`                                    | `300`  |\n| moveThreshold  | 按下后移动阈值，超出则不触发长按事件 | `{ x?: number; y?: number }`                | -      |\n| onClick        | 点击事件                             | `(event: MouseEvent \\| TouchEvent) => void` | -      |\n| onLongPressEnd | 长按结束事件                         | `(event: MouseEvent \\| TouchEvent) => void` | -      |\n\n### 备注\n\n禁用在手机上长按选择文本的能力请参考：https://stackoverflow.com/a/11237968\n"
  },
  {
    "path": "packages/hooks/src/useMap/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useMap from '../index';\n\nconst setup = (initialMap?: Iterable<[any, any]>) => renderHook(() => useMap(initialMap));\n\ndescribe('useMap', () => {\n  test('should init map and utils', () => {\n    const { result } = setup([\n      ['foo', 'bar'],\n      ['a', 1],\n    ]);\n    const [map, utils] = result.current;\n\n    expect(Array.from(map)).toEqual([\n      ['foo', 'bar'],\n      ['a', 1],\n    ]);\n    expect(utils).toStrictEqual({\n      get: expect.any(Function),\n      set: expect.any(Function),\n      setAll: expect.any(Function),\n      remove: expect.any(Function),\n      reset: expect.any(Function),\n    });\n  });\n\n  test('should init empty map if not initial object provided', () => {\n    const { result } = setup();\n    expect([...result.current[0]]).toEqual([]);\n\n    const { result: result2 } = setup(undefined);\n    expect([...result2.current[0]]).toEqual([]);\n  });\n\n  test('should get corresponding value for initial provided key', () => {\n    const { result } = setup([\n      ['foo', 'bar'],\n      ['a', 1],\n    ]);\n    const [, utils] = result.current;\n\n    let value;\n    act(() => {\n      value = utils.get('a');\n    });\n\n    expect(value).toBe(1);\n  });\n\n  test('should get corresponding value for existing provided key', () => {\n    const { result } = setup([\n      ['foo', 'bar'],\n      ['a', 1],\n    ]);\n\n    act(() => {\n      result.current[1].set('a', 99);\n    });\n\n    let value;\n    act(() => {\n      value = result.current[1].get('a');\n    });\n\n    expect(value).toBe(99);\n  });\n\n  test('should get undefined for non-existing provided key', () => {\n    const { result } = setup([\n      ['foo', 'bar'],\n      ['a', 1],\n    ]);\n    const [, utils] = result.current;\n\n    let value;\n    act(() => {\n      value = utils.get('nonExisting');\n    });\n\n    expect(value).toBeUndefined();\n  });\n\n  test('should set new key-value pair', () => {\n    const { result } = setup([\n      ['foo', 'bar'],\n      ['a', 1],\n    ]);\n    const [, utils] = result.current;\n\n    act(() => {\n      utils.set('newKey', 99);\n    });\n\n    expect([...result.current[0]]).toEqual([\n      ['foo', 'bar'],\n      ['a', 1],\n      ['newKey', 99],\n    ]);\n  });\n\n  test('should override current value if setting existing key', () => {\n    const { result } = setup([\n      ['foo', 'bar'],\n      ['a', 1],\n    ]);\n    const [, utils] = result.current;\n\n    act(() => {\n      utils.set('foo', 'qux');\n    });\n\n    expect([...result.current[0]]).toEqual([\n      ['foo', 'qux'],\n      ['a', 1],\n    ]);\n  });\n\n  test('should set new map', () => {\n    const { result } = setup([\n      ['foo', 'bar'],\n      ['a', 1],\n    ]);\n    const [, utils] = result.current;\n\n    act(() => {\n      utils.setAll([\n        ['foo', 'foo'],\n        ['a', 2],\n      ]);\n    });\n\n    expect([...result.current[0]]).toEqual([\n      ['foo', 'foo'],\n      ['a', 2],\n    ]);\n\n    act(() => {\n      // @ts-ignore\n      utils.setAll();\n    });\n    expect([...result.current[0]]).toEqual([]);\n  });\n\n  test('remove should be work', () => {\n    const { result } = setup([['msg', 'hello']]);\n    const { remove } = result.current[1];\n    expect(result.current[0].size).toBe(1);\n    act(() => {\n      remove('msg');\n    });\n    expect(result.current[0].size).toBe(0);\n\n    const { result: result2 } = setup([\n      ['foo', 'bar'],\n      ['a', 1],\n      ['b', 2],\n      ['c', 3],\n    ]);\n    const [, utils] = result2.current;\n\n    act(() => {\n      utils.remove('a');\n    });\n\n    expect([...result2.current[0]]).toEqual([\n      ['foo', 'bar'],\n      ['b', 2],\n      ['c', 3],\n    ]);\n  });\n\n  test('reset should be work', () => {\n    const { result } = setup([['msg', 'hello']]);\n    const { set, reset } = result.current[1];\n    act(() => {\n      set('text', 'new map');\n    });\n    expect([...result.current[0]]).toEqual([\n      ['msg', 'hello'],\n      ['text', 'new map'],\n    ]);\n    act(() => {\n      reset();\n    });\n    expect([...result.current[0]]).toEqual([['msg', 'hello']]);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useMap/demo/demo1.tsx",
    "content": "import { useMap } from 'ahooks';\n\nexport default () => {\n  const [map, { set, setAll, remove, reset, get }] = useMap<string | number, string>([\n    ['msg', 'hello world'],\n    [123, 'number type'],\n  ]);\n\n  return (\n    <div>\n      <button type=\"button\" onClick={() => set(String(Date.now()), new Date().toJSON())}>\n        Add\n      </button>\n      <button\n        type=\"button\"\n        onClick={() => setAll([['text', 'this is a new Map']])}\n        style={{ margin: '0 8px' }}\n      >\n        Set new Map\n      </button>\n      <button type=\"button\" onClick={() => remove('msg')} disabled={!get('msg')}>\n        Remove 'msg'\n      </button>\n      <button type=\"button\" onClick={() => reset()} style={{ margin: '0 8px' }}>\n        Reset\n      </button>\n      <div style={{ marginTop: 16 }}>\n        <pre>{JSON.stringify(Array.from(map), null, 2)}</pre>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useMap/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useMap\n\nA hook that can manage the state of Map.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst [\n  map,\n  {\n    set,\n    setAll,\n    remove,\n    reset,\n    get\n  }\n] = useMap<K, V>(initialValue);\n```\n\n### Result\n\n| Property | Description      | Type                                 |\n| -------- | ---------------- | ------------------------------------ |\n| map      | Map object       | `Map<K, V>`                          |\n| set      | Add item         | `(key: K, value: V) => void`         |\n| get      | Get item         | `(key: K) => V \\| undefined`         |\n| setAll   | Set a new Map    | `(newMap: Iterable<[K, V]>) => void` |\n| remove   | Remove key       | `(key: K) => void`                   |\n| reset    | Reset to default | `() => void`                         |\n\n### Params\n\n| Property     | Description                 | Type               | Default |\n| ------------ | --------------------------- | ------------------ | ------- |\n| initialValue | Optional, set default value | `Iterable<[K, V]>` | -       |\n"
  },
  {
    "path": "packages/hooks/src/useMap/index.ts",
    "content": "import { useState } from 'react';\nimport useMemoizedFn from '../useMemoizedFn';\n\nfunction useMap<K, T>(initialValue?: Iterable<readonly [K, T]>) {\n  const getInitValue = () => new Map(initialValue);\n  const [map, setMap] = useState<Map<K, T>>(getInitValue);\n\n  const set = (key: K, entry: T) => {\n    setMap((prev) => {\n      const temp = new Map(prev);\n      temp.set(key, entry);\n      return temp;\n    });\n  };\n\n  const setAll = (newMap: Iterable<readonly [K, T]>) => {\n    setMap(new Map(newMap));\n  };\n\n  const remove = (key: K) => {\n    setMap((prev) => {\n      const temp = new Map(prev);\n      temp.delete(key);\n      return temp;\n    });\n  };\n\n  const reset = () => setMap(getInitValue());\n\n  const get = (key: K) => map.get(key);\n\n  return [\n    map,\n    {\n      set: useMemoizedFn(set),\n      setAll: useMemoizedFn(setAll),\n      remove: useMemoizedFn(remove),\n      reset: useMemoizedFn(reset),\n      get: useMemoizedFn(get),\n    },\n  ] as const;\n}\n\nexport default useMap;\n"
  },
  {
    "path": "packages/hooks/src/useMap/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useMap\n\n管理 Map 类型状态的 Hook。\n\n## 代码演示\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst [\n  map,\n  {\n    set,\n    setAll,\n    remove,\n    reset,\n    get\n  }\n] = useMap<K, V>(initialValue);\n```\n\n### Result\n\n| 参数   | 说明                  | 类型                                 |\n| ------ | --------------------- | ------------------------------------ |\n| map    | Map 对象              | `Map<K, V>`                          |\n| set    | 添加元素              | `(key: K, value: V) => void`         |\n| get    | 获取元素              | `(key: K) => V \\| undefined`         |\n| setAll | 生成一个新的 Map 对象 | `(newMap: Iterable<[K, V]>) => void` |\n| remove | 移除元素              | `(key: K) => void`                   |\n| reset  | 重置为默认值          | `() => void`                         |\n\n### Params\n\n| 参数         | 说明                        | 类型               | 默认值 |\n| ------------ | --------------------------- | ------------------ | ------ |\n| initialValue | 可选项，传入默认的 Map 参数 | `Iterable<[K, V]>` | -      |\n"
  },
  {
    "path": "packages/hooks/src/useMemoizedFn/__tests__/index.spec.ts",
    "content": "import { act, type RenderHookResult, renderHook } from '@testing-library/react';\nimport { useState } from 'react';\nimport { describe, expect, test } from 'vitest';\nimport useMemoizedFn from '../';\n\nconst useCount = () => {\n  const [count, setCount] = useState(0);\n\n  const addCount = () => {\n    setCount((c) => c + 1);\n  };\n\n  const memoizedFn = useMemoizedFn(() => count);\n\n  return { addCount, memoizedFn };\n};\n\nlet hook: RenderHookResult<any, any>;\n\ndescribe('useMemoizedFn', () => {\n  test('useMemoizedFn should work', () => {\n    act(() => {\n      hook = renderHook(() => useCount());\n    });\n    const currentFn = hook.result.current.memoizedFn;\n    expect(hook.result.current.memoizedFn()).toBe(0);\n\n    act(() => {\n      hook.result.current.addCount();\n    });\n\n    expect(currentFn).toEqual(hook.result.current.memoizedFn);\n    expect(hook.result.current.memoizedFn()).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useMemoizedFn/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: useMemoizedFn is the same as useCallback.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: useMemoizedFn 与 useCallback 可以实现同样的效果。\n */\n\nimport { useState, useCallback } from 'react';\nimport { message } from 'antd';\nimport { useMemoizedFn } from 'ahooks';\n\nexport default () => {\n  const [count, setCount] = useState(0);\n\n  const callbackFn = useCallback(() => {\n    message.info(`Current count is ${count}`);\n  }, [count]);\n\n  const memoizedFn = useMemoizedFn(() => {\n    message.info(`Current count is ${count}`);\n  });\n\n  return (\n    <>\n      <p>count: {count}</p>\n      <button\n        type=\"button\"\n        onClick={() => {\n          setCount((c) => c + 1);\n        }}\n      >\n        Add Count\n      </button>\n      <div style={{ marginTop: 16 }}>\n        <button type=\"button\" onClick={callbackFn}>\n          call callbackFn\n        </button>\n        <button type=\"button\" onClick={memoizedFn} style={{ marginLeft: 8 }}>\n          call memoizedFn\n        </button>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useMemoizedFn/demo/demo2.tsx",
    "content": "/**\n * title: useMemoizedFn function reference will not change, which can be used for performance optimization.\n * desc: In the example, `memoizedFn` reference will not change, `callbackFn` will change when count changes.\n *\n * title.zh-CN: useMemoizedFn 函数地址不会变化，可以用于性能优化\n * desc.zh-CN: 示例中 `memoizedFn` 是不会变化的，`callbackFn` 在 count 变化时变化。\n */\n\nimport { useMemoizedFn } from 'ahooks';\nimport { message } from 'antd';\nimport { useCallback, useRef, useState, memo } from 'react';\n\nexport default () => {\n  const [count, setCount] = useState(0);\n\n  const callbackFn = useCallback(() => {\n    message.info(`Current count is ${count}`);\n  }, [count]);\n\n  const memoizedFn = useMemoizedFn(() => {\n    message.info(`Current count is ${count}`);\n  });\n\n  return (\n    <>\n      <p>count: {count}</p>\n      <button\n        type=\"button\"\n        onClick={() => {\n          setCount((c) => c + 1);\n        }}\n      >\n        Add Count\n      </button>\n\n      <p>You can click the button to see the number of sub-component renderings</p>\n\n      <div style={{ marginTop: 32 }}>\n        <h3>Component with useCallback function:</h3>\n        {/* use callback function, ExpensiveTree component will re-render on state change */}\n        <ExpensiveTree showCount={callbackFn} />\n      </div>\n\n      <div style={{ marginTop: 32 }}>\n        <h3>Component with useMemoizedFn function:</h3>\n        {/* use memoized function, ExpensiveTree component will only render once */}\n        <ExpensiveTree showCount={memoizedFn} />\n      </div>\n    </>\n  );\n};\n\n// some expensive component with React.memo\nconst ExpensiveTree = memo<{ [key: string]: any }>(({ showCount }) => {\n  const renderCountRef = useRef(0);\n  renderCountRef.current += 1;\n\n  return (\n    <div>\n      <p>Render Count: {renderCountRef.current}</p>\n      <button type=\"button\" onClick={showCount}>\n        showParentCount\n      </button>\n    </div>\n  );\n});\n"
  },
  {
    "path": "packages/hooks/src/useMemoizedFn/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useMemoizedFn\n\nHooks for persistent functions. In general, useMemoizedFn can be used instead of useCallback. See [FAQ](#faq) for special cases.\n\nIn some scenarios, we need to use useCallback to cache a function, but when the second parameter deps changes, the function will be regenerated, causing the function reference to change.\n\n```js\nconst [state, setState] = useState('');\n\n// When the state changes, the func reference will change\nconst func = useCallback(() => {\n  console.log(state);\n}, [state]);\n```\n\nUsing useMemoizedFn, you can omit the second parameter deps, and ensure that the function reference never change.\n\n```js\nconst [state, setState] = useState('');\n\n// func reference never change\nconst func = useMemoizedFn(() => {\n  console.log(state);\n});\n```\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Performance Improvement\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst memoizedFn = useMemoizedFn<T>(fn: T): T;\n```\n\n### Result\n\n| Property   | Description                                       | Type                      |\n| ---------- | ------------------------------------------------- | ------------------------- |\n| memoizedFn | Function that the reference address never changes | `(...args: any[]) => any` |\n\n### Params\n\n| Property | Description                       | Type                      | Default |\n| -------- | --------------------------------- | ------------------------- | ------- |\n| fn       | Function that require persistence | `(...args: any[]) => any` | -       |\n\n## FAQ\n\n### The function returned by `useMemoizedFn` will not inherit properties from fn itself?\n\nThe function returned by `useMemoizedFn` is entirely different from the reference of the passed `fn`, and it does not inherit any properties from `fn` itself. If you want to preserve the properties of the function itself after memoization, `useMemoizedFn` currently does not fulfill that requirement. In this case, consider downgrading to using `useCallback` or `useMemo` instead.\n\nRelated issues: [2273](https://github.com/alibaba/hooks/issues/2273)\n"
  },
  {
    "path": "packages/hooks/src/useMemoizedFn/index.ts",
    "content": "import { useMemo, useRef } from 'react';\nimport { isFunction } from '../utils';\nimport isDev from '../utils/isDev';\n\ntype noop = (this: any, ...args: any[]) => any;\n\ntype PickFunction<T extends noop> = (\n  this: ThisParameterType<T>,\n  ...args: Parameters<T>\n) => ReturnType<T>;\n\nconst useMemoizedFn = <T extends noop>(fn: T) => {\n  if (isDev) {\n    if (!isFunction(fn)) {\n      console.error(`useMemoizedFn expected parameter is a function, got ${typeof fn}`);\n    }\n  }\n\n  const fnRef = useRef<T>(fn);\n\n  // why not write `fnRef.current = fn`?\n  // https://github.com/alibaba/hooks/issues/728\n  fnRef.current = useMemo<T>(() => fn, [fn]);\n\n  const memoizedFn = useRef<PickFunction<T>>(undefined);\n\n  if (!memoizedFn.current) {\n    memoizedFn.current = function (this, ...args) {\n      return fnRef.current.apply(this, args);\n    };\n  }\n\n  return memoizedFn.current;\n};\n\nexport default useMemoizedFn;\n"
  },
  {
    "path": "packages/hooks/src/useMemoizedFn/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useMemoizedFn\n\n持久化 function 的 Hook，一般情况下，可以使用 useMemoizedFn 完全代替 useCallback，特殊情况见 [FAQ](#faq)。\n\n在某些场景中，我们需要使用 useCallback 来记住一个函数，但是在第二个参数 deps 变化时，会重新生成函数，导致函数地址变化。\n\n```js\nconst [state, setState] = useState('');\n\n// 在 state 变化时，func 地址会变化\nconst func = useCallback(() => {\n  console.log(state);\n}, [state]);\n```\n\n使用 useMemoizedFn，可以省略第二个参数 deps，同时保证函数地址永远不会变化。\n\n```js\nconst [state, setState] = useState('');\n\n// func 地址永远不会变化\nconst func = useMemoizedFn(() => {\n  console.log(state);\n});\n```\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 性能提升\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst memoizedFn = useMemoizedFn<T>(fn: T): T;\n```\n\n### Result\n\n| 参数       | 说明                       | 类型                      |\n| ---------- | -------------------------- | ------------------------- |\n| memoizedFn | 引用地址永远不会变化的函数 | `(...args: any[]) => any` |\n\n### Params\n\n| 参数 | 说明             | 类型                      | 默认值 |\n| ---- | ---------------- | ------------------------- | ------ |\n| fn   | 需要持久化的函数 | `(...args: any[]) => any` | -      |\n\n## FAQ\n\n### `useMemoizedFn` 返回的函数没有继承 fn 自身的属性？\n\n`useMemoizedFn` 返回的函数与传入的 fn 的引用完全不同，且没有继承 fn 自身的属性。如果想要持久化后函数自身的属性不丢失，目前 `useMemoizedFn` 满足不了，请降级使用 `useCallback`、`useMemo`。\n\nRelated issues: [2273](https://github.com/alibaba/hooks/issues/2273)\n"
  },
  {
    "path": "packages/hooks/src/useMount/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\nimport useMount from '../index';\n\ndescribe('useMount', () => {\n  test('test mount', async () => {\n    const destructor = vi.fn();\n    const fn = vi.fn();\n    fn.mockReturnValue(destructor);\n    const hook = renderHook(() => useMount(fn));\n    expect(fn).toHaveBeenCalledTimes(1);\n    expect(destructor).toHaveBeenCalledTimes(0);\n    hook.rerender();\n    expect(fn).toHaveBeenCalledTimes(1);\n    expect(destructor).toHaveBeenCalledTimes(0);\n    hook.unmount();\n    expect(fn).toHaveBeenCalledTimes(1);\n    expect(destructor).toHaveBeenCalledTimes(1);\n\n    renderHook(() => useMount(fn)).unmount();\n    expect(fn).toHaveBeenCalledTimes(2);\n    expect(destructor).toHaveBeenCalledTimes(2);\n  });\n\n  test('test mount with async function', async () => {\n    const mockAsyncFn = vi.fn().mockResolvedValue(undefined);\n    const hook = renderHook(() => useMount(mockAsyncFn));\n    expect(mockAsyncFn).toHaveBeenCalledTimes(1);\n    hook.rerender();\n    expect(mockAsyncFn).toHaveBeenCalledTimes(1);\n    hook.unmount();\n    expect(mockAsyncFn).toHaveBeenCalledTimes(1);\n  });\n\n  test('test mount with async function that returns cleanup', async () => {\n    const cleanup = vi.fn();\n    const mockAsyncFn = vi.fn().mockResolvedValue(cleanup);\n    const hook = renderHook(() => useMount(mockAsyncFn));\n    expect(mockAsyncFn).toHaveBeenCalledTimes(1);\n    hook.unmount();\n    // Cleanup should not be called for async functions\n    expect(cleanup).toHaveBeenCalledTimes(0);\n  });\n\n  // test('should output error when fn is not a function', () => {\n  //   const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});\n  //   renderHook(() => useMount(1 as any));\n  //   expect(errSpy).toBeCalledWith(\n  //     'useMount: parameter `fn` expected to be a function, but got \"number\".',\n  //   );\n  //   errSpy.mockRestore();\n  // });\n});\n"
  },
  {
    "path": "packages/hooks/src/useMount/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: The function is called right after the component is mounted.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 在组件首次渲染时，执行方法。\n */\n\nimport { useMount, useBoolean } from 'ahooks';\nimport { message } from 'antd';\nconst MyComponent = () => {\n  useMount(() => {\n    message.info('mount');\n\n    return () => {\n      message.info('unmount');\n    };\n  });\n\n  return <div>Hello World</div>;\n};\n\nexport default () => {\n  const [state, { toggle }] = useBoolean(false);\n\n  return (\n    <>\n      <button type=\"button\" onClick={toggle}>\n        {state ? 'unmount' : 'mount'}\n      </button>\n      {state && <MyComponent />}\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useMount/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useMount\n\nA hook that executes a function after the component is mounted.\n\n## Examples\n\n### Default Usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseMount(fn: EffectCallback);\n```\n\n### Params\n\n| Property | Description                 | Type         | Default |\n| -------- | --------------------------- | ------------ | ------- |\n| fn       | The function to be executed | `EffectCallback` | -       |\n"
  },
  {
    "path": "packages/hooks/src/useMount/index.ts",
    "content": "import { useEffect } from 'react';\nimport { type EffectCallback } from 'react';\nimport { isFunction } from '../utils';\nimport isDev from '../utils/isDev';\n\ntype MountCallback = EffectCallback | (() => Promise<void | (() => void)>);\n\nconst useMount = (fn: MountCallback) => {\n  if (isDev) {\n    if (!isFunction(fn)) {\n      console.error(\n        `useMount: parameter \\`fn\\` expected to be a function, but got \"${typeof fn}\".`,\n      );\n    }\n  }\n\n  useEffect(() => {\n    const result = fn?.();\n    // If fn returns a Promise, don't return it as cleanup function\n    if (result && typeof result === 'object' && typeof (result as any).then === 'function') {\n      return;\n    }\n    return result as ReturnType<EffectCallback>;\n  }, []);\n};\n\nexport default useMount;\n"
  },
  {
    "path": "packages/hooks/src/useMount/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useMount\n\n只在组件初始化时执行的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseMount(fn: EffectCallback);\n```\n\n### 参数\n\n| 参数 | 说明               | 类型         | 默认值 |\n| ---- | ------------------ | ------------ | ------ |\n| fn   | 初始化时执行的函数 | `EffectCallback` | -      |\n"
  },
  {
    "path": "packages/hooks/src/useMouse/__tests__/index.spec.ts",
    "content": "import { renderHook, waitFor } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\nimport useMouse from '../index';\n\ndescribe('useMouse', () => {\n  function moveMouse(x: number, y: number) {\n    document.dispatchEvent(\n      new MouseEvent('mousemove', {\n        clientX: x,\n        clientY: y,\n        screenX: x,\n        screenY: y,\n      }),\n    );\n  }\n\n  test('on mouseMove', async () => {\n    const hook = renderHook(() => useMouse());\n    expect(hook.result.current.pageX).toBeNaN();\n    expect(hook.result.current.pageY).toBeNaN();\n\n    moveMouse(10, 10);\n\n    // In jsdom environment, pageX and pageY might be set to clientX and clientY\n    await waitFor(() => expect(hook.result.current.clientX).toBe(10));\n    expect(hook.result.current.clientY).toBe(10);\n    expect(hook.result.current.screenX).toBe(10);\n    expect(hook.result.current.screenY).toBe(10);\n  });\n\n  test('should be work with target', async () => {\n    const events: Record<string, any> = {};\n    const getBoundingClientRectMock = vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect');\n    vi.spyOn(document, 'addEventListener').mockImplementation(\n      vi.fn((event: any, callback: any) => {\n        events[event] = callback;\n      }),\n    );\n\n    const targetEl = document.createElement('div');\n    getBoundingClientRectMock.mockReturnValue({\n      left: 100,\n      top: 100,\n      width: 200,\n      height: 200,\n    } as DOMRect);\n    const { result } = renderHook(() => useMouse(targetEl));\n    events['mousemove']({ pageX: 100, pageY: 100 });\n\n    await waitFor(() => expect(result.current.elementX).toBe(0));\n    expect(result.current.elementX).toBe(0);\n    expect(result.current.elementY).toBe(0);\n    expect(result.current.elementPosX).toBe(100);\n    expect(result.current.elementPosY).toBe(100);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useMouse/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Tracking cursor position.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 获取鼠标位置。\n */\n\nimport { useMouse } from 'ahooks';\nexport default () => {\n  const mouse = useMouse();\n\n  return (\n    <div>\n      <p>\n        Client - x: {mouse.clientX}, y: {mouse.clientY}\n      </p>\n      <p>\n        Page - x: {mouse.pageX}, y: {mouse.pageY}\n      </p>\n      <p>\n        Screen - x: {mouse.screenX}, y: {mouse.screenY}\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useMouse/demo/demo2.tsx",
    "content": "/**\n * title: Mouse position relative to the element\n * desc: By passing in the target element, you can get the position of the mouse relative to the element.\n *\n * title.zh-CN: 获取鼠标相对于元素的位置\n * desc.zh-CN: 通过传入目标元素，可以获取鼠标相对于元素的位置。\n */\n\nimport { useRef } from 'react';\nimport { useMouse } from 'ahooks';\n\nexport default () => {\n  const ref = useRef(null);\n  const mouse = useMouse(ref.current);\n\n  return (\n    <>\n      <div\n        ref={ref}\n        style={{\n          width: '200px',\n          height: '200px',\n          backgroundColor: 'gray',\n          color: 'white',\n          lineHeight: '200px',\n          textAlign: 'center',\n        }}\n      >\n        element\n      </div>\n      <div>\n        <p>\n          Mouse In Element - x: {mouse.elementX}, y: {mouse.elementY}\n        </p>\n        <p>\n          Element Position - x: {mouse.elementPosX}, y: {mouse.elementPosY}\n        </p>\n        <p>\n          Element Dimensions - width: {mouse.elementW}, height: {mouse.elementH}\n        </p>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useMouse/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useMouse\n\nTrack cursor position\n\n## Examples\n\n### Default Usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Mouse position relative to the element\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst state: {\n  screenX: number,\n  screenY: number,\n  clientX: number,\n  clientY: number,\n  pageX: number,\n  pageY: number,\n  elementX: number,\n  elementY: number,\n  elementH: number,\n  elementW: number,\n  elementPosX: number,\n  elementPosY: number,\n} = useMouse(target?: Target);\n```\n\n### Params\n\n| Property | Description        | Type                                                        | Default |\n| -------- | ------------------ | ----------------------------------------------------------- | ------- |\n| target   | DOM element or ref | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -       |\n\n### result\n\n| Property    | Description                                                                                                        | Type     |\n| ----------- | ------------------------------------------------------------------------------------------------------------------ | -------- |\n| screenX     | Position left relative to the top left of the physical screen/monitor                                              | `number` |\n| screenY     | Position top relative to the top left of the physical screen/monitor                                               | `number` |\n| clientX     | Position left relative to the upper left edge of the content area                                                  | `number` |\n| clientY     | Position top relative to the upper left edge of the content area                                                   | `number` |\n| pageX       | Position left relative to the top left of the fully rendered content area in the browser                           | `number` |\n| pageY       | Position top relative to the top left of the fully rendered content area in the browser                            | `number` |\n| elementX    | Position left relative to the upper left edge of the target element                                                | `number` |\n| elementY    | Position top relative to the upper left edge of the target element                                                 | `number` |\n| elementH    | Target element height                                                                                              | `number` |\n| elementW    | Target element width                                                                                               | `number` |\n| elementPosX | The position of the target element left relative to the top left of the fully rendered content area in the browser | `number` |\n| elementPosY | The position of the target element top relative to the top left of the fully rendered content area in the browser  | `number` |\n"
  },
  {
    "path": "packages/hooks/src/useMouse/index.ts",
    "content": "import useRafState from '../useRafState';\nimport useEventListener from '../useEventListener';\nimport type { BasicTarget } from '../utils/domTarget';\nimport { getTargetElement } from '../utils/domTarget';\n\nexport interface CursorState {\n  screenX: number;\n  screenY: number;\n  clientX: number;\n  clientY: number;\n  pageX: number;\n  pageY: number;\n  elementX: number;\n  elementY: number;\n  elementH: number;\n  elementW: number;\n  elementPosX: number;\n  elementPosY: number;\n}\n\nconst initState: CursorState = {\n  screenX: NaN,\n  screenY: NaN,\n  clientX: NaN,\n  clientY: NaN,\n  pageX: NaN,\n  pageY: NaN,\n  elementX: NaN,\n  elementY: NaN,\n  elementH: NaN,\n  elementW: NaN,\n  elementPosX: NaN,\n  elementPosY: NaN,\n};\n\nexport default (target?: BasicTarget) => {\n  const [state, setState] = useRafState(initState);\n\n  useEventListener(\n    'mousemove',\n    (event: MouseEvent) => {\n      const { screenX, screenY, clientX, clientY, pageX, pageY } = event;\n      const newState = {\n        screenX,\n        screenY,\n        clientX,\n        clientY,\n        pageX,\n        pageY,\n        elementX: NaN,\n        elementY: NaN,\n        elementH: NaN,\n        elementW: NaN,\n        elementPosX: NaN,\n        elementPosY: NaN,\n      };\n      const targetElement = getTargetElement(target);\n      if (targetElement) {\n        const { left, top, width, height } = targetElement.getBoundingClientRect();\n        newState.elementPosX = left + window.pageXOffset;\n        newState.elementPosY = top + window.pageYOffset;\n        newState.elementX = pageX - newState.elementPosX;\n        newState.elementY = pageY - newState.elementPosY;\n        newState.elementW = width;\n        newState.elementH = height;\n      }\n      setState(newState);\n    },\n    {\n      target: () => document,\n    },\n  );\n\n  return state;\n};\n"
  },
  {
    "path": "packages/hooks/src/useMouse/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useMouse\n\n监听鼠标位置\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 获取鼠标相对于元素的位置\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst state: {\n  screenX: number,\n  screenY: number,\n  clientX: number,\n  clientY: number,\n  pageX: number,\n  pageY: number,\n  elementX: number,\n  elementY: number,\n  elementH: number,\n  elementW: number,\n  elementPosX: number,\n  elementPosY: number,\n} = useMouse(target?: Target);\n```\n\n### Params\n\n| 参数   | 说明             | 类型                                                        |\n| ------ | ---------------- | ----------------------------------------------------------- |\n| target | DOM 节点或者 Ref | `Element` \\| `() => Element` \\| `MutableRefObject<Element>` |\n\n### Result\n\n| 参数        | 说明                           | 类型     |\n| ----------- | ------------------------------ | -------- |\n| screenX     | 距离显示器左侧的距离           | `number` |\n| screenY     | 距离显示器顶部的距离           | `number` |\n| clientX     | 距离当前视窗左侧的距离         | `number` |\n| clientY     | 距离当前视窗顶部的距离         | `number` |\n| pageX       | 距离完整页面左侧的距离         | `number` |\n| pageY       | 距离完整页面顶部的距离         | `number` |\n| elementX    | 距离指定元素左侧的距离         | `number` |\n| elementY    | 距离指定元素顶部的距离         | `number` |\n| elementH    | 指定元素的高                   | `number` |\n| elementW    | 指定元素的宽                   | `number` |\n| elementPosX | 指定元素距离完整页面左侧的距离 | `number` |\n| elementPosY | 指定元素距离完整页面顶部的距离 | `number` |\n"
  },
  {
    "path": "packages/hooks/src/useMutationObserver/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';\nimport useMutationObserver from '../index';\n\nconst options: MutationObserverInit = { attributes: true, childList: true };\n\ndescribe('useMutationObserver', () => {\n  let container: HTMLDivElement;\n\n  beforeEach(() => {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n  });\n\n  afterEach(() => {\n    document.body.removeChild(container);\n  });\n\n  test('should callback work when target style be changed', async () => {\n    const callback = vi.fn();\n    const { rerender } = renderHook(() => useMutationObserver(callback, () => container, options));\n    container.style.backgroundColor = '#000';\n    await rerender();\n    expect(callback).toBeCalled();\n  });\n\n  test('should callback work when target node tree be changed', async () => {\n    const callback = vi.fn();\n    const { rerender } = renderHook(() => useMutationObserver(callback, () => container, options));\n    const paraEl = document.createElement('p');\n    container.appendChild(paraEl);\n    await rerender();\n    expect(callback).toBeCalled();\n  });\n\n  test('should not work when target is null', async () => {\n    const callback = vi.fn();\n    const { rerender } = renderHook(() => useMutationObserver(callback, null, options));\n    container.style.backgroundColor = '#000';\n    await rerender();\n    expect(callback).not.toBeCalled();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useMutationObserver/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n *\n * title.zh-CN: 基础用法\n */\n\nimport { useMutationObserver } from 'ahooks';\nimport { useRef, useState } from 'react';\n\nconst App: React.FC = () => {\n  const [width, setWidth] = useState(200);\n  const [count, setCount] = useState(0);\n\n  const ref = useRef<HTMLDivElement>(null);\n\n  useMutationObserver(\n    (mutationsList) => {\n      mutationsList.forEach(() => setCount((c) => c + 1));\n    },\n    ref,\n    { attributes: true },\n  );\n\n  return (\n    <div>\n      <div ref={ref} style={{ width, padding: 12, border: '1px solid #000', marginBottom: 8 }}>\n        current width：{width}\n      </div>\n      <button onClick={() => setWidth((w) => w + 10)}>widening</button>\n      <p>Mutation count {count}</p>\n    </div>\n  );\n};\n\nexport default App;\n"
  },
  {
    "path": "packages/hooks/src/useMutationObserver/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useMutationObserver\n\nA hook that provides the ability to watch for changes being made to the DOM tree, refer to [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)\n\n## Examples\n\n### Default Usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseMutationObserver(\n  callback: MutationCallback,\n  target: Target,\n  options?: MutationObserverInit,\n);\n```\n\n## Params\n\n| Property | Description           | Type                                                                | Default |\n| -------- | --------------------- | ------------------------------------------------------------------- | ------- |\n| target   | DOM element or ref    | `() => Element` \\| `Element` \\| `MutableRefObject<Element>`         | -       |\n| callback | The callback function | `(mutations: MutationRecord[], observer: MutationObserver) => void` | -       |\n| options  | Setting               | `MutationObserverInit`                                              | -       |\n\n### Options\n\nFor options, please refer to [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#parameters)\n"
  },
  {
    "path": "packages/hooks/src/useMutationObserver/index.ts",
    "content": "import { getTargetElement } from '../utils/domTarget';\nimport type { BasicTarget } from '../utils/domTarget';\nimport useDeepCompareEffectWithTarget from '../utils/useDeepCompareWithTarget';\nimport useLatest from '../useLatest';\n\nconst useMutationObserver = (\n  callback: MutationCallback,\n  target: BasicTarget,\n  options: MutationObserverInit = {},\n): void => {\n  const callbackRef = useLatest(callback);\n\n  useDeepCompareEffectWithTarget(\n    () => {\n      const element = getTargetElement(target);\n      if (!element) {\n        return;\n      }\n      const observer = new MutationObserver(callbackRef.current);\n      observer.observe(element, options);\n      return () => {\n        observer?.disconnect();\n      };\n    },\n    [options],\n    target,\n  );\n};\n\nexport default useMutationObserver;\n"
  },
  {
    "path": "packages/hooks/src/useMutationObserver/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useMutationObserver\n\n一个监听指定的 DOM 树发生变化的 Hook，参考 [MutationObserver](https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver)\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseMutationObserver(\n  callback: MutationCallback,\n  target: Target,\n  options?: MutationObserverInit,\n);\n```\n\n### Params\n\n| 参数     | 说明             | 类型                                                                | 默认值 |\n| -------- | ---------------- | ------------------------------------------------------------------- | ------ |\n| callback | 触发的回调函数   | `(mutations: MutationRecord[], observer: MutationObserver) => void` | -      |\n| target   | DOM 节点或者 Ref | `Element` \\| `() => Element` \\| `MutableRefObject<Element>`         | -      |\n| options  | 设置项           | `MutationObserverInit`                                              | `{}`   |\n\n### Options\n\n配置项请参考 [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#parameters)\n"
  },
  {
    "path": "packages/hooks/src/useNetwork/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useNetwork from '../index';\n\ndescribe('useNetwork', () => {\n  test('toggle network state', () => {\n    const { result } = renderHook(() => useNetwork());\n    expect(result.current.online).toBeTruthy();\n    act(() => {\n      window.dispatchEvent(new Event('offline'));\n    });\n    expect(result.current.online).toBeFalsy();\n    act(() => {\n      window.dispatchEvent(new Event('online'));\n    });\n    expect(result.current.online).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useNetwork/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Return network status\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 返回网络状态信息\n */\n\nimport { useNetwork } from 'ahooks';\n\nexport default () => {\n  const networkState = useNetwork();\n\n  return (\n    <div>\n      <div>Network information: </div>\n      <pre>{JSON.stringify(networkState, null, 2)}</pre>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useNetwork/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useNetwork\n\nA hook that tracks the state of network connection.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\ninterface NetworkState {\n  online?: boolean;\n  since?: Date;\n  rtt?: number;\n  type?: string;\n  downlink?: number;\n  saveData?: boolean;\n  downlinkMax?: number;\n  effectiveType?: string;\n}\n\nconst result: NetworkState = useNetwork();\n```\n\n### Result\n\n| Property      | Description                                                    | Type                                                                                           |\n| ------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |\n| online        | Whether connected to network                                   | `boolean`                                                                                      |\n| since         | `online` latest update time                                    | `Date`                                                                                         |\n| rtt           | The effective round-trip time estimate in milliseconds         | `number`                                                                                       |\n| type          | The connection type that the user agent is using               | `bluetooth` \\| `cellular` \\| `ethernet` \\| `none` \\| `wifi` \\| `wimax` \\| `other` \\| `unknown` |\n| downlink      | The effective bandwidth estimate in megabits per second,       | `number`                                                                                       |\n| downlinkMax   | An upper bound on the downlink speed of the first network hop  | `number`                                                                                       |\n| saveData      | Whether the user agent has set the option to reduce data usage | `boolean`                                                                                      |\n| effectiveType | The effective connection type                                  | `slow-2g` \\| `2g` \\| `3g` \\| `4g`                                                              |\n\nMore information refer to [MDN NetworkInformation](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation)\n"
  },
  {
    "path": "packages/hooks/src/useNetwork/index.ts",
    "content": "import { useEffect, useState } from 'react';\nimport { isObject } from '../utils';\n\nexport interface NetworkState {\n  since?: Date;\n  online?: boolean;\n  rtt?: number;\n  type?: string;\n  downlink?: number;\n  saveData?: boolean;\n  downlinkMax?: number;\n  effectiveType?: string;\n}\n\nenum NetworkEventType {\n  ONLINE = 'online',\n  OFFLINE = 'offline',\n  CHANGE = 'change',\n}\n\nfunction getConnection() {\n  const nav = navigator as any;\n  if (!isObject(nav)) {\n    return null;\n  }\n  return nav.connection || nav.mozConnection || nav.webkitConnection;\n}\n\nfunction getConnectionProperty(): NetworkState {\n  const c = getConnection();\n  if (!c) {\n    return {};\n  }\n  return {\n    rtt: c.rtt,\n    type: c.type,\n    saveData: c.saveData,\n    downlink: c.downlink,\n    downlinkMax: c.downlinkMax,\n    effectiveType: c.effectiveType,\n  };\n}\n\nfunction useNetwork(): NetworkState {\n  const [state, setState] = useState(() => {\n    return {\n      since: undefined,\n      online: navigator?.onLine,\n      ...getConnectionProperty(),\n    };\n  });\n\n  useEffect(() => {\n    const onOnline = () => {\n      setState((prevState) => ({\n        ...prevState,\n        online: true,\n        since: new Date(),\n      }));\n    };\n\n    const onOffline = () => {\n      setState((prevState) => ({\n        ...prevState,\n        online: false,\n        since: new Date(),\n      }));\n    };\n\n    const onConnectionChange = () => {\n      setState((prevState) => ({\n        ...prevState,\n        ...getConnectionProperty(),\n      }));\n    };\n\n    window.addEventListener(NetworkEventType.ONLINE, onOnline);\n    window.addEventListener(NetworkEventType.OFFLINE, onOffline);\n\n    const connection = getConnection();\n    connection?.addEventListener(NetworkEventType.CHANGE, onConnectionChange);\n\n    return () => {\n      window.removeEventListener(NetworkEventType.ONLINE, onOnline);\n      window.removeEventListener(NetworkEventType.OFFLINE, onOffline);\n      connection?.removeEventListener(NetworkEventType.CHANGE, onConnectionChange);\n    };\n  }, []);\n\n  return state;\n}\n\nexport default useNetwork;\n"
  },
  {
    "path": "packages/hooks/src/useNetwork/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useNetwork\n\n管理网络连接状态的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\ninterface NetworkState {\n  online?: boolean;\n  since?: Date;\n  rtt?: number;\n  type?: string;\n  downlink?: number;\n  saveData?: boolean;\n  downlinkMax?: number;\n  effectiveType?: string;\n}\n\nconst result: NetworkState = useNetwork();\n```\n\n### Result\n\n| 属性          | 描述                                   | 类型                                                                                           |\n| ------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------- |\n| online        | 网络是否为在线                         | `boolean`                                                                                      |\n| since         | `online` 最后改变时间                  | `Date`                                                                                         |\n| rtt           | 当前连接下评估的往返时延               | `number`                                                                                       |\n| type          | 设备使用与所述网络进行通信的连接的类型 | `bluetooth` \\| `cellular` \\| `ethernet` \\| `none` \\| `wifi` \\| `wimax` \\| `other` \\| `unknown` |\n| downlink      | 有效带宽估算（单位：兆比特/秒）        | `number`                                                                                       |\n| downlinkMax   | 最大下行速度（单位：兆比特/秒）        | `number`                                                                                       |\n| saveData      | 用户代理是否设置了减少数据使用的选项   | `boolean`                                                                                      |\n| effectiveType | 网络连接的类型                         | `slow-2g` \\| `2g` \\| `3g` \\| `4g`                                                              |\n\n更多信息参考：[MDN NetworkInformation](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation)\n"
  },
  {
    "path": "packages/hooks/src/usePagination/__tests__/index.spec.ts",
    "content": "import type { RenderHookResult } from '@testing-library/react';\nimport { act, renderHook, waitFor } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport usePagination from '../';\n\n// 初始化\n// 基本 action\n// refreshDeps\n// cache\n\ndescribe('usePagination', () => {\n  let queryArgs: any;\n  const asyncFn = (query: any) => {\n    queryArgs = query;\n    return Promise.resolve({\n      current: query.current,\n      total: 55,\n      pageSize: query.pageSize,\n      list: [],\n    });\n  };\n\n  const setUp = (\n    service: Parameters<typeof usePagination>[0],\n    options: Parameters<typeof usePagination>[1],\n  ) => renderHook((o) => usePagination(service, o || options));\n\n  let hook: RenderHookResult<any, any>;\n\n  test('should fetch after first render', async () => {\n    queryArgs = undefined;\n    act(() => {\n      hook = setUp(asyncFn, {});\n    });\n    expect(hook.result.current.loading).toBe(true);\n    expect(queryArgs.current).toBe(1);\n    expect(queryArgs.pageSize).toBe(10);\n    await waitFor(() => expect(hook.result.current.loading).toBe(false));\n\n    expect(hook.result.current.pagination.current).toBe(1);\n    expect(hook.result.current.pagination.pageSize).toBe(10);\n    expect(hook.result.current.pagination.total).toBe(55);\n    expect(hook.result.current.pagination.totalPage).toBe(6);\n  });\n\n  test('should action work', async () => {\n    queryArgs = undefined;\n    act(() => {\n      hook = setUp(asyncFn, {});\n    });\n    expect(hook.result.current.loading).toBe(true);\n    expect(queryArgs.current).toBe(1);\n    expect(queryArgs.pageSize).toBe(10);\n    await waitFor(() => expect(hook.result.current.loading).toBe(false));\n\n    act(() => {\n      hook.result.current.pagination.changeCurrent(2);\n    });\n    expect(hook.result.current.loading).toBe(true);\n    expect(queryArgs.current).toBe(2);\n    expect(queryArgs.pageSize).toBe(10);\n    await waitFor(() => expect(hook.result.current.pagination.current).toBe(2));\n\n    act(() => {\n      hook.result.current.pagination.changeCurrent(10);\n    });\n    expect(hook.result.current.loading).toBe(true);\n    expect(queryArgs.current).toBe(6);\n    expect(queryArgs.pageSize).toBe(10);\n    await waitFor(() => expect(hook.result.current.pagination.current).toBe(6));\n\n    act(() => {\n      hook.result.current.pagination.changePageSize(20);\n    });\n    expect(hook.result.current.loading).toBe(true);\n    expect(queryArgs.current).toBe(3);\n    expect(queryArgs.pageSize).toBe(20);\n    await waitFor(() => expect(hook.result.current.pagination.current).toBe(3));\n    expect(hook.result.current.pagination.pageSize).toBe(20);\n    expect(hook.result.current.pagination.totalPage).toBe(3);\n\n    act(() => {\n      hook.result.current.pagination.onChange(2, 10);\n    });\n    expect(hook.result.current.loading).toBe(true);\n    expect(queryArgs.current).toBe(2);\n    expect(queryArgs.pageSize).toBe(10);\n    await waitFor(() => expect(hook.result.current.pagination.current).toBe(2));\n    expect(hook.result.current.pagination.pageSize).toBe(10);\n    expect(hook.result.current.pagination.totalPage).toBe(6);\n  });\n\n  test('should refreshDeps work', async () => {\n    queryArgs = undefined;\n    let dep = 1;\n    act(() => {\n      hook = setUp(asyncFn, {\n        refreshDeps: [dep],\n      });\n    });\n    expect(hook.result.current.loading).toBe(true);\n    expect(queryArgs.current).toBe(1);\n    expect(queryArgs.pageSize).toBe(10);\n    await waitFor(() => expect(hook.result.current.loading).toBe(false));\n\n    act(() => {\n      hook.result.current.pagination.onChange(3, 20);\n    });\n    expect(hook.result.current.loading).toBe(true);\n    await waitFor(() => expect(hook.result.current.pagination.current).toBe(3));\n    expect(hook.result.current.pagination.pageSize).toBe(20);\n\n    dep = 2;\n    hook.rerender({\n      refreshDeps: [dep],\n    });\n\n    expect(hook.result.current.loading).toBe(true);\n    expect(queryArgs.current).toBe(1);\n    expect(queryArgs.pageSize).toBe(20);\n    await waitFor(() => expect(hook.result.current.pagination.current).toBe(1));\n    expect(hook.result.current.pagination.pageSize).toBe(20);\n  });\n\n  test('should default params work', async () => {\n    queryArgs = undefined;\n    act(() => {\n      hook = setUp(asyncFn, {\n        defaultPageSize: 5,\n        defaultCurrent: 2,\n      });\n    });\n    expect(hook.result.current.loading).toBe(true);\n    expect(queryArgs.current).toBe(2);\n    expect(queryArgs.pageSize).toBe(5);\n    await waitFor(() => expect(hook.result.current.loading).toBe(false));\n\n    expect(hook.result.current.pagination.current).toBe(2);\n    expect(hook.result.current.pagination.pageSize).toBe(5);\n    expect(hook.result.current.pagination.total).toBe(55);\n    expect(hook.result.current.pagination.totalPage).toBe(11);\n\n    act(() => {\n      hook.result.current.pagination.changeCurrent(3);\n    });\n    expect(hook.result.current.loading).toBe(true);\n    expect(queryArgs.current).toBe(3);\n    expect(queryArgs.pageSize).toBe(5);\n    await waitFor(() => expect(hook.result.current.pagination.current).toBe(3));\n    expect(hook.result.current.pagination.pageSize).toBe(5);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/usePagination/demo/demo1.tsx",
    "content": "import { usePagination } from 'ahooks';\nimport { Pagination } from 'antd';\nimport Mock from 'mockjs';\ninterface UserListItem {\n  id: string;\n  name: string;\n  gender: 'male' | 'female';\n  email: string;\n  disabled: boolean;\n}\n\nconst userList = (current: number, pageSize: number) =>\n  Mock.mock({\n    total: 55,\n    [`list|${pageSize}`]: [\n      {\n        id: '@guid',\n        name: '@name',\n        'gender|1': ['male', 'female'],\n        email: '@email',\n        disabled: false,\n      },\n    ],\n  });\n\nasync function getUserList(params: {\n  current: number;\n  pageSize: number;\n}): Promise<{ total: number; list: UserListItem[] }> {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(userList(params.current, params.pageSize));\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const { data, loading, pagination } = usePagination(getUserList);\n  return (\n    <div>\n      {loading ? (\n        <p>loading</p>\n      ) : (\n        <ul>\n          {data?.list?.map((item) => (\n            <li key={item.email}>\n              {item.name} - {item.email}\n            </li>\n          ))}\n        </ul>\n      )}\n      <Pagination\n        current={pagination.current}\n        pageSize={pagination.pageSize}\n        total={data?.total}\n        onChange={pagination.onChange}\n        onShowSizeChange={pagination.onChange}\n        showQuickJumper\n        showSizeChanger\n        style={{ marginTop: 16, textAlign: 'right' }}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/usePagination/demo/demo2.tsx",
    "content": "import { Pagination } from 'antd';\nimport Mock from 'mockjs';\nimport { useEffect, useState } from 'react';\nimport { usePagination } from 'ahooks';\n\ninterface UserListItem {\n  id: string;\n  name: string;\n  gender: 'male' | 'female';\n  email: string;\n  disabled: boolean;\n}\n\nconst userList = (current: number, pageSize: number) =>\n  Mock.mock({\n    total: 55,\n    [`list|${pageSize}`]: [\n      {\n        id: '@guid',\n        name: '@name',\n        'gender|1': ['male', 'female'],\n        email: '@email',\n        disabled: false,\n      },\n    ],\n  });\n\nasync function getUserList(params: {\n  current: number;\n  pageSize: number;\n  gender: string;\n}): Promise<{ total: number; list: UserListItem[] }> {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(userList(params.current, params.pageSize));\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const [gender, setGender] = useState<string>('male');\n\n  const { data, loading, pagination, run, params } = usePagination(\n    ({ current, pageSize }, g: string) => {\n      return getUserList({\n        current,\n        pageSize,\n        gender: g,\n      });\n    },\n    {\n      manual: true,\n    },\n  );\n\n  useEffect(() => {\n    run(\n      {\n        current: 1,\n        pageSize: params[0]?.pageSize || 10,\n      },\n      gender,\n    );\n  }, [gender]);\n\n  return (\n    <div>\n      <select\n        value={gender}\n        style={{ width: 180, marginBottom: 24 }}\n        onChange={(e) => setGender(e.target.value)}\n      >\n        <option value=\"male\">male</option>\n        <option value=\"female\">female</option>\n      </select>\n      {loading ? (\n        <p>loading</p>\n      ) : (\n        <ul>\n          {data?.list?.map((item) => (\n            <li key={item.email}>\n              {item.name} - {item.email}\n            </li>\n          ))}\n        </ul>\n      )}\n      <Pagination\n        current={pagination.current}\n        pageSize={pagination.pageSize}\n        total={data?.total}\n        onChange={pagination.onChange}\n        onShowSizeChange={pagination.onChange}\n        showQuickJumper\n        showSizeChanger\n        style={{ marginTop: 16, textAlign: 'right' }}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/usePagination/demo/demo3.tsx",
    "content": "import { usePagination } from 'ahooks';\nimport { Pagination } from 'antd';\nimport Mock from 'mockjs';\nimport { useState } from 'react';\n\ninterface UserListItem {\n  id: string;\n  name: string;\n  gender: 'male' | 'female';\n  email: string;\n  disabled: boolean;\n}\n\nconst userList = (current: number, pageSize: number) =>\n  Mock.mock({\n    total: 55,\n    [`list|${pageSize}`]: [\n      {\n        id: '@guid',\n        name: '@name',\n        'gender|1': ['male', 'female'],\n        email: '@email',\n        disabled: false,\n      },\n    ],\n  });\n\nasync function getUserList(params: {\n  current: number;\n  pageSize: number;\n  gender: string;\n}): Promise<{ total: number; list: UserListItem[] }> {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(userList(params.current, params.pageSize));\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const [gender, setGender] = useState<string>('male');\n\n  const { data, loading, pagination } = usePagination(\n    ({ current, pageSize }) => {\n      return getUserList({\n        current,\n        pageSize,\n        gender,\n      });\n    },\n    {\n      refreshDeps: [gender],\n    },\n  );\n\n  return (\n    <div>\n      <select\n        value={gender}\n        style={{ width: 180, marginBottom: 24 }}\n        onChange={(e) => setGender(e.target.value)}\n      >\n        <option value=\"male\">male</option>\n        <option value=\"female\">female</option>\n      </select>\n      {loading ? (\n        <p>loading</p>\n      ) : (\n        <ul>\n          {data?.list?.map((item) => (\n            <li key={item.email}>\n              {item.name} - {item.email}\n            </li>\n          ))}\n        </ul>\n      )}\n      <Pagination\n        current={pagination.current}\n        pageSize={pagination.pageSize}\n        total={data?.total}\n        onChange={pagination.onChange}\n        onShowSizeChange={pagination.onChange}\n        showQuickJumper\n        showSizeChanger\n        style={{ marginTop: 16, textAlign: 'right' }}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/usePagination/demo/demo4.tsx",
    "content": "import { useBoolean, useUpdateEffect } from 'ahooks';\nimport { Pagination } from 'antd';\nimport Mock from 'mockjs';\nimport { useState } from 'react';\nimport { usePagination } from 'ahooks';\n\ninterface UserListItem {\n  id: string;\n  name: string;\n  gender: 'male' | 'female';\n  email: string;\n  disabled: boolean;\n}\n\nconst userList = (current: number, pageSize: number) =>\n  Mock.mock({\n    total: 55,\n    [`list|${pageSize}`]: [\n      {\n        id: '@guid',\n        name: '@name',\n        'gender|1': ['male', 'female'],\n        email: '@email',\n        disabled: false,\n      },\n    ],\n  });\n\nasync function getUserList(params: {\n  current: number;\n  pageSize: number;\n  gender: string;\n}): Promise<{ total: number; list: UserListItem[] }> {\n  console.log('cache demo', params.current, params.pageSize, params.gender);\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(userList(params.current, params.pageSize));\n    }, 1000);\n  });\n}\n\nconst PaginationComponent: React.FC = () => {\n  const { data, loading, pagination, run, params } = usePagination(\n    ({ current, pageSize }, g: string) => {\n      return getUserList({\n        current,\n        pageSize,\n        gender: g,\n      });\n    },\n    {\n      cacheKey: 'userList',\n    },\n  );\n\n  const [gender, setGender] = useState<string>(params[1] || 'male');\n\n  useUpdateEffect(() => {\n    run(\n      {\n        current: 1,\n        pageSize: params[0]?.pageSize || 10,\n      },\n      gender,\n    );\n  }, [gender]);\n\n  return (\n    <div>\n      <select\n        value={gender}\n        style={{ width: 180, marginBottom: 24 }}\n        onChange={(e) => setGender(e.target.value)}\n      >\n        <option value=\"male\">male</option>\n        <option value=\"female\">female</option>\n      </select>\n      {loading && !data ? (\n        <p>loading</p>\n      ) : (\n        <ul>\n          {data?.list?.map((item) => (\n            <li key={item.email}>\n              {item.name} - {item.email}\n            </li>\n          ))}\n        </ul>\n      )}\n      <Pagination\n        current={pagination.current}\n        pageSize={pagination.pageSize}\n        total={data?.total}\n        onChange={pagination.onChange}\n        onShowSizeChange={pagination.onChange}\n        showQuickJumper\n        showSizeChanger\n        style={{ marginTop: 16, textAlign: 'right' }}\n      />\n    </div>\n  );\n};\n\nexport default () => {\n  const [state, { toggle }] = useBoolean();\n  return (\n    <div>\n      <p>You can click the button multiple times, the conditions of pagination will be cached.</p>\n      <p>\n        <button type=\"button\" onClick={() => toggle()}>\n          show/hide\n        </button>\n      </p>\n      {state && <PaginationComponent />}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/usePagination/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# usePagination\n\n`usePagination` is implemented based on `useRequest` and encapsulates common paging logic. The differences from `useRequest` are as follows:\n\n1. The first parameter of `service` is `{ current: number, pageSize: number }`\n2. The data structure returned by `service` is `{ total: number, list: Item[] }`\n3. It will additionally return the `pagination` field, which contains all the pagination information and functions to operate the pagination.\n4. When `refreshDeps` changes, it will reset `current` to the first page and re-initiate the request. Generally, you can put the conditions that `pagination` depends on here\n\n## Examples\n\n### Basic usage\n\nThe default usage is the same as `useRequest`, but an additional `pagination` parameter will be returned, which contains all pagination information and functions to operate pagination.\n\n<code src=\"./demo/demo1.tsx\" />\n\n### More parameters\n\nThe following code demonstrates that the gender parameter is added. When the gender is modified, the paging is reset to the first page and the data is requested again.\n\n<code src=\"./demo/demo2.tsx\" />\n\n### refreshDeps\n\n`refreshDeps` is a syntactic sugar. When it changes, it will reset the page to the first page and request data again. Generally, you can put the dependent conditions here. The following example implements the previous function more conveniently through `refreshDeps`.\n\n<code src=\"./demo/demo3.tsx\" />\n\n### Cache\n\nThrough the `params` caching capability of `useRequest`, we can cache paging data and other conditions.\n\n<code src=\"./demo/demo4.tsx\" />\n\n## API\n\nAll parameters and returned results of `useRequest` are applicable to `usePagination`, so we won't repeat them here.\n\n```typescript\n\ntype Data<T> = { total: number; list: T[] };\ntype Params = [{ current: number; pageSize: number, [key: string]: any }, ...any[]];\n\nconst {\n  ...,\n  pagination: {\n    current: number;\n    pageSize: number;\n    total: number;\n    totalPage: number;\n    onChange: (current: number, pageSize: number) => void;\n    changeCurrent: (current: number) => void;\n    changePageSize: (pageSize: number) => void;\n  }\n} = usePagination<TData extends Data, TParams extends Params>(\n  service: (...args: TParams) => Promise<TData>,\n  {\n    ...,\n    defaultPageSize?: number;\n    refreshDeps?: any[];\n  }\n);\n```\n\n### Result\n\n| Property   | Description                                 | Type |\n| ---------- | ------------------------------------------- | ---- |\n| pagination | Paging data and methods of paging operation | `-`  |\n\n### Params\n\n| Property        | Description                                                                                                                                      | Type                   | Default |\n| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------- | ------- |\n| defaultPageSize | Default page size                                                                                                                                | `number`               | 10       |\n| defaultCurrent  | Number of pages on initial request                                                                                                               | `number`               | 1       |\n| refreshDeps     | Changes in `refreshDeps` will reset current to the first page and re-initiate the request. Generally, you can put the dependent conditions here. | `React.DependencyList` | `[]`    |\n"
  },
  {
    "path": "packages/hooks/src/usePagination/index.ts",
    "content": "import { useMemo } from 'react';\nimport useMemoizedFn from '../useMemoizedFn';\nimport useRequest from '../useRequest';\n\nimport type { Data, PaginationOptions, Params, Service, PaginationResult } from './types';\n\nconst usePagination = <TData extends Data, TParams extends Params>(\n  service: Service<TData, TParams>,\n  options: PaginationOptions<TData, TParams> = {},\n) => {\n  const { defaultPageSize = 10, defaultCurrent = 1, ...rest } = options;\n\n  const result = useRequest(service, {\n    defaultParams: [{ current: defaultCurrent, pageSize: defaultPageSize }] as unknown as TParams,\n    refreshDepsAction: () => {\n      // eslint-disable-next-line @typescript-eslint/no-use-before-define\n      changeCurrent(1);\n    },\n    ...rest,\n  });\n\n  const { current = 1, pageSize = defaultPageSize } = result.params[0] || {};\n\n  const total = result.data?.total || 0;\n  const totalPage = useMemo(() => Math.ceil(total / pageSize), [pageSize, total]);\n\n  const onChange = (c: number, p: number) => {\n    let toCurrent = c <= 0 ? 1 : c;\n    const toPageSize = p <= 0 ? 1 : p;\n    const tempTotalPage = Math.ceil(total / toPageSize);\n    if (toCurrent > tempTotalPage) {\n      toCurrent = Math.max(1, tempTotalPage);\n    }\n\n    const [oldPaginationParams = {}, ...restParams] = result.params || [];\n\n    result.run(\n      ...([\n        {\n          ...oldPaginationParams,\n          current: toCurrent,\n          pageSize: toPageSize,\n        },\n        ...restParams,\n      ] as TParams),\n    );\n  };\n\n  const changeCurrent = (c: number) => {\n    onChange(c, pageSize);\n  };\n\n  const changePageSize = (p: number) => {\n    onChange(current, p);\n  };\n\n  return {\n    ...result,\n    pagination: {\n      current,\n      pageSize,\n      total,\n      totalPage,\n      onChange: useMemoizedFn(onChange),\n      changeCurrent: useMemoizedFn(changeCurrent),\n      changePageSize: useMemoizedFn(changePageSize),\n    },\n  } as PaginationResult<TData, TParams>;\n};\n\nexport default usePagination;\n"
  },
  {
    "path": "packages/hooks/src/usePagination/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# usePagination\n\n`usePagination` 基于 `useRequest` 实现，封装了常见的分页逻辑。与 `useRequest` 不同的点有以下几点：\n\n1. `service` 的第一个参数为 `{ current: number, pageSize: number }`\n2. `service` 返回的数据结构为 `{ total: number, list: Item[] }`\n3. 会额外返回 `pagination` 字段，包含所有分页信息，及操作分页的函数。\n4. `refreshDeps` 变化，会重置 `current` 到第一页，并重新发起请求，一般你可以把 `pagination` 依赖的条件放这里\n\n## 代码演示\n\n### 基础用法\n\n默认用法与 `useRequest` 一致，但会多返回一个 `pagination` 参数，包含所有分页信息，及操作分页的函数。\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 更多参数\n\n下面的代码演示了，增加了性别参数，在修改性别时，重置分页到第一页，并重新请求数据。\n\n<code src=\"./demo/demo2.tsx\" />\n\n### refreshDeps\n\n`refreshDeps` 是一个语法糖，当它变化时，会重置分页到第一页，并重新请求数据，一般你可以把依赖的条件放这里。以下示例通过 `refreshDeps` 更方便的实现了上一个功能。\n\n<code src=\"./demo/demo3.tsx\" />\n\n### 缓存\n\n通过 `useRequest` 的 `params` 缓存能力，我们可以缓存分页数据和其它条件。\n\n<code src=\"./demo/demo4.tsx\" />\n\n## API\n\n`useRequest` 所有参数和返回结果均适用于 `usePagination`，此处不再赘述。\n\n```typescript\n\ntype Data<T> = { total: number; list: T[] };\ntype Params = [{ current: number; pageSize: number, [key: string]: any }, ...any[]];\n\nconst {\n  ...,\n  pagination: {\n    current: number;\n    pageSize: number;\n    total: number;\n    totalPage: number;\n    onChange: (current: number, pageSize: number) => void;\n    changeCurrent: (current: number) => void;\n    changePageSize: (pageSize: number) => void;\n  }\n} = usePagination<TData extends Data, TParams extends Params>(\n  service: (...args: TParams) => Promise<TData>,\n  {\n    ...,\n    defaultPageSize?: number;\n    refreshDeps?: any[];\n  }\n);\n```\n\n### Result\n\n| 参数       | 说明                     | 类型 |\n| ---------- | ------------------------ | ---- |\n| pagination | 分页数据及操作分页的方法 | `-`  |\n\n### Params\n\n| 参数            | 说明                                                                                        | 类型                   | 默认值 |\n| --------------- | ------------------------------------------------------------------------------------------- | ---------------------- | ------ |\n| defaultPageSize | 默认分页数量                                                                                | `number`               | 10      |\n| defaultCurrent  | 初次请求时的页数                                                                            | `number`               | 1      |\n| refreshDeps     | `refreshDeps` 变化，会重置 current 到第一页，并重新发起请求，一般你可以把依赖的条件放这里。 | `React.DependencyList` | `[]`   |\n"
  },
  {
    "path": "packages/hooks/src/usePagination/types.ts",
    "content": "import type { Options, Result } from '../useRequest/src/types';\n\nexport type Data = { total: number; list: any[] };\n\nexport type Params = [{ current: number; pageSize: number; [key: string]: any }, ...any[]];\n\nexport type Service<TData extends Data, TParams extends Params> = (\n  ...args: TParams\n) => Promise<TData>;\n\nexport interface PaginationResult<TData extends Data, TParams extends Params>\n  extends Result<TData, TParams> {\n  pagination: {\n    current: number;\n    pageSize: number;\n    total: number;\n    totalPage: number;\n    onChange: (current: number, pageSize: number) => void;\n    changeCurrent: (current: number) => void;\n    changePageSize: (pageSize: number) => void;\n  };\n}\n\nexport interface PaginationOptions<TData extends Data, TParams extends Params>\n  extends Options<TData, TParams> {\n  defaultPageSize?: number;\n  defaultCurrent?: number;\n}\n"
  },
  {
    "path": "packages/hooks/src/usePrevious/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport type { ShouldUpdateFunc } from '../';\nimport usePrevious from '../';\n\ndescribe('usePrevious', () => {\n  function getHook<T>(initialValue?: T, compareFunction?: ShouldUpdateFunc<T>) {\n    return renderHook(({ val, cmp }) => usePrevious<T>(val as T, cmp), {\n      initialProps: {\n        val: initialValue || 0,\n        cmp: compareFunction,\n      } as { val?: T; cmp?: ShouldUpdateFunc<T> },\n    });\n  }\n\n  test('should return undefined on init', () => {\n    expect(getHook().result.current).toBeUndefined();\n  });\n\n  test('should update previous value only after render with different value', () => {\n    const hook = getHook(0, () => true);\n\n    expect(hook.result.current).toBeUndefined();\n    hook.rerender({ val: 1 });\n    expect(hook.result.current).toBe(0);\n\n    hook.rerender({ val: 2 });\n    expect(hook.result.current).toBe(1);\n\n    hook.rerender({ val: 3 });\n    expect(hook.result.current).toBe(2);\n\n    hook.rerender({ val: 4 });\n    expect(hook.result.current).toBe(3);\n\n    hook.rerender({ val: 5 });\n    expect(hook.result.current).toBe(4);\n  });\n\n  test('should not update previous value if current value is the same', () => {\n    const hook = getHook(0);\n    expect(hook.result.current).toBeUndefined();\n    hook.rerender({ val: 1 });\n    expect(hook.result.current).toBe(0);\n    hook.rerender({ val: 1 });\n    expect(hook.result.current).toBe(0);\n  });\n\n  test('should work fine with `undefined` values', () => {\n    const hook = renderHook(({ value }) => usePrevious(value), {\n      initialProps: { value: undefined as undefined | number },\n    });\n\n    expect(hook.result.current).toBeUndefined();\n\n    hook.rerender({ value: 1 });\n    expect(hook.result.current).toBeUndefined();\n\n    hook.rerender({ value: undefined });\n    expect(hook.result.current).toBe(1);\n\n    hook.rerender({ value: 2 });\n    expect(hook.result.current).toBeUndefined();\n  });\n\n  test('should receive a predicate as a second parameter that will compare prev and current', () => {\n    const obj1 = { label: 'John', value: 'john' };\n    const obj2 = { label: 'Jonny', value: 'john' };\n    const obj3 = { label: 'Kate', value: 'kate' };\n    type Obj = { label: string; value: string };\n    const predicate = (a: Obj | undefined, b: Obj) => (a ? a.value !== b.value : true);\n\n    const hook = getHook(obj1 as Obj, predicate as any);\n\n    expect(hook.result.current).toBeUndefined();\n\n    hook.rerender({ val: obj2, cmp: predicate as any });\n\n    expect(hook.result.current).toBeUndefined();\n\n    hook.rerender({ val: obj3, cmp: predicate as any });\n\n    expect(hook.result.current).toBe(obj1);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/usePrevious/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Record the previous value.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 记录上次的 count 值\n */\n\nimport { usePrevious } from 'ahooks';\nimport { useState } from 'react';\n\nexport default () => {\n  const [count, setCount] = useState(0);\n  const previous = usePrevious(count);\n  return (\n    <>\n      <div>counter current value: {count}</div>\n      <div style={{ marginBottom: 8 }}>counter previous value: {previous}</div>\n      <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\n        increase\n      </button>\n      <button type=\"button\" style={{ marginLeft: 8 }} onClick={() => setCount((c) => c - 1)}>\n        decrease\n      </button>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/usePrevious/demo/demo2.tsx",
    "content": "/**\n * title: Custom shouldUpdate function\n * desc: Previous value update only when the shouldUpdate function return true.\n *\n * title.zh-CN: 自定义 shouldUpdate 函数\n * desc.zh-CN: 只有 shouldUpdate function 返回 true 时，才会记录值的变化。\n */\n\nimport { useState } from 'react';\nimport { usePrevious } from 'ahooks';\n\ninterface Person {\n  name: string;\n  job: string;\n}\n\nconst nameCompareFunction = (prev: Person | undefined, next: Person) => {\n  if (!prev) {\n    return true;\n  }\n  if (prev.name !== next.name) {\n    return true;\n  }\n  return false;\n};\n\nconst jobCompareFunction = (prev: Person | undefined, next: Person) => {\n  if (!prev) {\n    return true;\n  }\n  if (prev.job !== next.job) {\n    return true;\n  }\n  return false;\n};\n\nexport default () => {\n  const [state, setState] = useState({ name: 'Jack', job: 'student' });\n  const [nameInput, setNameInput] = useState('');\n  const [jobInput, setJobInput] = useState('');\n  const previousName = usePrevious(state, nameCompareFunction as any);\n  const previousJob = usePrevious(state, jobCompareFunction as any);\n\n  return (\n    <>\n      <div style={{ margin: '8px 0', border: '1px solid #e8e8e8', padding: 8 }}>\n        <div>current name: {state.name}</div>\n        <div>current job: {state.job}</div>\n      </div>\n      <div>previous name: {(previousName || {}).name}</div>\n      <div style={{ marginBottom: 8 }}>previous job: {(previousJob || {}).job}</div>\n      <div style={{ marginTop: 8 }}>\n        <input\n          style={{ width: 220 }}\n          value={nameInput}\n          onChange={(e) => setNameInput(e.target.value)}\n          placeholder=\"new name\"\n        />\n        <button\n          type=\"button\"\n          onClick={() => {\n            setState((s) => ({ ...s, name: nameInput }));\n          }}\n          style={{ marginLeft: 8 }}\n        >\n          update\n        </button>\n      </div>\n      <div style={{ marginTop: 8 }}>\n        <input\n          style={{ width: 220 }}\n          value={jobInput}\n          onChange={(e) => setJobInput(e.target.value)}\n          placeholder=\"new job\"\n        />\n        <button\n          type=\"button\"\n          onClick={() => {\n            setState((s) => ({ ...s, job: jobInput }));\n          }}\n          style={{ marginLeft: 8 }}\n        >\n          update\n        </button>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/usePrevious/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# usePrevious\n\nA Hook to return the previous state.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Custom shouldUpdate function\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst previousState: T = usePrevious<T>(\n  state: T,\n  shouldUpdate?: (prev: T | undefined, next: T) => boolean\n);\n```\n\n### Result\n\n| Property      | Description        | Type |\n| ------------- | ------------------ | ---- |\n| previousState | The previous value | `T`  |\n\n### Params\n\n| Property     | Description                                                   | Type                                         | Default                      |\n| ------------ | ------------------------------------------------------------- | -------------------------------------------- | ---------------------------- |\n| state        | The state that needs to be tracked                            | `T`                                          | -                            |\n| shouldUpdate | Optional. Customize whether the state value should be updated | `(prev: T \\| undefined, next: T) => boolean` | `(a, b) => !Object.is(a, b)` |\n"
  },
  {
    "path": "packages/hooks/src/usePrevious/index.ts",
    "content": "import { useRef } from 'react';\n\nexport type ShouldUpdateFunc<T> = (prev?: T, next?: T) => boolean;\n\nconst defaultShouldUpdate = <T>(a?: T, b?: T) => !Object.is(a, b);\n\nfunction usePrevious<T>(\n  state: T,\n  shouldUpdate: ShouldUpdateFunc<T> = defaultShouldUpdate,\n): T | undefined {\n  const prevRef = useRef<T>(undefined);\n  const curRef = useRef<T>(undefined);\n\n  if (shouldUpdate(curRef.current, state)) {\n    prevRef.current = curRef.current;\n    curRef.current = state;\n  }\n\n  return prevRef.current;\n}\n\nexport default usePrevious;\n"
  },
  {
    "path": "packages/hooks/src/usePrevious/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# usePrevious\n\n保存上一次状态的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 自定义 shouldUpdate 函数\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst previousState: T = usePrevious<T>(\n  state: T,\n  shouldUpdate?: (prev: T | undefined, next: T) => boolean\n);\n```\n\n### Result\n\n| 参数          | 说明            | 类型 |\n| ------------- | --------------- | ---- |\n| previousState | 上次 state 的值 | `T`  |\n\n### Params\n\n| 参数         | 说明                       | 类型                                         | 默认值                       |\n| ------------ | -------------------------- | -------------------------------------------- | ---------------------------- |\n| state        | 需要记录变化的值           | `T`                                          | -                            |\n| shouldUpdate | 可选，自定义判断值是否变化 | `(prev: T \\| undefined, next: T) => boolean` | `(a, b) => !Object.is(a, b)` |\n"
  },
  {
    "path": "packages/hooks/src/useRafInterval/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { afterAll, beforeAll, describe, expect, test, vi } from 'vitest';\n\nimport useRafInterval from '../index';\n\ninterface ParamsObj {\n  fn: (...arg: any) => any;\n  delay: number | undefined;\n  options?: { immediate: boolean };\n}\n\nconst setUp = ({ fn, delay, options }: ParamsObj) =>\n  renderHook(() => useRafInterval(fn, delay, options));\n\nconst FRAME_TIME = 16;\ndescribe('useRafInterval', () => {\n  beforeAll(() => {\n    vi.useFakeTimers();\n  });\n  afterAll(() => {\n    vi.restoreAllMocks();\n  });\n\n  test('interval should work', () => {\n    const callback = vi.fn();\n    setUp({ fn: callback, delay: FRAME_TIME });\n    expect(callback).not.toBeCalled();\n    vi.advanceTimersByTime(FRAME_TIME * 2.5);\n    expect(callback).toHaveBeenCalledTimes(2);\n  });\n\n  test('delay is undefined should stop', () => {\n    const delay: number | undefined = undefined;\n    const callback = vi.fn();\n    setUp({ fn: callback, delay });\n    expect(callback).not.toBeCalled();\n    vi.advanceTimersByTime(FRAME_TIME * 1.5);\n    expect(callback).not.toBeCalled();\n  });\n\n  test('immediate in options should work', () => {\n    const callback = vi.fn();\n    setUp({ fn: callback, delay: FRAME_TIME, options: { immediate: true } });\n    expect(callback).toBeCalled();\n    expect(callback).toHaveBeenCalledTimes(1);\n    vi.advanceTimersByTime(FRAME_TIME * 1.5);\n    expect(callback).toHaveBeenCalledTimes(2);\n  });\n\n  test('interval should be clear', () => {\n    const callback = vi.fn();\n    const hook = setUp({ fn: callback, delay: FRAME_TIME });\n\n    expect(callback).not.toBeCalled();\n\n    hook.result.current();\n    vi.advanceTimersByTime(FRAME_TIME * 2.5);\n    // not to be called\n    expect(callback).toHaveBeenCalledTimes(0);\n  });\n\n  test('execute clear in the callback and interval should be clear', () => {\n    const callback = vi.fn().mockImplementation(() => hook.result.current());\n    const hook = setUp({ fn: callback, delay: FRAME_TIME });\n\n    expect(callback).not.toBeCalled();\n\n    vi.advanceTimersByTime(FRAME_TIME * 1.5);\n    expect(callback).toHaveBeenCalledTimes(1);\n    vi.advanceTimersByTime(FRAME_TIME * 1.5);\n    expect(callback).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useRafInterval/__tests__/node.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { beforeAll, afterAll, describe, test, expect, vi } from 'vitest';\nimport useRafInterval from '../index';\n\ninterface ParamsObj {\n  fn: (...arg: any) => any;\n  delay: number | undefined;\n  options?: { immediate: boolean };\n}\n\nconst setUp = ({ fn, delay, options }: ParamsObj) =>\n  renderHook(() => useRafInterval(fn, delay, options));\n\nconst FRAME_TIME = 16;\ndescribe('useRafInterval', () => {\n  beforeAll(() => {\n    vi.useFakeTimers();\n  });\n  afterAll(() => {\n    vi.restoreAllMocks();\n  });\n  test('should downgrade to setInterval when requstAnimationFrame is undefined', () => {\n    Object.defineProperty(window, 'cancelAnimationFrame', { value: undefined });\n    Object.defineProperty(window, 'requestAnimationFrame', { value: undefined });\n\n    const callback = vi.fn();\n    setUp({ fn: callback, delay: FRAME_TIME });\n    expect(callback).not.toBeCalled();\n    vi.advanceTimersByTime(FRAME_TIME * 1.5);\n    expect(callback).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useRafInterval/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Execute once per 1000ms.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 每1000ms，执行一次\n */\n\nimport { useState } from 'react';\nimport { useRafInterval } from 'ahooks';\n\nexport default () => {\n  const [count, setCount] = useState(0);\n\n  useRafInterval(() => {\n    setCount(count + 1);\n  }, 1000);\n\n  return <div>count: {count}</div>;\n};\n"
  },
  {
    "path": "packages/hooks/src/useRafInterval/demo/demo2.tsx",
    "content": "/**\n * title: Advanced usage\n * desc: Modify the delay to realize the timer interval change and pause.\n *\n * title.zh-CN: 进阶使用\n * desc.zh-CN: 动态修改 delay 以实现定时器间隔变化与暂停。\n */\n\nimport { useState } from 'react';\nimport { useRafInterval } from 'ahooks';\n\nexport default () => {\n  const [count, setCount] = useState(0);\n  const [interval, setInterval] = useState(1000);\n\n  const clear = useRafInterval(() => {\n    setCount(count + 1);\n  }, interval);\n\n  return (\n    <div>\n      <p> count: {count} </p>\n      <p style={{ marginTop: 16 }}> interval: {interval} </p>\n      <button\n        onClick={() => setInterval((t) => (!!t ? t + 1000 : 1000))}\n        style={{ marginRight: 8 }}\n      >\n        interval + 1000\n      </button>\n      <button\n        style={{ marginRight: 8 }}\n        onClick={() => {\n          setInterval(1000);\n        }}\n      >\n        reset interval\n      </button>\n      <button\n        onClick={() => {\n          clear();\n        }}\n      >\n        clear\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRafInterval/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useRafInterval\n\nA hook implements with `requestAnimationFrame` for better performance. The API is consistent with `useInterval`, the advantage is that the execution of the timer can be stopped when the page is not rendering, such as page hiding or minimization.\n\nPlease note that the following two cases are likely to be inapplicable, and `useInterval` is preferred:\n\n- the time interval is less than `16ms`\n- want to execute the timer when page is not rendering;\n\n> `requestAnimationFrame` will automatically downgrade to `setInterval` in node environment\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Advanced usage\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nuseRafInterval(\n  fn: () => void,\n  delay?: number | undefined,\n  options?: Options\n): fn: () => void;\n```\n\n### Params\n\n| Property | Description                                                                                                                                                   | Type                    |\n| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |\n| fn       | The function to be executed every `delay` milliseconds.                                                                                                       | `() => void`            |\n| delay    | The time in milliseconds, the timer should delay in between executions of the specified function. The timer will be cancelled if delay is set to `undefined`. | `number` \\| `undefined` |\n| options  | Config of the interval behavior.                                                                                                                              | `Options`               |\n\n### Options\n\n| Property  | Description                                                            | Type      | Default |\n| --------- | ---------------------------------------------------------------------- | --------- | ------- |\n| immediate | Whether the function should be executed immediately on first execution | `boolean` | `false` |\n\n### Result\n\n| Property      | Description    | Type         |\n| ------------- | -------------- | ------------ |\n| clearInterval | clear interval | `() => void` |\n"
  },
  {
    "path": "packages/hooks/src/useRafInterval/index.ts",
    "content": "import { useCallback, useEffect, useRef } from 'react';\nimport useLatest from '../useLatest';\nimport { isNumber } from '../utils';\n\ninterface Handle {\n  id: ReturnType<typeof setInterval> | ReturnType<typeof requestAnimationFrame>;\n}\n\nconst setRafInterval = (callback: () => void, delay: number = 0): Handle => {\n  if (typeof requestAnimationFrame === 'undefined') {\n    return {\n      id: setInterval(callback, delay),\n    };\n  }\n  let start = Date.now();\n  const handle: Handle = {\n    id: 0,\n  };\n  const loop = () => {\n    const current = Date.now();\n    handle.id = requestAnimationFrame(loop);\n    if (current - start >= delay) {\n      callback();\n      start = Date.now();\n    }\n  };\n  handle.id = requestAnimationFrame(loop);\n  return handle;\n};\n\nconst cancelAnimationFrameIsNotDefined = (t: any): t is ReturnType<typeof setTimeout> => {\n  return typeof cancelAnimationFrame === 'undefined';\n};\n\nconst clearRafInterval = (handle: Handle) => {\n  if (cancelAnimationFrameIsNotDefined(handle.id)) {\n    return clearInterval(handle.id);\n  }\n  cancelAnimationFrame(handle.id);\n};\n\nfunction useRafInterval(\n  fn: () => void,\n  delay: number | undefined,\n  options?: {\n    immediate?: boolean;\n  },\n) {\n  const immediate = options?.immediate;\n\n  const fnRef = useLatest(fn);\n  const timerRef = useRef<Handle>(undefined);\n\n  const clear = useCallback(() => {\n    if (timerRef.current) {\n      clearRafInterval(timerRef.current);\n    }\n  }, []);\n\n  useEffect(() => {\n    if (!isNumber(delay) || delay < 0) {\n      return;\n    }\n    if (immediate) {\n      fnRef.current();\n    }\n    timerRef.current = setRafInterval(() => {\n      fnRef.current();\n    }, delay);\n    return clear;\n  }, [delay]);\n\n  return clear;\n}\n\nexport default useRafInterval;\n"
  },
  {
    "path": "packages/hooks/src/useRafInterval/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useRafInterval\n\n用 `requestAnimationFrame` 模拟实现 `setInterval`，API 和 `useInterval` 保持一致，好处是可以在页面不渲染的时候停止执行定时器，比如页面隐藏或最小化等。\n\n请注意，如下两种情况下很可能是不适用的，优先考虑 `useInterval` ：\n\n- 时间间隔小于 `16ms`\n- 希望页面不渲染的情况下依然执行定时器\n\n> Node 环境下 `requestAnimationFrame` 会自动降级到 `setInterval`\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 进阶使用\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nuseRafInterval(\n  fn: () => void,\n  delay?: number | undefined,\n  options?: Options\n): fn: () => void;\n```\n\n### Params\n\n| 参数    | 说明                                        | 类型                    |\n| ------- | ------------------------------------------- | ----------------------- |\n| fn      | 要定时调用的函数                            | `() => void`            |\n| delay   | 间隔时间，当取值 `undefined` 时会停止计时器 | `number` \\| `undefined` |\n| options | 配置计时器的行为                            | `Options`               |\n\n### Options\n\n| 参数      | 说明                     | 类型      | 默认值  |\n| --------- | ------------------------ | --------- | ------- |\n| immediate | 是否在首次渲染时立即执行 | `boolean` | `false` |\n\n### Result\n\n| 参数          | 说明       | 类型         |\n| ------------- | ---------- | ------------ |\n| clearInterval | 清除定时器 | `() => void` |\n"
  },
  {
    "path": "packages/hooks/src/useRafState/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\n\nimport useRafState from '../index';\n\ndescribe('useRafState', () => {\n  test('should work', () => {\n    const mockRaf = vi\n      .spyOn(window, 'requestAnimationFrame')\n      .mockImplementation((cb: FrameRequestCallback) => {\n        cb(0);\n        return 0;\n      });\n    const { result } = renderHook(() => useRafState(0));\n    const setRafState = result.current[1];\n    expect(result.current[0]).toBe(0);\n\n    act(() => {\n      setRafState(1);\n    });\n    expect(result.current[0]).toBe(1);\n    mockRaf.mockRestore();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useRafState/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n *\n * title.zh-CN: 基础用法\n */\n\nimport { useRafState } from 'ahooks';\nimport { useEffect } from 'react';\n\nexport default () => {\n  const [state, setState] = useRafState({\n    width: 0,\n    height: 0,\n  });\n\n  useEffect(() => {\n    const onResize = () => {\n      setState({\n        width: document.documentElement.clientWidth,\n        height: document.documentElement.clientHeight,\n      });\n    };\n    onResize();\n\n    window.addEventListener('resize', onResize);\n\n    return () => {\n      window.removeEventListener('resize', onResize);\n    };\n  }, []);\n\n  return (\n    <div>\n      <p>Try to resize the window </p>\n      current: {JSON.stringify(state)}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRafState/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useRafState\n\nUpdate the state in [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) callback, generally used for performance optimization.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### API\n\nSame as `React.useState`.\n"
  },
  {
    "path": "packages/hooks/src/useRafState/index.ts",
    "content": "import { useCallback, useRef, useState } from 'react';\nimport type { Dispatch, SetStateAction } from 'react';\nimport useUnmount from '../useUnmount';\n\nfunction useRafState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];\nfunction useRafState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];\n\nfunction useRafState<S>(initialState?: S | (() => S)) {\n  const ref = useRef(0);\n  const [state, setState] = useState(initialState);\n\n  const setRafState = useCallback((value: S | ((prevState: S) => S)) => {\n    cancelAnimationFrame(ref.current);\n\n    ref.current = requestAnimationFrame(() => {\n      setState(value as any);\n    });\n  }, []);\n\n  useUnmount(() => {\n    cancelAnimationFrame(ref.current);\n  });\n\n  return [state, setRafState] as const;\n}\n\nexport default useRafState;\n"
  },
  {
    "path": "packages/hooks/src/useRafState/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useRafState\n\n只在 [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) callback 时更新 state，一般用于性能优化。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### API\n\n与 `React.useState` 一致\n"
  },
  {
    "path": "packages/hooks/src/useRafTimeout/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { afterAll, beforeAll, describe, expect, test, vi } from 'vitest';\n\nimport useRafTimeout from '../index';\n\ninterface ParamsObj {\n  fn: (...arg: any) => any;\n  delay: number | undefined;\n}\n\nconst setUp = ({ fn, delay }: ParamsObj) => renderHook(() => useRafTimeout(fn, delay));\n\nconst FRAME_TIME = 16.7;\ndescribe('useRafTimeout', () => {\n  beforeAll(() => {\n    vi.useFakeTimers();\n  });\n  afterAll(() => {\n    vi.restoreAllMocks();\n  });\n\n  test('timeout should work', () => {\n    const callback = vi.fn();\n    setUp({ fn: callback, delay: FRAME_TIME });\n    expect(callback).not.toBeCalled();\n    vi.advanceTimersByTime(FRAME_TIME * 2.5);\n    expect(callback).toHaveBeenCalledTimes(1);\n  });\n\n  test('timeout should stop when delay is undefined', () => {\n    const delay: number | undefined = undefined;\n    const callback = vi.fn();\n    setUp({ fn: callback, delay });\n    expect(callback).not.toBeCalled();\n    vi.advanceTimersByTime(FRAME_TIME * 1.5);\n    expect(callback).not.toBeCalled();\n  });\n\n  test('timeout should be clear', () => {\n    const callback = vi.fn();\n\n    const hook = setUp({ fn: callback, delay: FRAME_TIME });\n    expect(callback).not.toBeCalled();\n\n    hook.result.current();\n    vi.advanceTimersByTime(FRAME_TIME * 2.5);\n    expect(callback).toHaveBeenCalledTimes(0);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useRafTimeout/__tests__/node.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { afterAll, beforeAll, describe, expect, test, vi } from 'vitest';\nimport useRafTimeout from '../index';\n\ninterface ParamsObj {\n  fn: (...arg: any) => any;\n  delay: number | undefined;\n}\n\nconst setUp = ({ fn, delay }: ParamsObj) => renderHook(() => useRafTimeout(fn, delay));\n\nconst FRAME_TIME = 16.7;\ndescribe('useRafTimeout', () => {\n  beforeAll(() => {\n    vi.useFakeTimers();\n  });\n  afterAll(() => {\n    vi.restoreAllMocks();\n  });\n  test('should downgrade to setTimeout when requstAnimationFrame is undefined', () => {\n    const _requestAnimationFrame = global.requestAnimationFrame;\n    const _cancelAnimationFrame = global.cancelAnimationFrame;\n\n    // @ts-ignore\n    delete global.requestAnimationFrame;\n    // @ts-ignore\n    delete global.cancelAnimationFrame;\n\n    const callback = vi.fn();\n    setUp({ fn: callback, delay: FRAME_TIME });\n    expect(callback).not.toBeCalled();\n    vi.advanceTimersByTime(FRAME_TIME * 1.5);\n    expect(callback).toHaveBeenCalledTimes(1);\n\n    global.requestAnimationFrame = _requestAnimationFrame;\n    global.cancelAnimationFrame = _cancelAnimationFrame;\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useRafTimeout/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Execute after 2000ms.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 在 2000ms 后执行。\n */\n\nimport { useState } from 'react';\nimport { useRafTimeout } from 'ahooks';\n\nexport default () => {\n  const [count, setCount] = useState(0);\n\n  useRafTimeout(() => {\n    setCount(count + 1);\n  }, 2000);\n\n  return <div>count: {count}</div>;\n};\n"
  },
  {
    "path": "packages/hooks/src/useRafTimeout/demo/demo2.tsx",
    "content": "/**\n * title: Advanced usage\n * desc: Modify the delay to realize the timer timeout change and pause.\n *\n * title.zh-CN: 进阶使用\n * desc.zh-CN: 动态修改 delay 以实现定时器间隔变化与暂停。\n */\n\nimport { useState } from 'react';\nimport { useRafTimeout } from 'ahooks';\n\nexport default () => {\n  const [count, setCount] = useState(0);\n  const [delay, setDelay] = useState<number | undefined>(1000);\n\n  const clear = useRafTimeout(() => {\n    setCount(count + 1);\n  }, delay);\n\n  return (\n    <div>\n      <p> count: {count} </p>\n      <p style={{ marginTop: 16 }}> Delay: {delay} </p>\n      <button onClick={() => setDelay((t) => (!!t ? t + 1000 : 1000))} style={{ marginRight: 8 }}>\n        Delay + 1000\n      </button>\n      <button\n        style={{ marginRight: 8 }}\n        onClick={() => {\n          setDelay(1000);\n        }}\n      >\n        reset Delay\n      </button>\n      <button\n        onClick={() => {\n          clear();\n        }}\n      >\n        clear\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRafTimeout/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useRafTimeout\n\nA hook implements with `requestAnimationFrame` for better performance. The API is consistent with `useTimeout`. the advantage is that will not trigger function when the page is not rendering, such as page hiding or minimization.\n\n> `requestAnimationFrame` will automatically downgrade to `setTimeout` in node environment\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Advanced usage\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nuseRafTimeout(\n  fn: () => void,\n  delay?: number | undefined,\n): fn: () => void;\n```\n\n### Params\n\n| Property | Description                                                                                                            | Type                    |\n| -------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------- |\n| fn       | The function to be executed after `delay` milliseconds.                                                                | `() => void`            |\n| delay    | The number of milliseconds to wait before executing the function. The timer will be cancelled if delay is `undefined`. | `number` \\| `undefined` |\n\n### Result\n\n| Property     | Description   | Type         |\n| ------------ | ------------- | ------------ |\n| clearTimeout | clear timeout | `() => void` |\n"
  },
  {
    "path": "packages/hooks/src/useRafTimeout/index.ts",
    "content": "import { useCallback, useEffect, useRef } from 'react';\nimport useLatest from '../useLatest';\nimport { isNumber } from '../utils';\n\ninterface Handle {\n  id: ReturnType<typeof setTimeout> | ReturnType<typeof requestAnimationFrame>;\n}\n\nconst setRafTimeout = (callback: () => void, delay: number = 0): Handle => {\n  if (typeof requestAnimationFrame === 'undefined') {\n    return {\n      id: setTimeout(callback, delay),\n    };\n  }\n\n  const handle: Handle = {\n    id: 0,\n  };\n\n  const startTime = Date.now();\n\n  const loop = () => {\n    const current = Date.now();\n    if (current - startTime >= delay) {\n      callback();\n    } else {\n      handle.id = requestAnimationFrame(loop);\n    }\n  };\n  handle.id = requestAnimationFrame(loop);\n  return handle;\n};\n\nconst cancelAnimationFrameIsNotDefined = (t: any): t is ReturnType<typeof setTimeout> => {\n  return typeof cancelAnimationFrame === 'undefined';\n};\n\nconst clearRafTimeout = (handle: Handle) => {\n  if (cancelAnimationFrameIsNotDefined(handle.id)) {\n    return clearTimeout(handle.id);\n  }\n  cancelAnimationFrame(handle.id);\n};\n\nfunction useRafTimeout(fn: () => void, delay: number | undefined) {\n  const fnRef = useLatest(fn);\n  const timerRef = useRef<Handle>(undefined);\n\n  const clear = useCallback(() => {\n    if (timerRef.current) {\n      clearRafTimeout(timerRef.current);\n    }\n  }, []);\n\n  useEffect(() => {\n    if (!isNumber(delay) || delay < 0) {\n      return;\n    }\n    timerRef.current = setRafTimeout(() => {\n      fnRef.current();\n    }, delay);\n    return clear;\n  }, [delay]);\n\n  return clear;\n}\n\nexport default useRafTimeout;\n"
  },
  {
    "path": "packages/hooks/src/useRafTimeout/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useRafTimeout\n\n用 `requestAnimationFrame` 模拟实现 `setTimeout`，API 和 `useTimeout` 保持一致，好处是可以在页面不渲染的时候不触发函数执行，比如页面隐藏或最小化等。\n\n> Node 环境下 `requestAnimationFrame` 会自动降级到 `setTimeout`\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 进阶使用\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nuseRafTimeout(\n  fn: () => void,\n  delay?: number | undefined,\n): fn: () => void;\n```\n\n### Params\n\n| 参数  | 说明                                                                       | 类型                    |\n| ----- | -------------------------------------------------------------------------- | ----------------------- |\n| fn    | 待执行函数                                                                 | `() => void`            |\n| delay | 定时时间（单位为毫秒）,支持动态变化，，当取值为 `undefined` 时会停止计时器 | `number` \\| `undefined` |\n\n### Result\n\n| 参数         | 说明       | 类型         |\n| ------------ | ---------- | ------------ |\n| clearTimeout | 清除定时器 | `() => void` |\n"
  },
  {
    "path": "packages/hooks/src/useReactive/__tests__/index.spec.tsx",
    "content": "import { act, fireEvent, render, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useReactive from '../';\n\nconst Demo = () => {\n  const state: {\n    count: number;\n    val: any;\n    foo?: string;\n    arr: number[];\n  } = useReactive({\n    count: 0,\n    val: {\n      val1: {\n        val2: '',\n      },\n    },\n    arr: [1],\n    foo: 'foo',\n  });\n\n  return (\n    <div>\n      <p>\n        counter state.count:<span role=\"addCount\">{state.count}</span>\n      </p>\n      <p>\n        delete property:<span role=\"deleteProperty\">{state.foo}</span>\n      </p>\n\n      <button role=\"addCountBtn\" onClick={() => (state.count += 1)}>\n        state.count++\n      </button>\n      <button role=\"deletePropertyBtn\" onClick={() => delete state.foo}>\n        delete state.foo\n      </button>\n      <button role=\"subCountBtn\" style={{ marginLeft: '50px' }} onClick={() => (state.count -= 1)}>\n        state.count--\n      </button>\n      <br />\n      <br />\n      <p>\n        state.arr: <span role=\"test-array\">{JSON.stringify(state.arr)}</span>\n      </p>\n      <button style={{ marginRight: '10px' }} onClick={() => state.arr.push(1)} role=\"pushbtn\">\n        push\n      </button>\n      <button style={{ marginRight: '10px' }} onClick={() => state.arr.pop()} role=\"popbtn\">\n        pop\n      </button>\n      <button style={{ marginRight: '10px' }} onClick={() => state.arr.shift()} role=\"shiftbtn\">\n        shift\n      </button>\n      <button\n        style={{ marginRight: '10px' }}\n        role=\"unshiftbtn\"\n        onClick={() => state.arr.unshift(2)}\n      >\n        unshift\n      </button>\n      <button style={{ marginRight: '10px' }} role=\"reverse\" onClick={() => state.arr.reverse()}>\n        reverse\n      </button>\n      <button style={{ marginRight: '10px' }} role=\"sort\" onClick={() => state.arr.sort()}>\n        sort\n      </button>\n      <br />\n      <br />\n      <p>nested structure</p>\n      <p role=\"inputVal1\">{state.val.val1.val2}</p>\n      <input\n        role=\"input1\"\n        style={{ width: 220, borderWidth: 1 }}\n        type=\"text\"\n        onChange={(e) => {\n          state.val.val1.val2 = e.target.value;\n        }}\n      />\n    </div>\n  );\n};\n\ndescribe('test useReactive feature', () => {\n  test('test count', () => {\n    const wrap = render(<Demo />);\n\n    const count = wrap.getByRole('addCount');\n    const addCountBtn = wrap.getByRole('addCountBtn');\n    const subCountBtn = wrap.getByRole('subCountBtn');\n\n    act(() => {\n      fireEvent.click(addCountBtn);\n    });\n    expect(count.textContent).toBe('1');\n\n    act(() => {\n      fireEvent.click(addCountBtn);\n      fireEvent.click(addCountBtn);\n    });\n    expect(count.textContent).toBe('3');\n\n    act(() => {\n      fireEvent.click(subCountBtn);\n    });\n    expect(count.textContent).toBe('2');\n\n    act(() => {\n      fireEvent.click(subCountBtn);\n      fireEvent.click(subCountBtn);\n      fireEvent.click(subCountBtn);\n      fireEvent.click(subCountBtn);\n      fireEvent.click(subCountBtn);\n    });\n    expect(count.textContent).toBe('-3');\n  });\n\n  test('test array', () => {\n    const wrap = render(<Demo />);\n    const testArray = wrap.getAllByRole('test-array')[0];\n    const pushbtn = wrap.getAllByRole('pushbtn')[0];\n    const popbtn = wrap.getAllByRole('popbtn')[0];\n    const shiftbtn = wrap.getAllByRole('shiftbtn')[0];\n    const unshiftbtn = wrap.getAllByRole('unshiftbtn')[0];\n    act(() => {\n      fireEvent.click(pushbtn);\n    });\n    expect(JSON.parse(testArray.textContent as any).length).toBe(2);\n    act(() => {\n      fireEvent.click(popbtn);\n    });\n    expect(JSON.parse(testArray.textContent as any).length).toBe(1);\n    act(() => {\n      fireEvent.click(unshiftbtn);\n    });\n    expect(JSON.parse(testArray.textContent as any).length).toBe(2);\n    act(() => {\n      fireEvent.click(shiftbtn);\n    });\n    expect(JSON.parse(testArray.textContent as any).length).toBe(1);\n  });\n\n  test('test special objects', () => {\n    const { result } = renderHook(() => {\n      // Almost all of the built-in objects are tested.\n      // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects\n      return useReactive({\n        a: new Function('return 1;'),\n        b: new Boolean(true),\n        c: Symbol.for('a'),\n        d: new Error('a'),\n        e: new Number(1),\n        f: BigInt(1),\n        g: Math,\n        h: new Date(),\n        i: new String('a'),\n        j1: new RegExp(/a/),\n        j2: /a/,\n        k: new Array(1),\n        l: new Map(),\n        m: new Set(),\n        n: new ArrayBuffer(1),\n        o: new DataView(new ArrayBuffer(1)),\n        p: Atomics,\n        q: JSON,\n        r: new Promise((resolve) => resolve(1)),\n        s: Reflect,\n        t: new Proxy({}, {}),\n        u: Intl,\n        v: WebAssembly,\n      });\n    });\n\n    expect(() => result.current.a.name).not.toThrowError();\n    expect(() => result.current.b.valueOf()).not.toThrowError();\n    expect(() => result.current.c.valueOf()).not.toThrowError();\n    expect(() => result.current.d.message).not.toThrowError();\n    expect(() => result.current.e.valueOf()).not.toThrowError();\n    expect(() => result.current.f.valueOf()).not.toThrowError();\n    expect(() => result.current.g.PI).not.toThrowError();\n    expect(() => result.current.h.getFullYear()).not.toThrowError();\n    expect(() => result.current.i.valueOf()).not.toThrowError();\n    expect(() => result.current.j1.test('a')).not.toThrowError();\n    expect(() => result.current.j2.test('a')).not.toThrowError();\n    expect(() => result.current.k.length).not.toThrowError();\n    expect(() => result.current.l.size).not.toThrowError();\n    expect(() => result.current.m.size).not.toThrowError();\n    expect(() => result.current.n.byteLength).not.toThrowError();\n    expect(() => result.current.o.byteLength).not.toThrowError();\n    expect(() => result.current.p.isLockFree(1)).not.toThrowError();\n    expect(() => result.current.q.stringify(1)).not.toThrowError();\n    expect(() => result.current.r.then()).not.toThrowError();\n    expect(() => result.current.s.ownKeys({})).not.toThrowError();\n    expect(() => result.current.t.toString()).not.toThrowError();\n    expect(() => result.current.u.DateTimeFormat()).not.toThrowError();\n    expect(() => result.current.v.Module).not.toThrowError();\n  });\n\n  test('test JSX element', () => {\n    const hook = renderHook(() => useReactive({ html: <div role=\"id\">foo</div> }));\n    const proxy = hook.result.current;\n    const wrap = render(proxy.html);\n    const html = wrap.getByRole('id');\n\n    expect(html.textContent).toBe('foo');\n    act(() => {\n      proxy.html = <div role=\"id\">bar</div>;\n      wrap.rerender(proxy.html);\n    });\n    expect(html.textContent).toBe('bar');\n    hook.unmount();\n  });\n\n  test('test read-only and non-configurable data property', () => {\n    const obj = {} as { user: { name: string } };\n    Reflect.defineProperty(obj, 'user', {\n      value: { name: 'foo' },\n      writable: false,\n      configurable: false,\n    });\n\n    const hook = renderHook(() => useReactive(obj));\n    const proxy = hook.result.current;\n\n    expect(() => proxy.user.name).not.toThrowError();\n    hook.unmount();\n  });\n\n  test('test input1', () => {\n    const wrap = render(<Demo />);\n\n    const input = wrap.getAllByRole('input1')[0];\n    const inputVal = wrap.getAllByRole('inputVal1')[0];\n    act(() => {\n      fireEvent.change(input, { target: { value: 'a' } });\n    });\n    expect(inputVal.textContent).toBe('a');\n\n    act(() => {\n      fireEvent.change(input, { target: { value: 'bbb' } });\n    });\n    expect(inputVal.textContent).toBe('bbb');\n  });\n\n  test('delete object property', () => {\n    const wrap = render(<Demo />);\n\n    const deleteProperty = wrap.getAllByRole('deleteProperty')[0];\n    const deletePropertyBtn = wrap.getAllByRole('deletePropertyBtn')[0];\n    expect(deleteProperty.textContent).toBe('foo');\n\n    act(() => {\n      fireEvent.click(deletePropertyBtn);\n    });\n    expect(deleteProperty.textContent).toBe('');\n  });\n\n  test('access from self to prototype chain', () => {\n    const parent: Record<string, string> = {\n      name: 'parent',\n      get value() {\n        return this.name;\n      },\n    };\n\n    const child: Record<string, string> = {\n      name: 'child',\n    };\n\n    const { result } = renderHook(() => useReactive(parent));\n    const proxy = result.current;\n\n    Object.setPrototypeOf(child, proxy);\n\n    expect(child.value).toBe('child');\n    expect(proxy.value).toBe('parent');\n    expect(parent.value).toBe('parent');\n\n    act(() => delete child.name);\n    expect(child.value).toBe('parent');\n    expect(proxy.value).toBe('parent');\n    expect(parent.value).toBe('parent');\n\n    act(() => delete proxy.name);\n    expect(child.value).toBeUndefined();\n    expect(proxy.value).toBeUndefined();\n    expect(parent.value).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useReactive/demo/demo1.tsx",
    "content": "import { useReactive } from 'ahooks';\n\nexport default () => {\n  const state = useReactive({\n    count: 0,\n    inputVal: '',\n    obj: {\n      value: '',\n    },\n  });\n\n  return (\n    <div>\n      <p> state.count：{state.count}</p>\n\n      <button style={{ marginRight: 8 }} onClick={() => state.count++}>\n        state.count++\n      </button>\n      <button onClick={() => state.count--}>state.count--</button>\n\n      <p style={{ marginTop: 20 }}> state.inputVal: {state.inputVal}</p>\n      <input onChange={(e) => (state.inputVal = e.target.value)} />\n\n      <p style={{ marginTop: 20 }}> state.obj.value: {state.obj.value}</p>\n      <input onChange={(e) => (state.obj.value = e.target.value)} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useReactive/demo/demo2.tsx",
    "content": "import { useReactive } from 'ahooks';\n\nexport default () => {\n  const state = useReactive<{ arr: number[] }>({\n    arr: [],\n  });\n\n  return (\n    <div>\n      <p>\n        state.arr: <span role=\"test-array\">{JSON.stringify(state.arr)}</span>\n      </p>\n      <button\n        style={{ marginRight: '10px' }}\n        onClick={() => state.arr.push(Math.floor(Math.random() * 100))}\n        role=\"pushbtn\"\n      >\n        push\n      </button>\n      <button style={{ marginRight: '10px' }} onClick={() => state.arr.pop()} role=\"popbtn\">\n        pop\n      </button>\n      <button style={{ marginRight: '10px' }} onClick={() => state.arr.shift()} role=\"shiftbtn\">\n        shift\n      </button>\n      <button\n        style={{ marginRight: '10px' }}\n        role=\"unshiftbtn\"\n        onClick={() => state.arr.unshift(Math.floor(Math.random() * 100))}\n      >\n        unshift\n      </button>\n      <button style={{ marginRight: '10px' }} role=\"reverse\" onClick={() => state.arr.reverse()}>\n        reverse\n      </button>\n      <button style={{ marginRight: '10px' }} role=\"sort\" onClick={() => state.arr.sort()}>\n        sort\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useReactive/demo/demo3.tsx",
    "content": "import { useReactive } from 'ahooks';\n\nexport default () => {\n  const state = useReactive({\n    bug: '',\n    bugs: ['feat', 'fix', 'chore'],\n    addBug(bug: any) {\n      this.bugs.push(bug);\n    },\n    get bugsCount() {\n      return this.bugs.length;\n    },\n  });\n\n  return (\n    <div>\n      <p>state.bugsCount: {state.bugsCount}</p>\n\n      <form\n        onSubmit={(e) => {\n          state.addBug(state.bug);\n          state.bug = '';\n          e.preventDefault();\n        }}\n      >\n        <input type=\"text\" value={state.bug} onChange={(e) => (state.bug = e.target.value)} />\n        <button type=\"submit\" style={{ marginLeft: '10px' }}>\n          Add\n        </button>\n        <button type=\"button\" style={{ marginLeft: '10px' }} onClick={() => state.bugs.pop()}>\n          Delete\n        </button>\n      </form>\n\n      <br />\n\n      <ul>\n        {state.bugs.map((bug) => (\n          <li key={bug}>{bug}</li>\n        ))}\n      </ul>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useReactive/demo/demo4.tsx",
    "content": "/**\n * desc: useReactive returns a proxy object which always has the same reference. If `useEffect`, `useMemo`, `useCallback` and props passed to child component rely on the proxy, none of the above will be invoked by any changes to the proxy.\n *\n * desc.zh-CN: useReactive 产生可操作的代理对象一直都是同一个引用，`useEffect` , `useMemo` ,`useCallback` ,`子组件属性传递` 等如果依赖的是这个代理对象是**不会**引起重新执行。\n */\n\nimport { useEffect, useState } from 'react';\nimport { useReactive } from 'ahooks';\n\nexport default () => {\n  const state = useReactive({ count: 0 });\n  const [stateCount, setStateCount] = useState(0);\n\n  const state2 = useReactive({ count: 0 });\n  const [stateCount2, setStateCount2] = useState(0);\n\n  // Depends on the object, because it is always the same reference, it will not be executed\n  useEffect(() => {\n    setStateCount(stateCount + 1);\n  }, [state]);\n\n  // Depends on the underlying data type, so as long as it changes, it will be re-executed\n  useEffect(() => {\n    setStateCount2(stateCount2 + 1);\n  }, [state2.count]);\n\n  return (\n    <div>\n      <button style={{ marginTop: 20 }} onClick={() => (state.count += 1)}>\n        stateCount + 1\n      </button>\n      <p>stateCount:{stateCount}</p>\n\n      <button style={{ marginTop: 20 }} onClick={() => (state2.count += 1)}>\n        stateCount2 + 1\n      </button>\n      <p>stateCount2:{stateCount2}</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useReactive/demo/index.tsx",
    "content": "import { useReactive } from 'ahooks';\n\nexport default () => {\n  const state = useReactive({\n    count: 0,\n    val: {\n      val1: {\n        val2: '',\n      },\n    },\n    arr: [1],\n  });\n\n  return (\n    <div>\n      <p>\n        counter state.count：<span role=\"addCount\">{state.count}</span>\n      </p>\n\n      <button role=\"addCountBtn\" onClick={() => (state.count += 1)}>\n        state.count++\n      </button>\n      <button role=\"subCountBtn\" style={{ marginLeft: '50px' }} onClick={() => (state.count -= 1)}>\n        state.count--\n      </button>\n      <br />\n      <br />\n      <p>\n        state.arr: <span role=\"test-array\">{JSON.stringify(state.arr)}</span>\n      </p>\n      <button style={{ marginRight: '10px' }} onClick={() => state.arr.push(1)} role=\"pushbtn\">\n        push\n      </button>\n      <button style={{ marginRight: '10px' }} onClick={() => state.arr.pop()} role=\"popbtn\">\n        pop\n      </button>\n      <button style={{ marginRight: '10px' }} onClick={() => state.arr.shift()} role=\"shiftbtn\">\n        shift\n      </button>\n      <button\n        style={{ marginRight: '10px' }}\n        role=\"unshiftbtn\"\n        onClick={() => state.arr.unshift(2)}\n      >\n        unshift\n      </button>\n      <button style={{ marginRight: '10px' }} role=\"reverse\" onClick={() => state.arr.reverse()}>\n        reverse\n      </button>\n      <button style={{ marginRight: '10px' }} role=\"sort\" onClick={() => state.arr.sort()}>\n        sort\n      </button>\n      <br />\n      <br />\n      <p>nested structure</p>\n      <p role=\"inputVal1\">{state.val.val1.val2}</p>\n      <input\n        role=\"input1\"\n        style={{ width: 220, borderWidth: 1 }}\n        type=\"text\"\n        onChange={(e) => {\n          state.val.val1.val2 = e.target.value;\n        }}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useReactive/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n## useReactive\n\nIt offers data reactivity when manipulating states and views, in which case `useState` is unnecessary for state definition. Modifying properties will automatically lead to view rerendering.\n\n## Examples\n\n### Default Usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Array\n\n<code src=\"./demo/demo2.tsx\" />\n\n### Computed Properties\n\n<code src=\"./demo/demo3.tsx\" />\n\n### Notice\n\n<code  src=\"./demo/demo4.tsx\" />\n\n## API\n\n```js\nconst state = useReactive(initialValue: Record<string, any>);\n```\n\n## Params\n\n| Params       | Description   | Type                  | Default |\n| ------------ | ------------- | --------------------- | ------- |\n| initialState | Current state | `Record<string, any>` | -       |\n\n## FAQ\n\n### When `useReactive` is used with `Map`, `Set`, it will throw an error or not work?\n\n`useReactive` is not compatible with `Map`, `Set`。\n\nRelated issues: [#2239](https://github.com/alibaba/hooks/discussions/2239)\n"
  },
  {
    "path": "packages/hooks/src/useReactive/index.ts",
    "content": "import { useRef } from 'react';\nimport isPlainObject from 'lodash/isPlainObject';\nimport useCreation from '../useCreation';\nimport useUpdate from '../useUpdate';\n\n// k:v 原对象:代理过的对象\nconst proxyMap = new WeakMap();\n// k:v 代理过的对象:原对象\nconst rawMap = new WeakMap();\n\nfunction observer<T extends Record<string, any>>(initialVal: T, cb: () => void): T {\n  const existingProxy = proxyMap.get(initialVal);\n\n  // 添加缓存 防止重新构建proxy\n  if (existingProxy) {\n    return existingProxy;\n  }\n\n  // 防止代理已经代理过的对象\n  // https://github.com/alibaba/hooks/issues/839\n  if (rawMap.has(initialVal)) {\n    return initialVal;\n  }\n\n  const proxy = new Proxy<T>(initialVal, {\n    get(target, key, receiver) {\n      const res = Reflect.get(target, key, receiver);\n\n      // https://github.com/alibaba/hooks/issues/1317\n      const descriptor = Reflect.getOwnPropertyDescriptor(target, key);\n      if (!descriptor?.configurable && !descriptor?.writable) {\n        return res;\n      }\n\n      // Only proxy plain object or array,\n      // otherwise it will cause: https://github.com/alibaba/hooks/issues/2080\n      return isPlainObject(res) || Array.isArray(res) ? observer(res, cb) : res;\n    },\n    set(target, key, val) {\n      const ret = Reflect.set(target, key, val);\n      cb();\n      return ret;\n    },\n    deleteProperty(target, key) {\n      const ret = Reflect.deleteProperty(target, key);\n      cb();\n      return ret;\n    },\n  });\n\n  proxyMap.set(initialVal, proxy);\n  rawMap.set(proxy, initialVal);\n\n  return proxy;\n}\n\nfunction useReactive<S extends Record<string, any>>(initialState: S): S {\n  const update = useUpdate();\n  const stateRef = useRef<S>(initialState);\n\n  const state = useCreation(() => {\n    return observer(stateRef.current, () => {\n      update();\n    });\n  }, []);\n\n  return state;\n}\n\nexport default useReactive;\n"
  },
  {
    "path": "packages/hooks/src/useReactive/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n## useReactive\n\n提供一种数据响应式的操作体验，定义数据状态不需要写`useState`，直接修改属性即可刷新视图。\n\n## 代码演示\n\n### 基本用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 数组操作\n\n<code src=\"./demo/demo2.tsx\" />\n\n### 计算属性\n\n<code src=\"./demo/demo3.tsx\" />\n\n### 注意\n\n<code src=\"./demo/demo4.tsx\" />\n\n## API\n\n```js\nconst state = useReactive(initialState: Record<string, any>);\n```\n\n## 参数\n\n| 参数         | 说明           | 类型                  | 默认值 |\n| ------------ | -------------- | --------------------- | ------ |\n| initialState | 当前的数据对象 | `Record<string, any>` | -      |\n\n## FAQ\n\n### `useReactive` 和 `Map`、`Set` 一起使用时报错或无效？\n\n`useReactive` 目前不兼容 `Map`、`Set`。\n\n相关 issues：[#2239](https://github.com/alibaba/hooks/discussions/2239)\n"
  },
  {
    "path": "packages/hooks/src/useRequest/__tests__/index.spec.ts",
    "content": "import { act, type RenderHookResult, renderHook } from '@testing-library/react';\nimport { afterAll, beforeAll, describe, expect, test, vi } from 'vitest';\nimport { request } from '../../utils/testingHelpers';\nimport useRequest from '../index';\n\nconst errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});\n\ndescribe('useRequest', () => {\n  beforeAll(() => {\n    vi.useFakeTimers();\n  });\n\n  afterAll(() => {\n    errorSpy.mockRestore();\n  });\n\n  const setUp = <TData = string, TParams extends any[] = any[]>(\n    service: (...args: TParams) => Promise<TData>,\n    options?: Parameters<typeof useRequest<TData, TParams>>[1],\n  ) => renderHook((o) => useRequest<TData, TParams>(service, o || options));\n\n  let hook: RenderHookResult<any, any>;\n\n  test('useRequest should auto run', async () => {\n    let value = '';\n    let success: string | undefined;\n    const successCallback = (data: string) => {\n      success = data;\n    };\n    const errorCallback = vi.fn();\n    const beforeCallback = () => {\n      value = 'before';\n    };\n    const finallyCallback = () => {\n      value = 'finally';\n    };\n    // auto run success\n    act(() => {\n      hook = setUp(request, {\n        onSuccess: successCallback,\n        onError: errorCallback,\n        onBefore: beforeCallback,\n        onFinally: finallyCallback,\n      });\n    });\n    expect(hook.result.current.loading).toBe(true);\n    expect(value).toBe('before');\n    expect(success).toBeUndefined();\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n    expect(success).toBe('success');\n    expect(hook.result.current.data).toBe('success');\n    expect(value).toBe('finally');\n    expect(errorCallback).toHaveBeenCalledTimes(0);\n\n    //manual run fail\n    act(() => {\n      hook.result.current.run(0);\n    });\n    expect(hook.result.current.loading).toBe(true);\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.error).toEqual(new Error('fail'));\n    expect(hook.result.current.loading).toBe(false);\n    expect(errorCallback).toHaveBeenCalledTimes(1);\n\n    //manual run success\n    act(() => {\n      hook.result.current.run(1);\n    });\n    expect(hook.result.current.loading).toBe(true);\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.data).toBe('success');\n    expect(hook.result.current.loading).toBe(false);\n    expect(errorCallback).toHaveBeenCalledTimes(1);\n    hook.unmount();\n\n    //auto run fail\n    act(() => {\n      hook = setUp(() => request(0), {\n        onSuccess: successCallback,\n        onError: errorCallback,\n      });\n    });\n    expect(hook.result.current.loading).toBe(true);\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.error).toEqual(new Error('fail'));\n    expect(hook.result.current.loading).toBe(false);\n    expect(errorCallback).toHaveBeenCalledTimes(2);\n    hook.unmount();\n  });\n\n  test('useRequest should be manually triggered', async () => {\n    act(() => {\n      hook = setUp(request, {\n        manual: true,\n      });\n    });\n    expect(hook.result.current.loading).toBe(false);\n    act(() => {\n      hook.result.current.run(1);\n    });\n    expect(hook.result.current.loading).toBe(true);\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n    expect(hook.result.current.data).toBe('success');\n    act(() => {\n      hook.result.current.run(0);\n    });\n    expect(hook.result.current.loading).toBe(true);\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n    expect(hook.result.current.error).toEqual(new Error('fail'));\n    hook.unmount();\n  });\n\n  test('useRequest runAsync should work', async () => {\n    let success = '',\n      error = '';\n\n    act(() => {\n      hook = setUp(request, {\n        manual: true,\n      });\n    });\n    act(() => {\n      hook.result.current\n        .runAsync(0)\n        .then((res: any) => {\n          success = res;\n        })\n        .catch((err: any) => {\n          error = err;\n        });\n    });\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(success).toBe('');\n    expect(error).toEqual(new Error('fail'));\n    success = '';\n    error = '';\n    act(() => {\n      hook.result.current\n        .runAsync(1)\n        .then((res: any) => {\n          success = res;\n        })\n        .catch((err: any) => {\n          error = err;\n        });\n    });\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(success).toBe('success');\n    expect(error).toBe('');\n    hook.unmount();\n  });\n\n  test('useRequest mutate should work', async () => {\n    act(() => {\n      hook = setUp(request, {});\n    });\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.data).toBe('success');\n    act(() => {\n      hook.result.current.mutate('hello');\n    });\n    expect(hook.result.current.data).toBe('hello');\n    hook.unmount();\n  });\n\n  test('runAsync should resolve immediately when ready=false', async () => {\n    // manual = true\n    act(() => {\n      hook = setUp(request, {\n        manual: true,\n        ready: false,\n      });\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    let resolved = false;\n    let value: any = 'init';\n\n    await act(async () => {\n      hook.result.current.runAsync(1).then((res: any) => {\n        resolved = true;\n        value = res;\n      });\n      await Promise.resolve();\n    });\n\n    expect(resolved).toBe(true);\n    expect(value).toBeUndefined();\n    expect(hook.result.current.loading).toBe(false);\n    hook.unmount();\n\n    // manual = false\n    act(() => {\n      hook = setUp(request, {\n        ready: false,\n      });\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    resolved = false;\n    value = 'init';\n\n    await act(async () => {\n      hook.result.current.runAsync(1).then((res: any) => {\n        resolved = true;\n        value = res;\n      });\n      await Promise.resolve();\n    });\n\n    expect(resolved).toBe(true);\n    expect(value).toBeUndefined();\n    expect(hook.result.current.loading).toBe(false);\n    hook.unmount();\n  });\n\n  test('useRequest defaultParams should work', async () => {\n    act(() => {\n      hook = setUp<string, [number, number, number]>(request, {\n        defaultParams: [1, 2, 3],\n      });\n    });\n    expect(hook.result.current.loading).toBe(true);\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.params).toEqual([1, 2, 3]);\n    expect(hook.result.current.data).toBe('success');\n    expect(hook.result.current.loading).toBe(false);\n    hook.unmount();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useRequest/__tests__/useAutoRunPlugin.spec.ts",
    "content": "import { act, type RenderHookResult, renderHook } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\nimport { request } from '../../utils/testingHelpers';\nimport useRequest from '../index';\n\ndescribe('useAutoRunPlugin', () => {\n  vi.useFakeTimers();\n\n  const setUp = <TData = string, TParams extends any[] = any[]>(\n    service: (...args: TParams) => Promise<TData>,\n    options?: Parameters<typeof useRequest<TData, TParams>>[1],\n  ) => renderHook((o) => useRequest<TData, TParams>(service, o || options));\n\n  let hook: RenderHookResult<any, any>;\n\n  test('useAutoRunPlugin ready should work', async () => {\n    let dep = 1;\n    act(() => {\n      hook = setUp(request, {\n        refreshDeps: [dep],\n      });\n    });\n    expect(hook.result.current.loading).toBe(true);\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    dep = 2;\n    hook.rerender({\n      refreshDeps: [dep],\n    });\n    expect(hook.result.current.loading).toBe(true);\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    hook.rerender({\n      refreshDeps: [dep],\n    });\n    expect(hook.result.current.loading).toBe(false);\n  });\n\n  test('useAutoRunPlugin manual=false ready=true work fine', async () => {\n    act(() => {\n      hook = setUp(request, {\n        ready: true,\n      });\n    });\n\n    expect(hook.result.current.loading).toBe(true);\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    hook.rerender({\n      ready: false,\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    hook.rerender({\n      ready: true,\n    });\n\n    expect(hook.result.current.loading).toBe(true);\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n  });\n\n  test('useAutoRunPlugin manual=false ready=false work fine', async () => {\n    act(() => {\n      hook = setUp(request, {\n        ready: false,\n      });\n    });\n\n    expect(hook.result.current.loading).toBe(false);\n\n    hook.rerender({\n      ready: true,\n    });\n\n    expect(hook.result.current.loading).toBe(true);\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    hook.rerender({\n      ready: false,\n    });\n    expect(hook.result.current.loading).toBe(false);\n  });\n\n  test('useAutoRunPlugin manual=false ready&defaultParams work fine', async () => {\n    act(() => {\n      hook = setUp<string, [number]>(request, {\n        ready: false,\n        defaultParams: [1],\n      });\n    });\n\n    expect(hook.result.current.loading).toBe(false);\n\n    hook.rerender({\n      ready: true,\n      defaultParams: [2],\n    });\n\n    expect(hook.result.current.loading).toBe(true);\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n    expect(hook.result.current.params).toEqual([2]);\n\n    hook.rerender({\n      ready: false,\n      defaultParams: [2],\n    });\n    hook.rerender({\n      ready: true,\n      defaultParams: [3],\n    });\n\n    expect(hook.result.current.loading).toBe(true);\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n    expect(hook.result.current.params).toEqual([3]);\n  });\n\n  test('useAutoRunPlugin manual=true ready work fine', async () => {\n    act(() => {\n      hook = setUp(request, {\n        ready: false,\n        manual: true,\n      });\n    });\n    expect(hook.result.current.loading).toBe(false);\n    act(() => {\n      hook.result.current.run();\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    hook.rerender({\n      ready: true,\n      manual: true,\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    act(() => {\n      hook.result.current.run();\n    });\n    expect(hook.result.current.loading).toBe(true);\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n  });\n\n  test('useAutoRunPlugin manual=false refreshDeps should work', async () => {\n    let dep = 1;\n    act(() => {\n      hook = setUp(request, {\n        refreshDeps: [dep],\n      });\n    });\n    expect(hook.result.current.loading).toBe(true);\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    dep = 2;\n    hook.rerender({\n      refreshDeps: [dep],\n    });\n    expect(hook.result.current.loading).toBe(true);\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    hook.rerender({\n      refreshDeps: [dep],\n    });\n    expect(hook.result.current.loading).toBe(false);\n  });\n\n  test('useAutoRunPlugin manual=true refreshDeps should work', async () => {\n    let dep = 1;\n    act(() => {\n      hook = setUp(request, {\n        manual: true,\n        refreshDeps: [dep],\n      });\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    dep = 2;\n    hook.rerender({\n      manual: true,\n      refreshDeps: [dep],\n    });\n    expect(hook.result.current.loading).toBe(false);\n  });\n\n  test('useAutoRunPlugin refreshDepsAction should work', async () => {\n    let dep = 1;\n    let count = 0;\n    const refreshDepsAction = () => {\n      count += 1;\n    };\n    act(() => {\n      hook = setUp(request, {\n        refreshDeps: [dep],\n        refreshDepsAction,\n      });\n    });\n    expect(hook.result.current.loading).toBe(true);\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    dep = 2;\n    hook.rerender({\n      refreshDeps: [dep],\n      refreshDepsAction,\n    });\n    expect(hook.result.current.loading).toBe(false);\n    expect(count).toBe(1);\n\n    hook.rerender({\n      refreshDeps: [dep],\n      refreshDepsAction,\n    });\n    expect(hook.result.current.loading).toBe(false);\n    expect(count).toBe(1);\n\n    dep = 3;\n    hook.rerender({\n      refreshDeps: [dep],\n      refreshDepsAction,\n    });\n    expect(hook.result.current.loading).toBe(false);\n    expect(count).toBe(2);\n  });\n\n  test('useAutoRunPlugin ready & refreshDeps change same time work fine', async () => {\n    const fn = vi.fn();\n\n    const asyncFn = () => {\n      return new Promise<string>((resolve) => {\n        fn();\n        return resolve('success');\n      });\n    };\n\n    act(() => {\n      hook = setUp<string, [number]>(asyncFn, {\n        ready: false,\n        defaultParams: [1],\n        refreshDeps: [1],\n      });\n    });\n\n    expect(hook.result.current.loading).toBe(false);\n\n    hook.rerender({\n      ready: true,\n      defaultParams: [2],\n      refreshDeps: [2],\n    });\n    expect(hook.result.current.loading).toBe(true);\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n    expect(hook.result.current.params).toEqual([2]);\n    expect(fn).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useRequest/__tests__/useCachePlugin.spec.tsx",
    "content": "import { act, render, renderHook } from '@testing-library/react';\nimport { useState } from 'react';\nimport { describe, expect, test, vi } from 'vitest';\nimport { request } from '../../utils/testingHelpers';\nimport useRequest, { clearCache } from '../index';\n\ndescribe('useCachePlugin', () => {\n  vi.useFakeTimers();\n\n  const setup = (\n    service: Parameters<typeof useRequest>[0],\n    options: Parameters<typeof useRequest>[1],\n  ) => renderHook(() => useRequest(service, options));\n\n  const testCacheKey = async (options: any) => {\n    const hook = setup(request, options);\n    expect(hook.result.current.loading).toBe(true);\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n    expect(hook.result.current.data).toBe('success');\n    hook.unmount();\n  };\n\n  test('useRequest cacheKey should work', async () => {\n    await testCacheKey({\n      cacheKey: 'testCacheKey',\n    });\n\n    vi.advanceTimersByTime(100);\n\n    const hook2 = setup(request, {\n      cacheKey: 'testCacheKey',\n    });\n    expect(hook2.result.current.loading).toBe(true);\n    expect(hook2.result.current.data).toBe('success');\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook2.result.current.loading).toBe(false);\n  });\n\n  test('useRequest staleTime should work', async () => {\n    await testCacheKey({\n      cacheKey: 'testStaleTime',\n      staleTime: 3000,\n    });\n\n    vi.advanceTimersByTime(1000);\n\n    const hook2 = setup(request, {\n      cacheKey: 'testStaleTime',\n      staleTime: 3000,\n    });\n    expect(hook2.result.current.loading).toBe(false);\n    expect(hook2.result.current.data).toBe('success');\n    hook2.unmount();\n\n    vi.advanceTimersByTime(3001);\n\n    const hook3 = setup(request, {\n      cacheKey: 'testStaleTime',\n      staleTime: 3000,\n    });\n    expect(hook3.result.current.loading).toBe(true);\n    expect(hook3.result.current.data).toBe('success');\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook3.result.current.loading).toBe(false);\n  });\n\n  test('useRequest cacheTime should work', async () => {\n    await testCacheKey({\n      cacheKey: 'testCacheTime',\n      cacheTime: 5000,\n    });\n\n    vi.advanceTimersByTime(1000);\n\n    const hook2 = setup(request, {\n      cacheKey: 'testCacheTime',\n      cacheTime: 5000,\n    });\n    expect(hook2.result.current.loading).toBe(true);\n    expect(hook2.result.current.data).toBe('success');\n    hook2.unmount();\n\n    vi.advanceTimersByTime(5001);\n\n    const hook3 = setup(request, {\n      cacheKey: 'testCacheTime',\n      cacheTime: 5000,\n    });\n    expect(hook3.result.current.loading).toBe(true);\n    expect(hook3.result.current.data).toBeUndefined();\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook3.result.current.loading).toBe(false);\n    expect(hook3.result.current.data).toBe('success');\n  });\n\n  test('clearCache should work', async () => {\n    await testCacheKey('testClearCache');\n\n    clearCache('testClearCache');\n    const hook2 = setup(request, {\n      cacheKey: 'testClearCache',\n    });\n    expect(hook2.result.current.loading).toBe(true);\n    expect(hook2.result.current.data).toBeUndefined();\n  });\n\n  test('setCache/getCache should work', async () => {\n    const cacheKey = `setCacheKey`;\n    await testCacheKey({\n      cacheKey,\n      setCache: (data: JSON) => localStorage.setItem(cacheKey, JSON.stringify(data)),\n      getCache: () => JSON.parse(localStorage.getItem(cacheKey) || '{}'),\n    });\n\n    vi.advanceTimersByTime(1000);\n    const hook2 = setup(request, {\n      cacheKey,\n      setCache: (data) => localStorage.setItem(cacheKey, JSON.stringify(data)),\n      getCache: () => JSON.parse(localStorage.getItem(cacheKey) || '{}'),\n    });\n    expect(hook2.result.current.loading).toBe(true);\n    expect(hook2.result.current.data).toBe('success');\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook2.result.current.loading).toBe(false);\n  });\n\n  test('cache should work when change data immediately', async () => {\n    const { result } = setup(request, {\n      cacheKey: 'mutateCacheKey',\n    });\n    act(() => {\n      result.current.mutate(1);\n    });\n    expect(result.current.data).toBe(1);\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(result.current.loading).toBe(false);\n    expect(result.current.data).toBe('success');\n  });\n\n  //github.com/alibaba/hooks/issues/1859\n  test('error should reset with activeKey', async () => {\n    const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});\n\n    let res = {} as any;\n    const TestComponent = () => {\n      const [key, setKey] = useState<number>(1);\n      const { data, error } = useRequest(() => request(key), {\n        refreshDeps: [key],\n        cacheKey: String(key),\n        staleTime: 300000,\n      });\n      res = {\n        data,\n        error,\n        setKey,\n      };\n      return null;\n    };\n\n    render(<TestComponent />);\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(res.error).toBeUndefined();\n\n    act(() => res.setKey(0));\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(errSpy).toBeCalled();\n    expect(res.error).not.toBeUndefined();\n\n    act(() => res.setKey(1));\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(res.error).toBeUndefined();\n\n    errSpy.mockRestore();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useRequest/__tests__/useDebouncePlugin.spec.ts",
    "content": "import { act, type RenderHookResult, renderHook } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\nimport { request } from '../../utils/testingHelpers';\nimport useRequest from '../index';\n\ndescribe('useDebouncePlugin', () => {\n  const setUp = (\n    service: Parameters<typeof useRequest>[0],\n    options: Parameters<typeof useRequest>[1],\n  ) => renderHook((o) => useRequest(service, o || options));\n\n  let hook: RenderHookResult<any, any>;\n\n  test('useDebouncePlugin should work', () => {\n    vi.useFakeTimers();\n    const callback = vi.fn();\n\n    act(() => {\n      hook = setUp(\n        () => {\n          callback();\n          return request({});\n        },\n        {\n          manual: true,\n          debounceWait: 100,\n        },\n      );\n    });\n\n    act(() => {\n      hook.result.current.run(1);\n      vi.advanceTimersByTime(50);\n      hook.result.current.run(2);\n      vi.advanceTimersByTime(50);\n      hook.result.current.run(3);\n      vi.advanceTimersByTime(50);\n      hook.result.current.run(4);\n    });\n\n    act(() => {\n      vi.runAllTimers();\n    });\n    expect(callback).toHaveBeenCalledTimes(1);\n\n    act(() => {\n      hook.result.current.run(1);\n      vi.advanceTimersByTime(50);\n      hook.result.current.run(2);\n      vi.advanceTimersByTime(50);\n      hook.result.current.run(3);\n      vi.advanceTimersByTime(50);\n      hook.result.current.run(4);\n    });\n\n    act(() => {\n      vi.runAllTimers();\n    });\n    expect(callback).toHaveBeenCalledTimes(2);\n\n    act(() => {\n      hook.result.current.run(1);\n      vi.advanceTimersByTime(50);\n      hook.result.current.run(2);\n      vi.advanceTimersByTime(50);\n      hook.result.current.cancel();\n    });\n\n    act(() => {\n      vi.runAllTimers();\n    });\n    expect(callback).toHaveBeenCalledTimes(2);\n\n    hook.unmount();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useRequest/__tests__/useLoadingDelayPlugin.spec.ts",
    "content": "import { act, type RenderHookResult, renderHook } from '@testing-library/react';\nimport { afterEach, describe, expect, test, vi } from 'vitest';\nimport { request } from '../../utils/testingHelpers';\nimport useRequest from '../index';\n\ndescribe('useLoadingDelayPlugin', () => {\n  vi.useFakeTimers();\n\n  const setUp = <TData = string, TParams extends any[] = any[]>(\n    service: (...args: TParams) => Promise<TData>,\n    options?: Parameters<typeof useRequest<TData, TParams>>[1],\n  ) => renderHook((o) => useRequest<TData, TParams>(service, o || options));\n\n  let hook: RenderHookResult<ReturnType<typeof useRequest>, any>;\n\n  afterEach(() => {\n    hook.unmount();\n  });\n\n  test('useLoadingDelayPlugin should work', async () => {\n    act(() => {\n      hook = setUp(request, {\n        loadingDelay: 2000,\n      });\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    act(() => {\n      hook = setUp(request, {\n        loadingDelay: 500,\n      });\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    act(() => {\n      vi.advanceTimersByTime(501);\n    });\n    expect(hook.result.current.loading).toBe(true);\n\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n  });\n\n  test('useLoadingDelayPlugin should no update loading when ready is false', async () => {\n    act(() => {\n      hook = setUp(request, {\n        loadingDelay: 2000,\n        ready: false,\n      });\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    act(() => {\n      vi.advanceTimersByTime(3000);\n    });\n\n    expect(hook.result.current.loading).toBe(false);\n  });\n\n  test('useLoadingDelayPlugin should update loading when ready is undefined', async () => {\n    act(() => {\n      hook = setUp(request, {\n        loadingDelay: 2000,\n      });\n    });\n    expect(hook.result.current.loading).toBe(false);\n\n    act(() => {\n      vi.advanceTimersByTime(3000);\n    });\n\n    expect(hook.result.current.loading).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useRequest/__tests__/usePollingPlugin.spec.ts",
    "content": "import { act, type RenderHookResult, renderHook } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\nimport { request } from '../../utils/testingHelpers';\nimport useRequest from '../index';\n\ndescribe('usePollingPlugin', () => {\n  vi.useFakeTimers();\n\n  const setUp = <TData = string, TParams extends any[] = any[]>(\n    service: (...args: TParams) => Promise<TData>,\n    options?: Parameters<typeof useRequest<TData, TParams>>[1],\n  ) => renderHook((o) => useRequest<TData, TParams>(service, o || options));\n\n  let hook: RenderHookResult<any, any>;\n\n  test('usePollingPlugin pollingInterval=100 pollingWhenHidden=true  should work', async () => {\n    const callback = vi.fn();\n    act(() => {\n      hook = setUp(\n        () => {\n          callback();\n          return request(1);\n        },\n        {\n          pollingInterval: 100,\n          pollingWhenHidden: true,\n        },\n      );\n    });\n    expect(hook.result.current.loading).toBe(true);\n\n    // 第一次请求完成\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n    expect(hook.result.current.data).toBe('success');\n    expect(callback).toHaveBeenCalledTimes(1);\n\n    // 第一次 polling (100ms 间隔 + 1000ms 执行)\n    await act(async () => {\n      vi.advanceTimersByTime(1100);\n    });\n    expect(callback).toHaveBeenCalledTimes(2);\n\n    // 第二次 polling\n    await act(async () => {\n      vi.advanceTimersByTime(1100);\n    });\n    expect(callback).toHaveBeenCalledTimes(3);\n\n    act(() => {\n      hook.result.current.cancel();\n    });\n\n    // 取消后不应该再 polling\n    act(() => {\n      vi.advanceTimersByTime(1100);\n    });\n    expect(callback).toHaveBeenCalledTimes(3);\n\n    // 手动重新运行\n    act(() => {\n      hook.result.current.run();\n    });\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(callback).toHaveBeenCalledTimes(4);\n\n    // 恢复 polling\n    await act(async () => {\n      vi.advanceTimersByTime(1100);\n    });\n    expect(callback).toHaveBeenCalledTimes(5);\n  });\n\n  let hook2: RenderHookResult<any, any>;\n  test('usePollingPlugin pollingErrorRetryCount=3 should work', async () => {\n    // if request error and set pollingErrorRetryCount\n    // and the number of consecutive failures exceeds pollingErrorRetryCount, polling stops\n    let errorCallback: any;\n    act(() => {\n      errorCallback = vi.fn();\n      hook2 = setUp(() => request(0), {\n        pollingErrorRetryCount: 3,\n        pollingInterval: 100,\n        pollingWhenHidden: true,\n        onError: errorCallback,\n      });\n    });\n\n    expect(hook2.result.current.loading).toBe(true);\n    expect(errorCallback).toHaveBeenCalledTimes(0);\n\n    // 第一次请求失败\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(hook2.result.current.loading).toBe(false);\n    expect(errorCallback).toHaveBeenCalledTimes(1);\n\n    // 第一次重试失败 (100ms 间隔 + 1000ms 执行)\n    await act(async () => {\n      vi.advanceTimersByTime(1100);\n    });\n    expect(errorCallback).toHaveBeenCalledTimes(2);\n\n    // 第二次重试失败\n    await act(async () => {\n      vi.advanceTimersByTime(1100);\n    });\n    expect(errorCallback).toHaveBeenCalledTimes(3);\n\n    // 第三次重试失败\n    await act(async () => {\n      vi.advanceTimersByTime(1100);\n    });\n    expect(errorCallback).toHaveBeenCalledTimes(4);\n\n    // 达到重试限制，停止 polling\n    act(() => {\n      vi.advanceTimersByTime(1100);\n    });\n    expect(errorCallback).toHaveBeenCalledTimes(4);\n\n    // 手动重新运行\n    act(() => {\n      hook2.result.current.run();\n    });\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(errorCallback).toHaveBeenCalledTimes(5);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useRequest/__tests__/useRefreshOnWindowFocusPlugin.spec.ts",
    "content": "import { act, fireEvent, type RenderHookResult, renderHook } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\nimport { request } from '../../utils/testingHelpers';\nimport useRequest from '../index';\n\ndescribe('useRefreshOnWindowFocusPlugin', () => {\n  vi.useFakeTimers();\n\n  const setUp = <TData = string, TParams extends any[] = any[]>(\n    service: (...args: TParams) => Promise<TData>,\n    options?: Parameters<typeof useRequest<TData, TParams>>[1],\n  ) => renderHook((o) => useRequest<TData, TParams>(service, o || options));\n\n  let hook: RenderHookResult<any, any>;\n  let hook1: RenderHookResult<any, any>;\n  let hook2: RenderHookResult<any, any>;\n\n  test('useRefreshOnWindowFocusPlugin should work', async () => {\n    act(() => {\n      hook = setUp(request, {\n        refreshOnWindowFocus: true,\n        focusTimespan: 5000,\n      });\n    });\n    expect(hook.result.current.loading).toBe(true);\n    await act(async () => {\n      vi.advanceTimersByTime(1001);\n    });\n    expect(hook.result.current.loading).toBe(false);\n    act(() => {\n      fireEvent.focus(window);\n    });\n    expect(hook.result.current.loading).toBe(true);\n\n    await act(async () => {\n      vi.advanceTimersByTime(2000);\n    });\n    expect(hook.result.current.loading).toBe(false);\n    act(() => {\n      vi.advanceTimersByTime(3000);\n      fireEvent.focus(window);\n    });\n    expect(hook.result.current.loading).toBe(true);\n  });\n\n  test('fix: multiple unsubscriptions should not delete the last subscription listener ', async () => {\n    act(() => {\n      hook1 = setUp(request, {\n        refreshOnWindowFocus: true,\n      });\n      hook2 = setUp(request, {\n        refreshOnWindowFocus: true,\n      });\n    });\n\n    expect(hook1.result.current.loading).toBe(true);\n    expect(hook2.result.current.loading).toBe(true);\n\n    await act(async () => {\n      vi.advanceTimersByTime(1001);\n    });\n    expect(hook1.result.current.loading).toBe(false);\n    expect(hook2.result.current.loading).toBe(false);\n\n    act(() => {\n      fireEvent.focus(window);\n    });\n\n    expect(hook1.result.current.loading).toBe(true);\n    expect(hook2.result.current.loading).toBe(true);\n\n    await act(async () => {\n      vi.advanceTimersByTime(2000);\n    });\n\n    expect(hook1.result.current.loading).toBe(false);\n    expect(hook2.result.current.loading).toBe(false);\n\n    hook1.unmount();\n\n    act(() => {\n      vi.advanceTimersByTime(3000);\n      fireEvent.focus(window);\n    });\n\n    expect(hook1.result.current.loading).toBe(false);\n    // hook2 should not unsubscribe\n    expect(hook2.result.current.loading).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useRequest/__tests__/useRetryPlugin.spec.ts",
    "content": "import { act, type RenderHookResult, renderHook } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\nimport { request } from '../../utils/testingHelpers';\nimport useRequest from '../index';\n\ndescribe('useRetryPlugin', () => {\n  vi.useFakeTimers();\n\n  const setUp = <TData = string, TParams extends any[] = any[]>(\n    service: (...args: TParams) => Promise<TData>,\n    options?: Parameters<typeof useRequest<TData, TParams>>[1],\n  ) => renderHook((o) => useRequest<TData, TParams>(service, o || options));\n\n  let hook: RenderHookResult<any, any>;\n  let hook2: RenderHookResult<any, any>;\n\n  test('useRetryPlugin should work', async () => {\n    let errorCallback: any;\n    act(() => {\n      errorCallback = vi.fn();\n      hook = setUp(() => request(0), {\n        retryCount: 3,\n        onError: errorCallback,\n      });\n    });\n    act(() => {\n      vi.advanceTimersByTime(500);\n    });\n    expect(errorCallback).toHaveBeenCalledTimes(0);\n\n    // 第一次执行失败 (1000ms)\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(errorCallback).toHaveBeenCalledTimes(1);\n\n    // 第一次重试失败 (等待 2000ms 后重试 + 1000ms 执行)\n    await act(async () => {\n      vi.advanceTimersByTime(3000);\n    });\n    expect(errorCallback).toHaveBeenCalledTimes(2);\n\n    // 第二次重试失败 (等待 4000ms 后重试 + 1000ms 执行)\n    await act(async () => {\n      vi.advanceTimersByTime(5000);\n    });\n    expect(errorCallback).toHaveBeenCalledTimes(3);\n\n    // 第三次重试失败 (等待 8000ms 后重试 + 1000ms 执行)\n    await act(async () => {\n      vi.advanceTimersByTime(9000);\n    });\n    expect(errorCallback).toHaveBeenCalledTimes(4);\n\n    // 达到重试次数限制，不再重试\n    act(() => {\n      vi.advanceTimersByTime(10000);\n    });\n    expect(errorCallback).toHaveBeenCalledTimes(4);\n    hook.unmount();\n\n    // cancel should work\n    act(() => {\n      errorCallback = vi.fn();\n      hook2 = setUp(() => request(0), {\n        retryCount: 3,\n        onError: errorCallback,\n      });\n    });\n    expect(errorCallback).toHaveBeenCalledTimes(0);\n\n    // 第一次执行失败\n    await act(async () => {\n      vi.advanceTimersByTime(1000);\n    });\n    expect(errorCallback).toHaveBeenCalledTimes(1);\n\n    // 第一次重试失败\n    await act(async () => {\n      vi.advanceTimersByTime(3000);\n    });\n    expect(errorCallback).toHaveBeenCalledTimes(2);\n\n    // 取消重试\n    act(() => {\n      hook2.result.current.cancel();\n    });\n    act(() => {\n      vi.advanceTimersByTime(10000);\n    });\n    expect(errorCallback).toHaveBeenCalledTimes(2);\n    hook2.unmount();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useRequest/__tests__/useThrottlePlugin.spec.ts",
    "content": "import { act, type RenderHookResult, renderHook } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\nimport { request } from '../../utils/testingHelpers';\nimport useRequest from '../index';\n\ndescribe('useThrottlePlugin', () => {\n  vi.useFakeTimers();\n\n  const setUp = (\n    service: Parameters<typeof useRequest>[0],\n    options: Parameters<typeof useRequest>[1],\n  ) => renderHook((o) => useRequest(service, o || options));\n\n  let hook: RenderHookResult<any, any>;\n  test('useThrottlePlugin should work', () => {\n    const callback = vi.fn();\n\n    act(() => {\n      hook = setUp(\n        () => {\n          callback();\n          return request({});\n        },\n        {\n          manual: true,\n          throttleWait: 100,\n        },\n      );\n    });\n\n    act(() => {\n      hook.result.current.run(1);\n      vi.advanceTimersByTime(50);\n      hook.result.current.run(2);\n      vi.advanceTimersByTime(50);\n      hook.result.current.run(3);\n      vi.advanceTimersByTime(50);\n      hook.result.current.run(4);\n      vi.advanceTimersByTime(40);\n    });\n\n    expect(callback).toHaveBeenCalledTimes(2);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/basic/basic.en-US.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# Basic usage\n\nIn this section, we will introduce the core and basic functionalities of `useRequest`, that is, the functionalities of the `useRequest` kernel.\n\n## Default request\n\nBy default, the first parameter of `useRequest` is an asynchronous function, which is automatically executed when the component is initialized. At the same time, it automatically manages the status of `loading`, `data`, `error` of the asynchronous function.\n\n```js\nconst { data, error, loading } = useRequest(service);\n```\n\n<br />\n\n<code src=\"./demo/default.tsx\" />\n\n## Manually trigger\n\nIf `options.manual = true` is set, `useRequest` will not be executed by default, and the execution needs to be triggered by `run` or `runAsync`.\n\n```tsx | pure\nconst { loading, run, runAsync } = useRequest(service, {\n  manual: true\n});\n\n<button onClick={run} disabled={loading}>\n  {loading ? 'Loading' : 'Edit'}\n</button>\n```\n\nThe difference between `run` and `runAsync` is:\n\n- `run` is a normal synchronous function, we will automatically catch the exception, you can use `options.onError` to handle the behavior of the exception.\n- `runAsync` is a asynchronous function that returns a `Promise`. If you use `runAsync` to call it, it means you need to catch the exception yourself.\n\n  ```ts\n  runAsync().then((data) => {\n    console.log(data);\n  }).catch((error) => {\n    console.log(error);\n  })\n  ```\n\nNext, we will demonstrate the difference between `run` and `runAsync` through the simple scenario of editing the username.\n\n<code src=\"./demo/manual-run.tsx\" />\n\n<code src=\"./demo/manual-runAsync.tsx\" />\n\n## The life cycle\n\n`useRequest` provides the following life cycle for you to do some processing in different stages of asynchronous functions.\n\n- `onBefore`: Triggered before the request\n- `onSuccess`: Triggered when the request is resolved\n- `onError`: Triggered when the request is rejected\n- `onFinally`: Triggered when the request is completed\n\n<code src=\"./demo/lifeCycle.tsx\" />\n\n## Refresh (repeat the last request)\n\n`useRequest` provides the `refresh` and `refreshAsync` methods so that we can use the last parameters to re-run the request.\n\nIf in the scenario of reading user information\n\n1. We read the user information with ID 1 `run(1)`\n2. We updated user information by some ways\n3. We want to re-initiate the last request, then we can use `refresh` instead of `run(1)`, which is very useful in scenarios with complex parameters\n\n<code src=\"./demo/refresh.tsx\" />\n\nOf course, the difference between `refresh` and `refreshAsync` is the same as `run` and `runAsync`.\n\n## Change data immediately\n\n`useRequest` provides `mutate`, which can immediate modify the `data`.\n\nThe usage of `mutate` is consistent with `React.setState`, supports: `mutate(newData)` and `mutate((oldData) => newData)`.\n\nIn the following example, we demonstrate a scenario of `mutate`.\n\nWe have modified the user name, but we do not want to wait for the request to be successful before giving feedback to the user. Instead, modify the data directly, then call the modify request in background, and provide additional feedback after the request returns.\n\n<code src=\"./demo/mutate.tsx\" />\n\n<!-- ## Format data\n\n`useRequest` provides the `formatResult` configuration item, which can format the data returned by the service once. If `formatResult` is configured, the return value of other places where `data` is used shall prevail, such as the parameters of `result.data`, `options.onSuccess` and so on. -->\n\n<!-- <code src=\"./demo/formatResult.tsx\" /> -->\n\n## Cancel response\n\n`useRequest` provides a `cancel` function, which will **ignore** the data and error returned by the current promise\n\n**Note: Calling `cancel` doesn't cancel the execution of promise**\n\nAt the same time, `useRequest` will automatically ignore the response at the following timing:\n\n- When the component is unmounting, the ongoing promise\n- Race cancellation, when the previous promise has not returned, if the next promise is initiated, the previous promise will be ignored\n\n<code src=\"./demo/cancel.tsx\" />\n\n## Parameter management\n\nThe `params` returned by `useRequest` will record the parameters of `service`. For example, if you trigger `run(1, 2, 3)`, then `params` is equal to `[1, 2, 3]`.\n\nIf we set `options.manual = false`, the parameters of calling `service` for the first time can be set by `options.defaultParams`.\n\n<code src=\"./demo/params.tsx\" />\n\n## API\n\n```ts\nconst {\n  loading: boolean,\n  data?: TData,\n  error?: Error,\n  params: TParams || [],\n  run: (...params: TParams) => void,\n  runAsync: (...params: TParams) => Promise<TData>,\n  refresh: () => void,\n  refreshAsync: () => Promise<TData>,\n  mutate: (data?: TData | ((oldData?: TData) => (TData | undefined))) => void,\n  cancel: () => void,\n} = useRequest<TData, TParams>(\n  service: (...args: TParams) => Promise<TData>,\n  {\n    manual?: boolean,\n    defaultParams?: TParams,\n    onBefore?: (params: TParams) => void,\n    onSuccess?: (data: TData, params: TParams) => void,\n    onError?: (e: Error, params: TParams) => void,\n    onFinally?: (params: TParams, data?: TData, e?: Error) => void,\n  }\n);\n```\n\n### Result\n\n| Property     | Description                                                                                                                                                                             | Type                                                                  |\n| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |\n| data         | Data returned by service                                                                                                                                                                | `TData` \\| `undefined`                                                |\n| error        | Exception thrown by service                                                                                                                                                             | `Error` \\| `undefined`                                                |\n| loading      | Is the service being executed                                                                                                                                                           | `boolean`                                                             |\n| params       | An array of parameters for the service being executed. For example, you triggered `run(1, 2, 3)`, then params is equal to `[1, 2, 3]`                                                   | `TParams` \\| `[]`                                                     |\n| run          | <ul><li> Manually trigger the execution of the service, and the parameters will be passed to the service</li><li>Automatic handling of exceptions, feedback through `onError`</li></ul> | `(...params : TParams) => void`                                       |\n| runAsync     | The usage is the same as `run`, but it returns a Promise, so you need to handle the exception yourself.                                                                                 | `(...params: TParams) => Promise<TData>`                              |\n| refresh      | Use the last params, call `run` again                                                                                                                                                   | `() => void`                                                          |\n| refreshAsync | Use the last params, call `runAsync` again                                                                                                                                              | `() => Promise<TData>`                                                |\n| mutate       | Mutate `data` directly                                                                                                                                                                  | `(data?: TData / ((oldData?: TData) => (TData / undefined))) => void` |\n| cancel       | Ignore the current promise response                                                                                                                                                     | `() => void`                                                          |\n\n### Options\n\n| Property      | Description                                                                                                                                                                                                      | Type                                                 | Default |\n| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ------- |\n| manual        | <ul><li> The default is `false`. That is, the service is automatically executed during initialization. </li><li>If set to `true`, you need to manually call `run` or `runAsync` to trigger execution. </li></ul> | `boolean`                                            | `false` |\n| defaultParams | The parameters passed to the service at the first default execution                                                                                                                                              | `TParams`                                            | -       |\n| onBefore      | Triggered before service execution                                                                                                                                                                               | `(params: TParams) => void`                          | -       |\n| onSuccess     | Triggered when service resolve                                                                                                                                                                                   | `(data: TData, params: TParams) => void`             | -       |\n| onError       | Triggered when service reject                                                                                                                                                                                    | `(e: Error, params: TParams) => void`                | -       |\n| onFinally     | Triggered when service execution is complete                                                                                                                                                                     | `(params: TParams, data?: TData, e?: Error) => void` | -       |\n\nAbove we have introduced the most basic functionalities of useRequest, and then we will introduce some more advanced functionalities.\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/basic/basic.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# 基础用法\n\n这一小节我们会介绍 `useRequest` 最核心，最基础的能力，也就是 `useRequest` 内核的能力。\n\n## 默认请求\n\n默认情况下，`useRequest` 第一个参数是一个异步函数，在组件初始化时，会自动执行该异步函数。同时自动管理该异步函数的 `loading` , `data` , `error` 等状态。\n\n```js\nconst { data, error, loading } = useRequest(service);\n```\n\n<br />\n\n<code src=\"./demo/default.tsx\" />\n\n## 手动触发\n\n如果设置了 `options.manual = true`，则 `useRequest` 不会默认执行，需要通过 `run` 或者 `runAsync` 来触发执行。\n\n```tsx | pure\nconst { loading, run, runAsync } = useRequest(service, {\n  manual: true\n});\n\n<button onClick={run} disabled={loading}>\n  {loading ? 'Loading' : 'Edit'}\n</button>\n```\n\n`run` 与 `runAsync` 的区别在于：\n\n- `run` 是一个普通的同步函数，我们会自动捕获异常，你可以通过 `options.onError` 来处理异常时的行为。\n- `runAsync` 是一个返回 `Promise` 的异步函数，如果使用 `runAsync` 来调用，则意味着你需要自己捕获异常。\n\n  ```ts\n  runAsync().then((data) => {\n    console.log(data);\n  }).catch((error) => {\n    console.log(error);\n  })\n  ```\n\n接下来我们通过修改用户名这个简单的场景，来演示 useRequest 手动触发模式，以及 `run` 与 `runAsync` 的区别。\n\n<code src=\"./demo/manual-run.tsx\" />\n\n<code src=\"./demo/manual-runAsync.tsx\" />\n\n## 生命周期\n\n`useRequest` 提供了以下几个生命周期配置项，供你在异步函数的不同阶段做一些处理。\n\n- `onBefore`：请求之前触发\n- `onSuccess`：请求成功触发\n- `onError`：请求失败触发\n- `onFinally`：请求完成触发\n\n<code src=\"./demo/lifeCycle.tsx\" />\n\n## 刷新（重复上一次请求）\n\n`useRequest` 提供了 `refresh` 和 `refreshAsync` 方法，使我们可以使用上一次的参数，重新发起请求。\n\n假如在读取用户信息的场景中\n\n1. 我们读取了 ID 为 1 的用户信息 `run(1)`\n2. 我们通过某种手段更新了用户信息\n3. 我们想重新发起上一次的请求，那我们就可以使用 `refresh` 来代替 `run(1)`，这在复杂参数的场景中是非常有用的\n\n<code src=\"./demo/refresh.tsx\" />\n\n当然 `refresh` 和 `refreshAsync` 的区别和 `run` 和 `runAsync` 是一致的。\n\n## 立即变更数据\n\n`useRequest` 提供了 `mutate`, 支持立即修改 `useRequest` 返回的 `data` 参数。\n\n`mutate` 的用法与 `React.setState` 一致，支持 `mutate(newData)` 和 `mutate((oldData) => newData)` 两种写法。\n\n下面的示例，我们演示了一种 `mutate` 的应用场景。\n\n我们修改了用户名，但是我们不希望等编辑接口调用成功之后，才给用户反馈。而是直接修改页面数据，同时在背后去调用修改接口，等修改接口返回之后，另外提供反馈。\n\n<code src=\"./demo/mutate.tsx\" />\n\n<!-- ## 格式化数据\n\n`useRequest` 提供了 `formatResult` 配置项，可以对 service 返回的数据做一次格式化。如果配置了 `formatResult`，则其它用到 `data` 的地方均以该返回值为准，比如 `result.data`，`options.onSuccess` 的参数等等。 -->\n\n<!-- <code src=\"./demo/formatResult.tsx\" /> -->\n\n## 取消响应\n\n`useRequest` 提供了 `cancel` 函数，用于**忽略**当前 promise 返回的数据和错误\n\n**注意：调用 `cancel` 函数并不会取消 promise 的执行**\n\n同时 `useRequest` 会在以下时机自动忽略响应：\n\n- 组件卸载时，正在进行的 promise\n- 竞态取消，当上一次 promise 还没返回时，又发起了下一次 promise，则会忽略上一次 promise 的响应\n\n<code src=\"./demo/cancel.tsx\" />\n\n## 参数管理\n\n`useRequest` 返回的 `params` 会记录当次调用 `service` 的参数数组。比如你触发了 `run(1, 2, 3)`，则 `params` 等于 `[1, 2, 3]` 。\n\n如果我们设置了 `options.manual = false`，则首次调用 `service` 的参数可以通过 `options.defaultParams` 来设置。\n\n<code src=\"./demo/params.tsx\" />\n\n## API\n\n```ts\nconst {\n  loading: boolean,\n  data?: TData,\n  error?: Error,\n  params: TParams || [],\n  run: (...params: TParams) => void,\n  runAsync: (...params: TParams) => Promise<TData>,\n  refresh: () => void,\n  refreshAsync: () => Promise<TData>,\n  mutate: (data?: TData | ((oldData?: TData) => (TData | undefined))) => void,\n  cancel: () => void,\n} = useRequest<TData, TParams>(\n  service: (...args: TParams) => Promise<TData>,\n  {\n    manual?: boolean,\n    defaultParams?: TParams,\n    onBefore?: (params: TParams) => void,\n    onSuccess?: (data: TData, params: TParams) => void,\n    onError?: (e: Error, params: TParams) => void,\n    onFinally?: (params: TParams, data?: TData, e?: Error) => void,\n  }\n);\n```\n\n### Result\n\n| 参数         | 说明                                                                                                     | 类型                                                                  |\n| ------------ | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |\n| data         | service 返回的数据                                                                                       | `TData` \\| `undefined`                                                |\n| error        | service 抛出的异常                                                                                       | `Error` \\| `undefined`                                                |\n| loading      | service 是否正在执行                                                                                     | `boolean`                                                             |\n| params       | 当次执行的 service 的参数数组。比如你触发了 `run(1, 2, 3)`，则 params 等于 `[1, 2, 3]`                   | `TParams` \\| `[]`                                                     |\n| run          | <ul><li> 手动触发 service 执行，参数会传递给 service</li><li>异常自动处理，通过 `onError` 反馈</li></ul> | `(...params: TParams) => void`                                        |\n| runAsync     | 与 `run` 用法一致，但返回的是 Promise，需要自行处理异常。                                                | `(...params: TParams) => Promise<TData>`                              |\n| refresh      | 使用上一次的 params，重新调用 `run`                                                                      | `() => void`                                                          |\n| refreshAsync | 使用上一次的 params，重新调用 `runAsync`                                                                 | `() => Promise<TData>`                                                |\n| mutate       | 直接修改 `data`                                                                                          | `(data?: TData / ((oldData?: TData) => (TData / undefined))) => void` |\n| cancel       | 忽略当前 Promise 的响应                                                                                  | `() => void`                                                          |\n\n### Options\n\n| 参数          | 说明                                                                                                                                       | 类型                                                 | 默认值  |\n| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- | ------- |\n| manual        | <ul><li> 默认 `false`。 即在初始化时自动执行 service。</li><li>如果设置为 `true`，则需要手动调用 `run` 或 `runAsync` 触发执行。 </li></ul> | `boolean`                                            | `false` |\n| defaultParams | 首次默认执行时，传递给 service 的参数                                                                                                      | `TParams`                                            | -       |\n| onBefore      | service 执行前触发                                                                                                                         | `(params: TParams) => void`                          | -       |\n| onSuccess     | service resolve 时触发                                                                                                                     | `(data: TData, params: TParams) => void`             | -       |\n| onError       | service reject 时触发                                                                                                                      | `(e: Error, params: TParams) => void`                | -       |\n| onFinally     | service 执行完成时触发                                                                                                                     | `(params: TParams, data?: TData, e?: Error) => void` | -       |\n\n以上我们介绍了 useRequest 最基础的功能，接下来我们介绍一些更高级的能力。\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/basic/demo/cancel.tsx",
    "content": "import { message } from 'antd';\nimport { useState } from 'react';\nimport { useRequest } from 'ahooks';\n\nfunction editUsername(username: string): Promise<void> {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      if (Math.random() > 0.5) {\n        resolve();\n      } else {\n        reject(new Error('Failed to modify username'));\n      }\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const [state, setState] = useState('');\n\n  const { loading, run, cancel } = useRequest(editUsername, {\n    manual: true,\n    onSuccess: (result, params) => {\n      setState('');\n      message.success(`The username was changed to \"${params[0]}\" !`);\n    },\n    onError: (error) => {\n      message.error(error.message);\n    },\n  });\n\n  return (\n    <div>\n      <input\n        onChange={(e) => setState(e.target.value)}\n        value={state}\n        placeholder=\"Please enter username\"\n        style={{ width: 240, marginRight: 16 }}\n      />\n      <button disabled={loading} type=\"button\" onClick={() => run(state)}>\n        {loading ? 'Loading' : 'Edit'}\n      </button>\n      <button type=\"button\" onClick={cancel} style={{ marginLeft: 16 }}>\n        Cancel\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/basic/demo/default.tsx",
    "content": "/**\n * title: Read username\n *\n * title.zh-CN: 读取用户名称\n */\n\nimport { useRequest } from 'ahooks';\nimport Mock from 'mockjs';\nfunction getUsername(): Promise<string> {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      if (Math.random() > 0.5) {\n        resolve(Mock.mock('@name'));\n      } else {\n        reject(new Error('Failed to get username'));\n      }\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const { data, error, loading } = useRequest(getUsername);\n\n  if (error) {\n    return <div>{error.message}</div>;\n  }\n  if (loading) {\n    return <div>loading...</div>;\n  }\n  return <div>Username: {data}</div>;\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/basic/demo/lifeCycle.tsx",
    "content": "import { message } from 'antd';\nimport { useState } from 'react';\nimport { useRequest } from 'ahooks';\n\nfunction editUsername(username: string): Promise<void> {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      if (Math.random() > 0.5) {\n        resolve();\n      } else {\n        reject(new Error('Failed to modify username'));\n      }\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const [state, setState] = useState('');\n\n  const { loading, run } = useRequest(editUsername, {\n    manual: true,\n    onBefore: (params) => {\n      message.info(`Start Request: ${params[0]}`);\n    },\n    onSuccess: (result, params) => {\n      setState('');\n      message.success(`The username was changed to \"${params[0]}\" !`);\n    },\n    onError: (error) => {\n      message.error(error.message);\n    },\n    onFinally: (params, result, error) => {\n      message.info(`Request finish`);\n    },\n  });\n\n  return (\n    <div>\n      <input\n        onChange={(e) => setState(e.target.value)}\n        value={state}\n        placeholder=\"Please enter username\"\n        style={{ width: 240, marginRight: 16 }}\n      />\n      <button disabled={loading} type=\"button\" onClick={() => run(state)}>\n        {loading ? 'Loading' : 'Edit'}\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/basic/demo/manual-run.tsx",
    "content": "/**\n * title: Edit username\n * desc: In this example, we use `run(username)` to edit the username, and use `onSuccess` and `onError` to handle success and failure.\n *\n * title.zh-CN: 修改用户名\n * desc.zh-CN: 在这个例子中，我们通过 `run(username)` 来修改用户名，通过 `onSuccess` 和 `onError` 来处理成功和失败。\n */\n\nimport { message } from 'antd';\nimport { useState } from 'react';\nimport { useRequest } from 'ahooks';\n\nfunction editUsername(username: string): Promise<void> {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      if (Math.random() > 0.5) {\n        resolve();\n      } else {\n        reject(new Error('Failed to modify username'));\n      }\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const [state, setState] = useState('');\n\n  const { loading, run } = useRequest(editUsername, {\n    manual: true,\n    onSuccess: (result, params) => {\n      setState('');\n      message.success(`The username was changed to \"${params[0]}\" !`);\n    },\n    onError: (error) => {\n      message.error(error.message);\n    },\n  });\n\n  return (\n    <div>\n      <input\n        onChange={(e) => setState(e.target.value)}\n        value={state}\n        placeholder=\"Please enter username\"\n        style={{ width: 240, marginRight: 16 }}\n      />\n      <button disabled={loading} type=\"button\" onClick={() => run(state)}>\n        {loading ? 'Loading' : 'Edit'}\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/basic/demo/manual-runAsync.tsx",
    "content": "/**\n * title: Edit username\n * desc: In this example, we use `runAsync(username)` to edit the user name. At this time, we must catch the exception through catch.\n *\n * title.zh-CN: 修改用户名\n * desc.zh-CN: 在这个例子中，我们通过 `runAsync(username)` 来修改用户名，此时必须通过 catch 来自行处理异常。\n */\n\nimport { message } from 'antd';\nimport { useState } from 'react';\nimport { useRequest } from 'ahooks';\n\nfunction editUsername(username: string): Promise<void> {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      if (Math.random() > 0.5) {\n        resolve();\n      } else {\n        reject(new Error('Failed to modify username'));\n      }\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const [state, setState] = useState('');\n\n  const { loading, runAsync } = useRequest(editUsername, {\n    manual: true,\n  });\n\n  const onClick = async () => {\n    try {\n      await runAsync(state);\n      setState('');\n      message.success(`The username was changed to \"${state}\" !`);\n    } catch (error) {\n      message.error((error as Error).message);\n    }\n  };\n\n  return (\n    <div>\n      <input\n        onChange={(e) => setState(e.target.value)}\n        value={state}\n        placeholder=\"Please enter username\"\n        style={{ width: 240, marginRight: 16 }}\n      />\n      <button disabled={loading} type=\"button\" onClick={onClick}>\n        {loading ? 'Loading' : 'Edit'}\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/basic/demo/mutate.tsx",
    "content": "/**\n * title: Edit username\n *\n * title.zh-CN: 修改用户名\n */\n\nimport { message } from 'antd';\nimport { useState, useRef } from 'react';\nimport { useRequest } from 'ahooks';\nimport Mock from 'mockjs';\n\nfunction getUsername(): Promise<string> {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(Mock.mock('@name'));\n    }, 1000);\n  });\n}\n\nfunction editUsername(username: string): Promise<void> {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      if (Math.random() > 0.5) {\n        resolve();\n      } else {\n        reject(new Error('Failed to modify username'));\n      }\n    }, 1000);\n  });\n}\n\nexport default () => {\n  // store last username\n  const lastRef = useRef<string | undefined>(undefined);\n\n  const [state, setState] = useState('');\n\n  // get username\n  const { data: username, mutate } = useRequest(getUsername);\n\n  // edit username\n  const { run: edit } = useRequest(editUsername, {\n    manual: true,\n    onSuccess: (result, params) => {\n      setState('');\n      message.success(`The username was changed to \"${params[0]}\" !`);\n    },\n    onError: (error) => {\n      message.error(error.message);\n      mutate(lastRef.current);\n    },\n  });\n\n  const onChange = () => {\n    lastRef.current = username || undefined;\n    mutate(state);\n    edit(state);\n  };\n\n  return (\n    <div>\n      <p>Username: {username}</p>\n      <input\n        onChange={(e) => setState(e.target.value)}\n        value={state}\n        placeholder=\"Please enter username\"\n        style={{ width: 240, marginRight: 16 }}\n      />\n      <button type=\"button\" onClick={onChange}>\n        Edit\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/basic/demo/params.tsx",
    "content": "import { useRequest } from 'ahooks';\nimport Mock from 'mockjs';\nimport { useState } from 'react';\n\nfunction getUsername(id: string): Promise<string> {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(Mock.mock('@name'));\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const [state, setState] = useState('');\n\n  // get username\n  const {\n    data: username,\n    run,\n    params,\n  } = useRequest(getUsername, {\n    defaultParams: ['1'],\n  });\n\n  const onChange = () => {\n    run(state);\n  };\n\n  return (\n    <div>\n      <input\n        onChange={(e) => setState(e.target.value)}\n        value={state}\n        placeholder=\"Please enter userId\"\n        style={{ width: 240, marginRight: 16 }}\n      />\n      <button type=\"button\" onClick={onChange}>\n        GetUserName\n      </button>\n      <p style={{ marginTop: 8 }}>UserId: {params[0]}</p>\n      <p>Username: {username}</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/basic/demo/refresh.tsx",
    "content": "/**\n * title: Refresh username\n *\n * title.zh-CN: 刷新用户名称\n */\n\nimport { useRequest } from 'ahooks';\nimport Mock from 'mockjs';\nimport { useEffect } from 'react';\n\nfunction getUsername(id: number): Promise<string> {\n  console.log('use-request-refresh-id', id);\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(Mock.mock('@name'));\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const { data, loading, run, refresh } = useRequest((id: number) => getUsername(id), {\n    manual: true,\n  });\n\n  useEffect(() => {\n    run(1);\n  }, []);\n\n  if (loading) {\n    return <div>loading...</div>;\n  }\n  return (\n    <div>\n      <p>Username: {data}</p>\n      <button onClick={refresh} type=\"button\">\n        Refresh\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/cache/cache.en-US.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# Cache & SWR\n\nIf `options.cacheKey` is set, `useRequest` will cache the successful data . The next time the component is initialized, if there is cached data, we will return the cached data first, and then send a new request in background, which is the ability of SWR.\n\nYou can set the data retention time through `options.staleTime`. During this time, we consider the data to be fresh and will not re-initiate the request.\n\nYou can also set the data cache time through `options.cacheTime`, after this time, we will clear the cached data.\n\nNext, through a few examples to experience these features.\n\n### SWR\n\nIn the following example, we set the `cacheKey`. When the component is loaded for the second time, the cached content will be returned first, and then the request will be re-run in background. You can experience the effect by clicking the button.\n\n<code src=\"./demo/cacheKey.tsx\" />\n\n### Keep your data fresh\n\nBy setting `staleTime`, we can specify the data retention time, during which time the request will not be re-run. The following example sets a fresh time of 5s, you can experience the effect by clicking the button\n\n<code src=\"./demo/staleTime.tsx\" />\n\n### Data sharing\n\n> Note: If no new request is issued, the \"Data sharing\" will not be triggered. `cacheTime` and `staleTime` parameters will invalidate \"Data sharing\". [#2313](https://github.com/alibaba/hooks/issues/2313)\n\nThe content of the same `cacheKey` is shared globally, which will bring the following features:\n\n- Sharing request `Promise`: Only one of the same `cacheKey` will initiate a request at the same time, and the subsequent ones will share the same request `Promise`.\n- Data synchronization: When a request is made by one `cacheKey`, the contents of other identical `cacheKey` will be synchronized accordingly.\n\nIn the following example, the two components will only initiate one request during initialization. And the content of the two articles is always synchronized.\n\n<code src=\"./demo/share.tsx\" />\n\n### Parameters cache\n\nThe cached data includes `data` and `params`. Through the `params` caching mechanism, we can remember the conditions of the last request and initialize it next time.\n\nIn the following example, we can initialize the `keyword` from the cached `params`\n\n<code src=\"./demo/params.tsx\" />\n\n### Clear cache\n\nahooks provides a `clearCache` method, which can clear the cache data of the specified `cacheKey`.\n\n<code src=\"./demo/clearCache.tsx\" />\n\n### Custom cache\n\nBy setting `setCache` and `getCache`, you can customize the cache, for example, you can store data in `localStorage`, `IndexDB`, etc.\n\nPlease note:\n\n1. `setCache` and `getCache` need to be used together.\n2. In the custom cache mode, `cacheTime` and `clearCache` will be unused, please implement it yourself according to the actual situation.\n\n<code src=\"./demo/setCache.tsx\" />\n\n## API\n\n```ts\ninterface CachedData<TData, TParams> {\n  data: TData;\n  params: TParams;\n  time: number;\n}\n```\n\n### Options\n\n| Property  | Description                                                                                                                                                                                                                                   | Type                              | Default  |\n| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | -------- |\n| cacheKey  | A unique ID of the request. Data of the same `cacheKey` will synchronized globally (`cacheTime` and `staleTime` parameters will invalidate this mechanism, see demo: [Data sharing](#data-sharing))                                           | `string`                          | -        |\n| cacheTime | <ul><li> Set the cache time. By default, the cached data will be cleared after 5 minutes.</li><li> If set to `-1`, the cached data will never expire</li></ul>                                                                                | `number`                          | `300000` |\n| staleTime | <ul><li> Time to consider the cached data is fresh. Within this time interval, the request will not be re-initiated</li><li> If set to `-1`, it means that the data is always fresh</li></ul>                                                 | `number`                          | `0`      |\n| setCache  | <ul><li> Custom set cache </li><li> `setCache` and `getCache` need to be used together</li><li> In the custom cache mode, `cacheTime` and `clearCache` are useless, please implement it yourself according to the actual situation.</li></ul> | `(data: CachedData) => void;`     | -        |\n| getCache  | Custom get cache                                                                                                                                                                                                                              | `(params: TParams) => CachedData` | -        |\n\n### clearCache\n\n```tsx | pure\nimport { clearCache } from 'ahooks';\n\nclearCache(cacheKey?: string | string[]);\n```\n\n1. Support clearing a single cache, or a group of caches\n2. If `cacheKey` is empty, all cached data will be cleared\n\n## Remark\n\n- Only successful request data will be cached\n- Cached data includes `data` and `params`\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/cache/cache.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# 缓存 & SWR\n\n如果设置了 `options.cacheKey`，`useRequest` 会将当前请求成功的数据缓存起来。下次组件初始化时，如果有缓存数据，我们会优先返回缓存数据，然后在背后发送新请求，也就是 SWR 的能力。\n\n你可以通过 `options.staleTime` 设置数据保持新鲜时间，在该时间内，我们认为数据是新鲜的，不会重新发起请求。\n\n你也可以通过 `options.cacheTime` 设置数据缓存时间，超过该时间，我们会清空该条缓存数据。\n\n接下来通过几个例子来体验缓存这些功能。\n\n### SWR\n\n下面的示例，我们设置了 `cacheKey`，在组件第二次加载时，会优先返回缓存的内容，然后在背后重新发起请求。你可以通过点击按钮来体验效果。\n\n<code src=\"./demo/cacheKey.tsx\" />\n\n### 数据保持新鲜\n\n通过设置 `staleTime`，我们可以指定数据新鲜时间，在这个时间内，不会重新发起请求。下面的示例设置了 5s 的新鲜时间，你可以通过点击按钮来体验效果\n\n<code src=\"./demo/staleTime.tsx\" />\n\n### 数据共享\n\n> 注意：如果没有发起新请求，不会触发数据共享。`cacheTime`、`staleTime` 参数会使数据共享失效。[#2313](https://github.com/alibaba/hooks/issues/2313)\n\n同一个 `cacheKey` 的内容，在全局是共享的，这会带来以下几个特性：\n\n- 请求 `Promise` 共享：相同的 `cacheKey` 同时只会有一个在发起请求，后发起的会共用同一个请求 `Promise`\n- 数据同步：当某个 `cacheKey` 发起请求时，其它相同 `cacheKey` 的内容均会随之同步\n\n下面的示例中，初始化时，两个组件只会发起一个请求。并且两篇文章的内容永远是同步的。\n\n<code src=\"./demo/share.tsx\" />\n\n### 参数缓存\n\n缓存的数据包括 `data` 和 `params`，通过 `params` 缓存机制，我们可以记忆上一次请求的条件，并在下次初始化。\n\n下面的示例中，我们可以从缓存的 `params` 中初始化 `keyword`\n\n<code src=\"./demo/params.tsx\" />\n\n### 删除缓存\n\nahooks 提供了一个 `clearCache` 方法，可以清除指定 `cacheKey` 的缓存数据。\n\n<code src=\"./demo/clearCache.tsx\" />\n\n### 自定义缓存\n\n通过配置 `setCache` 和 `getCache`，可以自定义数据缓存，比如可以将数据存储到 `localStorage`、`IndexDB` 等。\n\n请注意：\n\n1. `setCache` 和 `getCache` 需要配套使用。\n2. 在自定义缓存模式下，`cacheTime` 和 `clearCache` 不会生效，请根据实际情况自行实现。\n\n<code src=\"./demo/setCache.tsx\" />\n\n## API\n\n```ts\ninterface CachedData<TData, TParams> {\n  data: TData;\n  params: TParams;\n  time: number;\n}\n```\n\n### Options\n\n| 参数      | 说明                                                                                                                                                                          | 类型                              | 默认值   |\n| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | -------- |\n| cacheKey  | 请求的唯一标识。相同 `cacheKey` 的数据全局同步（`cacheTime`、`staleTime` 参数会使该机制失效，见示例：[数据共享](#数据共享)）。                                                | `string`                          | -        |\n| cacheTime | <ul><li> 设置缓存数据回收时间。默认缓存数据 5 分钟后回收 </li><li> 如果设置为 `-1`, 则表示缓存数据永不过期</li></ul>                                                          | `number`                          | `300000` |\n| staleTime | <ul><li> 缓存数据保持新鲜时间。在该时间间隔内，认为数据是新鲜的，不会重新发请求 </li><li> 如果设置为 `-1`，则表示数据永远新鲜</li></ul>                                       | `number`                          | `0`      |\n| setCache  | <ul><li> 自定义设置缓存 </li><li> `setCache` 和 `getCache` 需要配套使用</li><li> 在自定义缓存模式下，`cacheTime` 和 `clearCache` 不会生效，请根据实际情况自行实现。</li></ul> | `(data: CachedData) => void;`     | -        |\n| getCache  | 自定义读取缓存                                                                                                                                                                | `(params: TParams) => CachedData` | -        |\n\n### clearCache\n\n```tsx | pure\nimport { clearCache } from 'ahooks';\n\nclearCache(cacheKey?: string | string[]);\n```\n\n1. 支持清空单个缓存，或一组缓存\n2. 如果 `cacheKey` 为空，则清空所有缓存数据\n\n## 备注\n\n- 只有成功的请求数据才会缓存\n- 缓存的数据包括 `data` 和 `params`\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/cache/demo/cacheKey.tsx",
    "content": "import { useBoolean } from 'ahooks';\nimport Mock from 'mockjs';\nimport React from 'react';\nimport { useRequest } from 'ahooks';\n\nconst getArticle = async () => {\n  return new Promise<{ data: string; time: number }>((resolve) => {\n    setTimeout(() => {\n      resolve({\n        data: Mock.mock('@paragraph'),\n        time: Date.now(),\n      });\n    }, 1000);\n  });\n};\n\nconst Article: React.FC = () => {\n  const { data, loading } = useRequest(getArticle, {\n    cacheKey: 'cacheKey-demo',\n  });\n  if (!data && loading) {\n    return <p>Loading</p>;\n  }\n  return (\n    <>\n      <p>Background loading: {loading ? 'true' : 'false'}</p>\n      <p>Latest request time: {data?.time}</p>\n      <p>{data?.data}</p>\n    </>\n  );\n};\n\nexport default () => {\n  const [state, { toggle }] = useBoolean();\n  return (\n    <div>\n      <button type=\"button\" onClick={() => toggle()}>\n        show/hidden\n      </button>\n      {state && <Article />}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/cache/demo/clearCache.tsx",
    "content": "import Mock from 'mockjs';\nimport { useRequest, clearCache, useBoolean } from 'ahooks';\nimport { message } from 'antd';\n\nconst getArticle = async () => {\n  return new Promise<{ data: string; time: number }>((resolve) => {\n    setTimeout(() => {\n      resolve({\n        data: Mock.mock('@paragraph'),\n        time: Date.now(),\n      });\n    }, 3000);\n  });\n};\n\nconst Article = ({ cacheKey }: { cacheKey: string }) => {\n  const { data, loading } = useRequest(getArticle, {\n    cacheKey,\n  });\n  if (!data && loading) {\n    return <p>Loading</p>;\n  }\n  return (\n    <>\n      <p>Background loading: {loading ? 'true' : 'false'}</p>\n      <p>Latest request time: {data?.time}</p>\n      <p>{data?.data}</p>\n    </>\n  );\n};\n\nconst clear = (cacheKey?: string | string[]) => {\n  clearCache(cacheKey);\n  const tips = Array.isArray(cacheKey) ? cacheKey.join('、') : cacheKey;\n  message.success(`Clear ${tips ?? 'All'} finished`);\n};\n\nexport default () => {\n  const [state, { toggle }] = useBoolean();\n  return (\n    <div>\n      <p>\n        <button type=\"button\" onClick={() => toggle()}>\n          show/hidden\n        </button>\n      </p>\n      <p>\n        <button style={{ marginRight: 8 }} onClick={() => clear('Article1')}>\n          Clear Article1\n        </button>\n        <button style={{ marginRight: 8 }} onClick={() => clear('Article2')}>\n          Clear Article2\n        </button>\n        <button style={{ marginRight: 8 }} onClick={() => clear(['Article2', 'Article3'])}>\n          Clear Article2 and Article3\n        </button>\n        <button onClick={() => clear()}>Clear All</button>\n      </p>\n      <h2>Article 1</h2>\n      {state && <Article cacheKey=\"Article1\" />}\n      <h2>Article 2</h2>\n      {state && <Article cacheKey=\"Article2\" />}\n      <h2>Article 3</h2>\n      {state && <Article cacheKey=\"Article3\" />}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/cache/demo/params.tsx",
    "content": "import { useBoolean } from 'ahooks';\nimport useRequest from '../../../';\nimport Mock from 'mockjs';\nimport { useState } from 'react';\n\nconst getArticle = async (keyword: string) => {\n  console.log('cacheKey', keyword);\n  return new Promise<{ data: string; time: number }>((resolve) => {\n    setTimeout(() => {\n      resolve({\n        data: Mock.mock('@paragraph'),\n        time: Date.now(),\n      });\n    }, 1000);\n  });\n};\n\nconst Article = () => {\n  const { data, params, loading, run } = useRequest(getArticle, {\n    cacheKey: 'cacheKey-demo',\n  });\n\n  const [keyword, setKeyword] = useState(params[0] || '');\n\n  if (!data && loading) {\n    return <p>Loading</p>;\n  }\n  return (\n    <>\n      <div>\n        <input\n          style={{ width: 300 }}\n          value={keyword}\n          onChange={(e) => setKeyword(e.target.value)}\n        />\n        <button\n          style={{ marginLeft: 8 }}\n          onClick={() => {\n            run(keyword);\n          }}\n        >\n          Get\n        </button>\n      </div>\n      <p>Background loading: {loading ? 'true' : 'false'}</p>\n      <p>Latest request time: {data?.time}</p>\n      <p>Keyword: {keyword}</p>\n      <p>{data?.data}</p>\n    </>\n  );\n};\n\nexport default () => {\n  const [state, { toggle }] = useBoolean();\n  return (\n    <div>\n      <button type=\"button\" onClick={() => toggle()} style={{ marginBottom: 16 }}>\n        show/hidden\n      </button>\n      {state && <Article />}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/cache/demo/setCache.tsx",
    "content": "import { useBoolean } from 'ahooks';\nimport Mock from 'mockjs';\nimport React from 'react';\nimport { useRequest } from 'ahooks';\n\nconst getArticle = async () => {\n  return new Promise<{ data: string; time: number }>((resolve) => {\n    setTimeout(() => {\n      resolve({\n        data: Mock.mock('@paragraph'),\n        time: Date.now(),\n      });\n    }, 1000);\n  });\n};\n\nconst cacheKey = 'setCache-demo';\n\nconst Article: React.FC = () => {\n  const { data, loading } = useRequest(getArticle, {\n    cacheKey,\n    setCache: (value) => localStorage.setItem(cacheKey, JSON.stringify(value)),\n    getCache: () => JSON.parse(localStorage.getItem(cacheKey) || '{}'),\n  });\n  if (!data && loading) {\n    return <p>Loading</p>;\n  }\n  return (\n    <>\n      <p>Background loading: {loading ? 'true' : 'false'}</p>\n      <p>Latest request time: {data?.time}</p>\n      <p>{data?.data}</p>\n    </>\n  );\n};\n\nexport default () => {\n  const [state, { toggle }] = useBoolean();\n  return (\n    <div>\n      <button type=\"button\" onClick={() => toggle()}>\n        show/hidden\n      </button>\n      {state && <Article />}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/cache/demo/share.tsx",
    "content": "import Mock from 'mockjs';\nimport { useRequest } from 'ahooks';\n\nconst getArticle = async () => {\n  console.log('cacheKey-share');\n  return new Promise<{ data: string; time: number }>((resolve) => {\n    setTimeout(() => {\n      resolve({\n        data: Mock.mock('@paragraph'),\n        time: Date.now(),\n      });\n    }, 3000);\n  });\n};\n\nconst Article = () => {\n  const { data, loading, refresh } = useRequest(getArticle, {\n    cacheKey: 'cacheKey-share',\n  });\n  if (!data && loading) {\n    return <p>Loading</p>;\n  }\n  return (\n    <>\n      <p>Background loading: {loading ? 'true' : 'false'}</p>\n      <p>\n        <button onClick={refresh} type=\"button\">\n          更新\n        </button>\n      </p>\n      <p>Latest request time: {data?.time}</p>\n      <p>{data?.data}</p>\n    </>\n  );\n};\n\nexport default () => {\n  return (\n    <div>\n      <h2>Article 1</h2>\n      <Article />\n      <h2>Article 2</h2>\n      <Article />\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/cache/demo/staleTime.tsx",
    "content": "import { useBoolean } from 'ahooks';\nimport Mock from 'mockjs';\nimport { useRequest } from 'ahooks';\n\nconst getArticle = async () => {\n  console.log('cacheKey-staleTime');\n  return new Promise<{ data: string; time: number }>((resolve) => {\n    setTimeout(() => {\n      resolve({\n        data: Mock.mock('@paragraph'),\n        time: Date.now(),\n      });\n    }, 1000);\n  });\n};\n\nconst Article = () => {\n  const { data, loading } = useRequest(getArticle, {\n    cacheKey: 'staleTime-demo',\n    staleTime: 5000,\n  });\n  if (!data && loading) {\n    return <p>Loading</p>;\n  }\n  return (\n    <>\n      <p>Background loading: {loading ? 'true' : 'false'}</p>\n      <p>Latest request time: {data?.time}</p>\n      <p>{data?.data}</p>\n    </>\n  );\n};\n\nexport default () => {\n  const [state, { toggle }] = useBoolean();\n  return (\n    <div>\n      <button type=\"button\" onClick={() => toggle()}>\n        show/hidden\n      </button>\n      {state && <Article />}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/debounce/debounce.en-US.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# Debounce\n\nEnter the debounce mode by setting `options.debounceWait`. At this time, if `run` or `runAsync` is triggered frequently, the request will be executed with the debounce strategy.\n\n```tsx | pure\nconst { data, run } = useRequest(getUsername, {\n  debounceWait: 300,\n  manual: true\n});\n```\n\nAs in the example code above, if `run` is triggered frequently, it will only wait for 300ms to execute after the last trigger.\n\nYou can quickly enter text in the input box below to experience the effect\n\n<code src=\"./demo/debounce.tsx\" />\n\n## API\n\n### Options\n\nThe usage and effect of all debounce property are the same as [lodash.debounce](https://lodash.com/docs/4.17.15#debounce)\n\n| Property         | Description                                                                  | Type      | Default Value |\n| ---------------- | ---------------------------------------------------------------------------- | --------- | ------------- |\n| debounceWait     | Debounce delay time, in milliseconds. After setting, enter the debounce mode | `number`  | -             |\n| debounceLeading  | Execute the request before the delay starts                                  | `boolean` | `false`       |\n| debounceTrailing | Execute the request after the delay ends                                     | `boolean` | `true`        |\n| debounceMaxWait  | The maximum time request is allowed to be delayed before it’s executed       | `number`  | -             |\n\n## Remark\n\n- `options.debounceWait`, `options.debounceLeading`, `options.debounceTrailing`, `options.debounceMaxWait` support dynamic changes.\n- `runAsync` will return a `Promise` when it is actually executed. When it is not executed, there will be no return.\n- `cancel` can abort a function waiting to be executed.\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/debounce/debounce.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# 防抖\n\n通过设置 `options.debounceWait`，进入防抖模式，此时如果频繁触发 `run` 或者 `runAsync`，则会以防抖策略进行请求。\n\n```tsx | pure\nconst { data, run } = useRequest(getUsername, {\n  debounceWait: 300,\n  manual: true\n});\n```\n\n如上示例代码，频繁触发 `run`，只会在最后一次触发结束后等待 300ms 执行。\n\n你可以在下面 input 框中快速输入文本，体验效果\n\n<code src=\"./demo/debounce.tsx\" />\n\n## API\n\n### Options\n\ndebounce 所有参数用法和效果同 [lodash.debounce](https://www.lodashjs.com/docs/lodash.debounce/)\n\n| 参数             | 说明                                           | 类型      | 默认值  |\n| ---------------- | ---------------------------------------------- | --------- | ------- |\n| debounceWait     | 防抖等待时间, 单位为毫秒，设置后，进入防抖模式 | `number`  | -       |\n| debounceLeading  | 在延迟开始前执行调用                           | `boolean` | `false` |\n| debounceTrailing | 在延迟结束后执行调用                           | `boolean` | `true`  |\n| debounceMaxWait  | 允许被延迟的最大值                             | `number`  | -       |\n\n## 备注\n\n- `options.debounceWait`、`options.debounceLeading`、`options.debounceTrailing`、`options.debounceMaxWait` 支持动态变化。\n- `runAsync` 在真正执行时，会返回 `Promise`。在未被执行时，不会有任何返回。\n- `cancel` 可以中止正在等待执行的函数。\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/debounce/demo/debounce.tsx",
    "content": "import { useRequest } from 'ahooks';\nimport Mock from 'mockjs';\nasync function getEmail(search?: string): Promise<string[]> {\n  console.log('debounce getEmail', search);\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(Mock.mock({ 'data|5': ['@email'] }).data);\n    }, 300);\n  });\n}\n\nexport default () => {\n  const { data, loading, run } = useRequest(getEmail, {\n    debounceWait: 1000,\n    manual: true,\n  });\n\n  return (\n    <div>\n      <input placeholder=\"Search Emails\" onChange={(e) => run(e.target.value)} />\n      {loading ? (\n        <p>loading</p>\n      ) : (\n        <ul style={{ marginTop: 8 }}>\n          {data?.map((i) => (\n            <li key={i}>{i}</li>\n          ))}\n        </ul>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/index/demo/default.tsx",
    "content": "/**\n * title: Read username\n *\n * title.zh-CN: 读取用户名称\n */\n\nimport { useRequest } from 'ahooks';\nimport Mock from 'mockjs';\nfunction getUsername(): Promise<string> {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(Mock.mock('@name'));\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const { data, error, loading } = useRequest(getUsername);\n\n  if (error) {\n    return <div>failed to load</div>;\n  }\n  if (loading) {\n    return <div>loading...</div>;\n  }\n  return <div>Username: {data}</div>;\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/index/demo/manual.tsx",
    "content": "/**\n * title: Edit username\n * desc: In this example, we modify the username.\n *\n * title.zh-CN: 修改用户名\n * desc.zh-CN: 在这个例子中，我们尝试修改用户名。\n */\n\nimport { message } from 'antd';\nimport { useState } from 'react';\nimport { useRequest } from 'ahooks';\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nfunction changeUsername(username: string): Promise<{ success: boolean }> {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve({ success: true });\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const [state, setState] = useState('');\n\n  const { loading, run } = useRequest(changeUsername, {\n    manual: true,\n    onSuccess: (result, params) => {\n      if (result.success) {\n        setState('');\n        message.success(`The username was changed to \"${params[0]}\" !`);\n      }\n    },\n  });\n\n  return (\n    <div>\n      <input\n        onChange={(e) => setState(e.target.value)}\n        value={state}\n        placeholder=\"Please enter username\"\n        style={{ width: 240, marginRight: 16 }}\n      />\n      <button disabled={loading} type=\"button\" onClick={() => run(state)}>\n        {loading ? 'Loading' : 'Edit'}\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/index/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# Quick Start\n\n`useRequest` is a powerful Hooks for asynchronous data management. `useRequest` is sufficient enough for network request scenarios in React projects.\n\n`useRequest` organizes code through a plug-in pattern, the core code is extremely simple, and can be easily extended for more advanced features. Current features include:\n\n- Automatic/manual request\n- Polling\n- Debounce\n- Throttle\n- Refresh on window focus\n- Error retry\n- Loading delay\n- SWR(stale-while-revalidate)\n- Caching\n\nNext, let's get to know `useRequest` from the two simplest examples.\n\n## Default usage\n\nThe first parameter of `useRequest` is an asynchronous function, which will be automatically triggered when the component is first loaded. At the same time, it automatically manages the status of `loading`, `data`, `error` of the asynchronous function.\n\n```js\nconst { data, error, loading } = useRequest(getUsername);\n```\n\n<br />\n\n<code src=\"./demo/default.tsx\" />\n\n## Manual trigger\n\nIf `options.manual = true` is set, useRequest will not be executed by default, and the execution needs to be triggered by `run`.\n\n```js\nconst { loading, run } = useRequest(changeUsername, {\n  manual: true\n});\n```\n\n<br />\n\n<code src=\"./demo/manual.tsx\" />\n\nIn the above two examples, we demonstrated the most basic usages of `useRequest`. Next, we will introduce the features of `useRequest` one by one.\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/index/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# 快速上手\n\n`useRequest` 是一个强大的异步数据管理的 Hooks，React 项目中的网络请求场景使用 `useRequest` 就够了。\n\n`useRequest` 通过插件式组织代码，核心代码极其简单，并且可以很方便的扩展出更高级的功能。目前已有能力包括：\n\n- 自动请求/手动请求\n- 轮询\n- 防抖\n- 节流\n- 屏幕聚焦重新请求\n- 错误重试\n- loading delay\n- SWR(stale-while-revalidate)\n- 缓存\n\n接下来让我们先从两个最简单的例子认识 `useRequest`。\n\n## 默认用法\n\n`useRequest` 的第一个参数是一个异步函数，在组件初次加载时，会自动触发该函数执行。同时自动管理该异步函数的 `loading` , `data` , `error` 等状态。\n\n```js\nconst { data, error, loading } = useRequest(getUsername);\n```\n\n<br />\n\n<code src=\"./demo/default.tsx\" />\n\n## 手动触发\n\n如果设置了 `options.manual = true`，则 useRequest 不会默认执行，需要通过 `run` 来触发执行。\n\n```js\nconst { loading, run } = useRequest(changeUsername, {\n  manual: true\n});\n```\n\n<br />\n\n<code src=\"./demo/manual.tsx\" />\n\n上面两个例子，我们演示了 `useRequest` 最基础的用法，接下来的我们开始逐个详细介绍 `useRequest` 的特性。\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/loadingDelay/demo/loadingDelay.tsx",
    "content": "import { useRequest } from 'ahooks';\nimport Mock from 'mockjs';\n\nfunction getUsername(): Promise<string> {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(Mock.mock('@name'));\n    }, 200);\n  });\n}\n\nexport default () => {\n  const action = useRequest(getUsername);\n\n  const withLoadingDelayAction = useRequest(getUsername, {\n    loadingDelay: 300,\n  });\n\n  const trigger = () => {\n    action.run();\n    withLoadingDelayAction.run();\n  };\n\n  return (\n    <div>\n      <button type=\"button\" onClick={trigger}>\n        run\n      </button>\n\n      <div style={{ margin: '24px 0', width: 300 }}>\n        Username: {action.loading ? 'Loading...' : action.data}\n      </div>\n      <div>\n        Username: {withLoadingDelayAction.loading ? 'Loading...' : withLoadingDelayAction.data}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/loadingDelay/loadingDelay.en-US.md",
    "content": "---\nnav:\n   path: /hooks\ngroup:\n   path: /use-request\n---\n\n# Loading Delay\n\nBy setting `options.loadingDelay`, you can delay the time when `loading` turns to `true`, effectively prevent UI flashing.\n\n```tsx | pure\nconst { loading, data } = useRequest(getUsername, {\n   loadingDelay: 300\n});\n\nreturn <div>{ loading ? 'Loading...' : data }</div>\n```\n\nFor example, in the above scenario, if `getUsername` returns within 300ms, `loading` will not become `true`, avoiding the page displays `Loading...`.\n\nYou can quickly click the button in the example below to experience the effect\n\n<code src=\"./demo/loadingDelay.tsx\" />\n\n## API\n\n| Property     | Description                                       | Type     | Default |\n| ------------ | ------------------------------------------------- | -------- | ------- |\n| loadingDelay | Set the delay time for `loading` to become `true` | `number` | `0`     |\n\n## Remark\n\n`options.loadingDelay` supports dynamic changes.\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/loadingDelay/loadingDelay.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# Loading Delay\n\n通过设置 `options.loadingDelay` ，可以延迟 `loading` 变成 `true` 的时间，有效防止闪烁。\n\n```tsx | pure\nconst { loading, data } = useRequest(getUsername, {\n  loadingDelay: 300\n});\n\nreturn <div>{ loading ? 'Loading...' : data }</div>\n```\n\n例如上面的场景，假如 `getUsername` 在 300ms 内返回，则 `loading` 不会变成 `true`，避免了页面展示 `Loading...` 的情况。\n\n你可以快速点击下面示例中的按钮以体验效果\n\n<code src=\"./demo/loadingDelay.tsx\" />\n\n## API\n\n| 参数         | 说明                                  | 类型     | 默认值 |\n| ------------ | ------------------------------------- | -------- | ------ |\n| loadingDelay | 设置 `loading` 变成 `true` 的延迟时间 | `number` | `0`    |\n\n## 备注\n\n`options.loadingDelay` 支持动态变化。\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/polling/demo/polling.tsx",
    "content": "import { useRequest } from 'ahooks';\nimport Mock from 'mockjs';\n\nfunction getUsername() {\n  console.log('polling getUsername');\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(Mock.mock('@name'));\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const { data, loading, run, cancel } = useRequest(getUsername, {\n    pollingInterval: 1000,\n    pollingWhenHidden: false,\n  });\n\n  return (\n    <>\n      <p>Username: {loading ? 'Loading' : String(data)}</p>\n      <button type=\"button\" onClick={run}>\n        start\n      </button>\n      <button type=\"button\" onClick={cancel} style={{ marginLeft: 16 }}>\n        stop\n      </button>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/polling/demo/pollingError.tsx",
    "content": "import { useRequest } from 'ahooks';\nimport Mock from 'mockjs';\nimport { message } from 'antd';\n\nfunction getUsername() {\n  console.log('polling getUsername Error');\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      reject(new Error(Mock.mock('@name')));\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const { data, loading, run, cancel } = useRequest(getUsername, {\n    pollingInterval: 1000,\n    pollingWhenHidden: false,\n    pollingErrorRetryCount: 3,\n    manual: true,\n    onError: (error) => {\n      message.error(error.message);\n    },\n  });\n\n  return (\n    <>\n      <p>Username: {loading ? 'Loading' : String(data)}</p>\n      <button type=\"button\" onClick={run}>\n        start\n      </button>\n      <button type=\"button\" onClick={cancel} style={{ marginLeft: 16 }}>\n        stop\n      </button>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/polling/polling.en-US.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# Polling\n\nBy setting `options.pollingInterval`, enter the polling mode, `useRequest` will periodically trigger service execution.\n\n```tsx | pure\nconst { data, run, cancel } = useRequest(getUsername, {\n  pollingInterval: 3000,\n});\n```\n\nFor example, in the above scenario, `getUsername` will be requested every 3000ms. You can stop polling by `cancel` and start polling by `run/runAsync`.\n\nYou can experience the effect through the following example\n\n<code src=\"./demo/polling.tsx\" />\n\n## Polling error retry\n\nPolling by `options. PollingErrorRetryCount` configuration error retry count.\n\n```tsx | pure\nconst { data, run, cancel } = useRequest(getUsername, {\n  pollingInterval: 3000,\n  pollingErrorRetryCount: 3,\n});\n```\n\nYou can experience the effect through the following example.\n\n<code src=\"./demo/pollingError.tsx\" />\n\n## API\n\n### Return\n\n| Property | Description   | Type                                     |\n| -------- | ------------- | ---------------------------------------- |\n| run      | Start polling | `(...params: TParams) => void`           |\n| runAsync | Start polling | `(...params: TParams) => Promise<TData>` |\n| cancel   | Stop polling  | `() => void`                             |\n\n### Options\n\n| Property               | Description                                                                                                                                                                  | Type      | Default |\n| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------- |\n| pollingInterval        | Polling interval, in milliseconds. If the value is greater than 0, the polling mode is activated.                                                                            | `number`  | `0`     |\n| pollingWhenHidden      | Whether to continue polling when the page is hidden. If set to false, polling will be temporarily paused when the page is hidden, and resume when the page is visible again. | `boolean` | `true`  |\n| pollingErrorRetryCount | Number of polling error retries. If set to -1, an infinite number of times                                                                                                   | `number`  | `-1`    |\n\n## Remark\n\n- `options.pollingInterval`, `options.pollingWhenHidden` support dynamic changes.\n- If you set `options.manual = true`, the initialization will not start polling, you need start it by `run/runAsync`.\n- If the `pollingInterval` changes from 0 to a value greater than 0, polling will not start automatically, and you need start it by `run/runAsync`.\n- The polling logic is to wait for `pollingInterval` time after each request is completed, and then initiate the next request.\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/polling/polling.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# 轮询\n\n通过设置 `options.pollingInterval`，进入轮询模式，`useRequest` 会定时触发 service 执行。\n\n```tsx | pure\nconst { data, run, cancel } = useRequest(getUsername, {\n  pollingInterval: 3000,\n});\n```\n\n例如上面的场景，会每隔 3000ms 请求一次 `getUsername`。同时你可以通过 `cancel` 来停止轮询，通过 `run/runAsync` 来启动轮询。\n\n你可以通过下面的示例来体验效果\n\n<code src=\"./demo/polling.tsx\" />\n\n## 轮询错误重试\n\n通过 `options.pollingErrorRetryCount` 轮询错误重试次数。\n\n```tsx | pure\nconst { data, run, cancel } = useRequest(getUsername, {\n  pollingInterval: 3000,\n  pollingErrorRetryCount: 3,\n});\n```\n\n你可以通过下面的示例来体验效果。\n\n<code src=\"./demo/pollingError.tsx\" />\n\n## API\n\n### Return\n\n| 参数     | 说明     | 类型                                     |\n| -------- | -------- | ---------------------------------------- |\n| run      | 启动轮询 | `(...params: TParams) => void`           |\n| runAsync | 启动轮询 | `(...params: TParams) => Promise<TData>` |\n| cancel   | 停止轮询 | `() => void`                             |\n\n### Options\n\n| 参数                   | 说明                                                                                                   | 类型      | 默认值 |\n| ---------------------- | ------------------------------------------------------------------------------------------------------ | --------- | ------ |\n| pollingInterval        | 轮询间隔，单位为毫秒。如果值大于 0，则处于轮询模式。                                                   | `number`  | `0`    |\n| pollingWhenHidden      | 在页面隐藏时，是否继续轮询。如果设置为 false，在页面隐藏时会暂时停止轮询，页面重新显示时继续上次轮询。 | `boolean` | `true` |\n| pollingErrorRetryCount | 轮询错误重试次数。如果设置为 -1，则无限次                                                              | `number`  | `-1`   |\n\n## 备注\n\n- `options.pollingInterval`、`options.pollingWhenHidden` 支持动态变化。\n- 如果设置 `options.manual = true`，则初始化不会启动轮询，需要通过 `run/runAsync` 触发开始。\n- 如果设置 `pollingInterval` 由 `0` 变成 `大于 0` 的值，不会启动轮询，需要通过 `run/runAsync` 触发开始。\n- 轮询原理是在每次请求完成后，等待 `pollingInterval` 时间，发起下一次请求。\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/ready/demo/manualReady.tsx",
    "content": "import { useRequest, useToggle } from 'ahooks';\nimport Mock from 'mockjs';\nfunction getUsername() {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(Mock.mock('@name'));\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const [ready, { toggle }] = useToggle(false);\n\n  const { data, loading, run } = useRequest(getUsername, {\n    ready,\n    manual: true,\n  });\n\n  return (\n    <>\n      <p>\n        Ready: {JSON.stringify(ready)}\n        <button onClick={toggle} style={{ marginLeft: 16 }}>\n          Toggle Ready\n        </button>\n      </p>\n      <p>\n        Username: {loading ? 'Loading' : String(data)}\n        <button type=\"button\" onClick={run} style={{ marginLeft: 16 }}>\n          run\n        </button>\n      </p>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/ready/demo/ready.tsx",
    "content": "import { useRequest, useToggle } from 'ahooks';\nimport Mock from 'mockjs';\nfunction getUsername() {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(Mock.mock('@name'));\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const [ready, { toggle }] = useToggle(false);\n\n  const { data, loading } = useRequest(getUsername, {\n    ready,\n  });\n\n  return (\n    <>\n      <p>\n        Ready: {JSON.stringify(ready)}\n        <button onClick={toggle} style={{ marginLeft: 16 }}>\n          Toggle Ready\n        </button>\n      </p>\n      <p>Username: {loading ? 'Loading' : String(data)}</p>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/ready/ready.en-US.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# Ready\n\nBy setting `options.ready`, you can control whether a request is sent. When its value is `false`, the request will never be sent.\n\nThe specific behavior is as follows:\n\n1. In the automatic mode of `manual=false`, every time `ready` changes from `false` to `true`, a request will be automatically executed with the parameter `options.defaultParams`.\n2. When `manual=true` manual request mode, as long as `ready=false`, the request triggered by `run/runAsync` will not be executed.\n\n## Automatic mode\n\nThe following example demonstrates the behavior of `ready` in automatic mode. Every time `ready` changes from `false` to `true`, the request will be executed.\n\n<code src=\"./demo/ready.tsx\" />\n\n## Manual mode\n\nThe following example demonstrates the behavior of `ready` in manual mode. Only when `ready` is equal to `true`, `run` will be executed.\n\n<code src=\"./demo/manualReady.tsx\" />\n\n## API\n\n### Options\n\n| Property | Description                  | Type      | Default |\n| -------- | ---------------------------- | --------- | ------- |\n| ready    | Is the current request ready | `boolean` | `true`  |\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/ready/ready.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# Ready\n\n通过设置 `options.ready`，可以控制请求是否发出。当其值为 `false` 时，请求永远都不会发出。\n\n其具体行为如下：\n\n1. 当 `manual=false` 自动请求模式时，每次 `ready` 从 `false` 变为 `true` 时，都会自动发起请求，会带上参数 `options.defaultParams`。\n2. 当 `manual=true` 手动请求模式时，只要 `ready=false`，则通过 `run/runAsync` 触发的请求都不会执行。\n\n## 自动模式\n\n以下示例演示了自动模式下 `ready` 的行为。每次 `ready` 从 `false` 变为 `true` 时，都会重新发起请求。\n\n<code src=\"./demo/ready.tsx\" />\n\n## 手动模式\n\n以下示例演示了手动模式下 `ready` 的行为。只有当 `ready` 等于 `true` 时，`run` 才会执行。\n\n<code src=\"./demo/manualReady.tsx\" />\n\n## API\n\n### Options\n\n| 参数  | 说明                 | 类型      | 默认值 |\n| ----- | -------------------- | --------- | ------ |\n| ready | 当前请求是否准备好了 | `boolean` | `true` |\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/refreshDeps/demo/refreshDeps.tsx",
    "content": "/**\n * title: Repeat last request\n * desc: When the dependency array changes, use the previous parameters to make the request again.\n *\n * title.zh-CN: 重复上一次请求\n * desc.zh-CN: 依赖数组变化时，使用上一次的参数重新发起请求。\n */\n\nimport { useState } from 'react';\nimport Mock from 'mockjs';\nimport { Space, Button } from 'antd';\nimport { useRequest } from 'ahooks';\n\nfunction getUsername(id: number): Promise<string> {\n  console.log('getUsername id:', id);\n\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(Mock.mock('@name'));\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const [userId, setUserId] = useState<number>();\n  const { data, loading, run } = useRequest((id: number) => getUsername(id), {\n    refreshDeps: [userId],\n  });\n\n  return (\n    <Space direction=\"vertical\">\n      <p>Username: {loading ? 'loading...' : data}</p>\n      <Button onClick={() => setUserId(Math.random())}>Use previous id to refresh</Button>\n      <Button onClick={() => run(Math.random())}>Use latest id to refresh</Button>\n    </Space>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/refreshDeps/demo/refreshDepsAction.tsx",
    "content": "/**\n * title: Custom refresh\n * desc: This example shows that when the dependency array changes, it checks the parameters' validity first and then makes a new request.\n *\n * title.zh-CN: 自定义刷新行为\n * desc.zh-CN: 该示例展示了当依赖数组变化时，首先校验参数合法性，然后发起新的请求。\n */\n\nimport { useState } from 'react';\nimport Mock from 'mockjs';\nimport isNumber from 'lodash/isNumber';\nimport { Button, Space } from 'antd';\nimport { useRequest } from 'ahooks';\n\nfunction getUsername(id: number): Promise<string> {\n  console.log('getUsername id:', id);\n\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(Mock.mock('@name'));\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const [userId, setUserId] = useState<number>();\n  const { data, loading, run } = useRequest((id: number) => getUsername(id), {\n    refreshDeps: [userId],\n    refreshDepsAction: () => {\n      if (!isNumber(userId)) {\n        console.log(\n          `parameter \"userId\" expected to be a number, but got ${typeof userId}.`,\n          userId,\n        );\n        return;\n      }\n      run(userId);\n    },\n  });\n\n  return (\n    <Space direction=\"vertical\">\n      <p>Username: {loading ? 'loading...' : data}</p>\n      <Button onClick={() => setUserId(Math.random())}>\n        Use latest id to refresh (by `refreshDeps`)\n      </Button>\n      <Button onClick={() => run(Math.random())}>Use latest id to refresh (by `run`)</Button>\n    </Space>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/refreshDeps/refresyDeps.en-US.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# RefreshDeps\n\nBy setting `options.refreshDeps`, `useRequest` will run [refresh](https://ahooks.js.org/hooks/use-request/basic/#result) automatically when dependencies change, achieving the effect of [Refresh (repeat the last request)](https://ahooks.js.org/hooks/use-request/basic/#refresh-repeat-the-last-request).\n\n```tsx | pure\nconst [userId, setUserId] = useState('1');\nconst { data, run } = useRequest(() => getUserSchool(userId), {\n  refreshDeps: [userId],\n});\n```\n\nIn the example code above, `useRequest` will execution when it is initialized and `userId` changes.\n\nIt is exactly the same with the following implementation\n\n```tsx | pure\nconst [userId, setUserId] = useState('1');\nconst { data, refresh } = useRequest(() => getUserSchool(userId));\n\nuseEffect(() => {\n  refresh();\n}, [userId]);\n```\n\n### Repeat last request\n\n<code src=\"./demo/refreshDeps.tsx\" />\n\n### Custom refresh\n\n<code src=\"./demo/refreshDepsAction.tsx\" />\n\n## API\n\n### Options\n\n| Property          | Description                                                                                                   | Type                   | Default |\n| ----------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------- | ------- |\n| refreshDeps       | When the content of the array changes, trigger refresh.                                                       | `React.DependencyList` | `[]`    |\n| refreshDepsAction | Customize the request behavior during dependency refresh; this parameter is invoked when dependencies change. | `() => void`           | -       |\n\n## Remark\n\n- If you set `options.manual = true`, both `refreshDeps` and `refreshDepsAction` are no longer effective, you need to trigger the request by `run/runAsync`.\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/refreshDeps/refresyDeps.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# 依赖刷新\n\n通过设置 `options.refreshDeps`，在依赖变化时， `useRequest` 会自动调用 [refresh](https://ahooks.js.org/zh-CN/hooks/use-request/basic/#result) 方法，实现[刷新（重复上一次请求）](https://ahooks.js.org/zh-CN/hooks/use-request/basic/#刷新重复上一次请求)的效果。\n\n```tsx | pure\nconst [userId, setUserId] = useState('1');\nconst { data, run } = useRequest(() => getUserSchool(userId), {\n  refreshDeps: [userId],\n});\n```\n\n上面的示例代码，`useRequest` 会在初始化和 `userId` 变化时，触发函数执行。\n\n与下面代码实现功能完全一致\n\n```tsx | pure\nconst [userId, setUserId] = useState('1');\nconst { data, refresh } = useRequest(() => getUserSchool(userId));\n\nuseEffect(() => {\n  refresh();\n}, [userId]);\n```\n\n### 重复上一次请求\n\n<code src=\"./demo/refreshDeps.tsx\" />\n\n### 自定义刷新行为\n\n<code src=\"./demo/refreshDepsAction.tsx\" />\n\n## API\n\n### Options\n\n| 参数              | 说明                                                                                                                                                       | 类型         | 默认值 |\n| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------ |\n| refreshDeps       | 依赖数组。当数组内容变化后[刷新（重复上一次请求）](https://ahooks.js.org/zh-CN/hooks/use-request/basic/#刷新重复上一次请求)。同 `useEffect` 的第二个参数。 | `any[]`      | `[]`   |\n| refreshDepsAction | 自定义依赖数组变化时的请求行为。                                                                                                                           | `() => void` | -      |\n\n## 备注\n\n- 如果设置 `options.manual = true`，则 `refreshDeps`, `refreshDepsAction` 都不再生效，需要通过 `run/runAsync` 手动触发请求。\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/refreshOnWindowFocus/demo/refreshOnWindowFocus.tsx",
    "content": "import Mock from 'mockjs';\nimport { useRequest } from 'ahooks';\n\nfunction getUsername() {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(Mock.mock('@name'));\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const { data, loading } = useRequest(getUsername, {\n    refreshOnWindowFocus: true,\n  });\n\n  return <div>Username: {loading ? 'Loading' : String(data)}</div>;\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/refreshOnWindowFocus/refreshOnWindowFocus.en-US.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# RefreshOnWindowFocus\n\nBy setting `options.refreshOnWindowFocus`, the request will be refreshed when the browser is `refocus` and `revisible`.\n\n```tsx | pure\nconst { data } = useRequest(getUsername, {\n  refreshOnWindowFocus: true,\n});\n```\n\nYou can click outside the browser, and then click the current page to experience the effect (or hide the current page and redisplay). If the interval from the previous request is greater than 5000ms, it will be requested again.\n\n<code src=\"./demo/refreshOnWindowFocus.tsx\" />\n\n## API\n\n### Options\n\n| Property             | Description                                                              | Type      | Default |\n| -------------------- | ------------------------------------------------------------------------ | --------- | ------- |\n| refreshOnWindowFocus | Whether to re-initiate the request when the screen refocus or revisible. | `boolean` | `false` |\n| focusTimespan        | Re-request interval, in milliseconds                                     | `number`  | `5000`  |\n\n## Remark\n\n- `options.refreshOnWindowFocus`, `options.focusTimespan` support dynamic changes.\n- Listen for browser events `visibilitychange` and `focus`.\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/refreshOnWindowFocus/refreshOnWindowFocus.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# 屏幕聚焦重新请求\n\n通过设置 `options.refreshOnWindowFocus`，在浏览器窗口 `refocus` 和 `revisible` 时，会重新发起请求。\n\n```tsx | pure\nconst { data } = useRequest(getUsername, {\n  refreshOnWindowFocus: true,\n});\n```\n\n你可以点击浏览器外部，再点击当前页面来体验效果（或者隐藏当前页面，重新展示），如果和上一次请求间隔大于 5000ms，则会重新请求一次。\n\n<code src=\"./demo/refreshOnWindowFocus.tsx\" />\n\n## API\n\n### Options\n\n| 参数                 | 说明                                         | 类型      | 默认值  |\n| -------------------- | -------------------------------------------- | --------- | ------- |\n| refreshOnWindowFocus | 在屏幕重新获取焦点或重新显示时，重新发起请求 | `boolean` | `false` |\n| focusTimespan        | 重新请求间隔，单位为毫秒                     | `number`  | `5000`  |\n\n## 备注\n\n- `options.refreshOnWindowFocus`、`options.focusTimespan` 支持动态变化。\n- 监听的浏览器事件为 `visibilitychange` 和 `focus`。\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/retry/demo/retry.tsx",
    "content": "import { useRequest } from 'ahooks';\nimport { useState } from 'react';\nimport { message } from 'antd';\n\nfunction editUsername(username: string) {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      reject(new Error('Failed to modify username'));\n    }, 1000);\n  });\n}\n\nexport default () => {\n  const [state, setState] = useState('');\n  const { loading, run } = useRequest(editUsername, {\n    retryCount: 3,\n    manual: true,\n    onError: (error) => {\n      message.error(error.message);\n    },\n  });\n\n  return (\n    <div>\n      <input\n        onChange={(e) => setState(e.target.value)}\n        value={state}\n        placeholder=\"Please enter username\"\n        style={{ width: 240, marginRight: 16 }}\n      />\n      <button disabled={loading} type=\"button\" onClick={() => run(state)}>\n        {loading ? 'Loading' : 'Edit'}\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/retry/retry.en-US.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# Error Retry\n\nBy setting `options.retryCount`, set the number of error retries, useRequest will retry after it fails.\n\n```tsx | pure\nconst { data, run } = useRequest(getUsername, {\n  retryCount: 3,\n});\n```\n\nAs in the example code above, after the request is failed, it will retry 3 times.\n\nYou can type text in the input box below and click the Edit button to experience the effect\n\n<code src=\"./demo/retry.tsx\" />\n\n## API\n\n### Options\n\n| Property      | Description                                                                                                                                                                                                                                                                                          | Type     | Default |\n| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------- |\n| retryCount    | The number of retries. If set to `-1`, it will try again indefinitely.                                                                                                                                                                                                                               | `number` | -       |\n| retryInterval | <ul><li>Retry interval in milliseconds. </li><li>If not set, the simple exponential backoff algorithm will be used by default, taking `1000 * 2 ** retryCount`, that is, waiting for 2s for the first retry, and 4s for the second retry. By analogy, if it is greater than 30s, take 30s </li></ul> | `number` | -       |\n\n## Remark\n\n- `options.retryCount`, `options.retryInterval` support dynamic changes.\n- `cancel` can cancel the ongoing retry behavior.\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/retry/retry.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# 错误重试\n\n通过设置 `options.retryCount`，指定错误重试次数，则 useRequest 在失败后会进行重试。\n\n```tsx | pure\nconst { data, run } = useRequest(getUsername, {\n  retryCount: 3,\n});\n```\n\n如上示例代码，在请求异常后，会做 3 次重试。\n\n你可以在下面 input 框中输入文本，并点击 Edit 按钮，体验效果\n\n<code src=\"./demo/retry.tsx\" />\n\n## API\n\n### Options\n\n| 参数          | 说明                                                                                                                                                                                                    | 类型     | 默认值 |\n| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------ |\n| retryCount    | 错误重试次数。如果设置为 `-1`，则无限次重试。                                                                                                                                                           | `number` | -      |\n| retryInterval | <ul><li>重试时间间隔，单位为毫秒。</li><li>如果不设置，默认采用简易的指数退避算法，取 `1000 * 2 ** retryCount`，也就是第一次重试等待 2s，第二次重试等待 4s，以此类推，如果大于 30s，则取 30s </li></ul> | `number` | -      |\n\n## 备注\n\n- `options.retryCount`、`options.retryInterval` 支持动态变化。\n- `cancel` 可以取消正在进行的重试行为。\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/throttle/demo/throttle.tsx",
    "content": "import { useRequest } from 'ahooks';\nimport Mock from 'mockjs';\nasync function getEmail(search?: string): Promise<string[]> {\n  console.log('throttle getEmail', search);\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(Mock.mock({ 'data|5': ['@email'] }).data);\n    }, 300);\n  });\n}\n\nexport default () => {\n  const { data, loading, run } = useRequest(getEmail, {\n    throttleWait: 1000,\n    manual: true,\n  });\n\n  return (\n    <div>\n      <input placeholder=\"Search Emails\" onChange={(e) => run(e.target.value)} />\n      {loading ? (\n        <p>loading</p>\n      ) : (\n        <ul style={{ marginTop: 8 }}>\n          {data?.map((i) => (\n            <li key={i}>{i}</li>\n          ))}\n        </ul>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/throttle/throttle.en-US.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# Throttle\n\nEnter the throttle mode by setting `options.throttleWait`. At this time, if `run` or `runAsync` is triggered frequently, the request will be executed with the throttle strategy.\n\n```tsx | pure\nconst { data, run } = useRequest(getUsername, {\n  throttleWait: 300,\n  manual: true\n});\n```\n\nAs in the example code above, if `run` is triggered frequently, it will only be executed once every 300ms.\n\nYou can quickly enter text in the input box below to experience the effect\n\n<code src=\"./demo/throttle.tsx\" />\n\n## API\n\n### Options\n\nThe usage and effects of all throttle property are the same as [lodash.throttle](https://lodash.com/docs/4.17.15#throttle)\n\n| Property         | Description                                                                 | Type      | Default Value |\n| ---------------- | --------------------------------------------------------------------------- | --------- | ------------- |\n| throttleWait     | Throttle wait time, in milliseconds. After setting, enter the throttle mode | `number`  | -             |\n| throttleLeading  | Execute the request before throttling starts                                | `boolean` | `true`        |\n| throttleTrailing | Execute the request after throttling ends                                   | `boolean` | `true`        |\n\n## Remark\n\n- `options.throttleWait`, `options.throttleLeading`, `options.throttleTrailing` support dynamic changes.\n- `runAsync` will return a `Promise` when it is actually executed. When it is not executed, there will be no return.\n- `cancel` can abort a function waiting to be executed.\n"
  },
  {
    "path": "packages/hooks/src/useRequest/doc/throttle/throttle.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /use-request\n---\n\n# 节流\n\n通过设置 `options.throttleWait`，进入节流模式，此时如果频繁触发 `run` 或者 `runAsync`，则会以节流策略进行请求。\n\n```tsx | pure\nconst { data, run } = useRequest(getUsername, {\n  throttleWait: 300,\n  manual: true\n});\n```\n\n如上示例代码，频繁触发 `run`，只会每隔 300ms 执行一次。\n\n你可以在下面 input 框中快速输入文本，体验效果\n\n<code src=\"./demo/throttle.tsx\" />\n\n## API\n\n### Options\n\nthrottle 所有参数用法和效果同 [lodash.throttle](https://www.lodashjs.com/docs/lodash.throttle/)\n\n| 参数             | 说明                                           | 类型      | 默认值 |\n| ---------------- | ---------------------------------------------- | --------- | ------ |\n| throttleWait     | 节流等待时间, 单位为毫秒，设置后，进入节流模式 | `number`  | -      |\n| throttleLeading  | 在节流开始前执行调用                           | `boolean` | `true` |\n| throttleTrailing | 在节流结束后执行调用                           | `boolean` | `true` |\n\n## 备注\n\n- `options.throttleWait`、`options.throttleLeading`、`options.throttleTrailing` 支持动态变化。\n- `runAsync` 在真正执行时，会返回 `Promise`。在未被执行时，不会有任何返回。\n- `cancel` 可以中止正在等待执行的函数。\n"
  },
  {
    "path": "packages/hooks/src/useRequest/index.ts",
    "content": "import useRequest from './src/useRequest';\nimport { clearCache } from './src/utils/cache';\n\nexport { clearCache };\n\nexport default useRequest;\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/Fetch.ts",
    "content": "/* eslint-disable @typescript-eslint/no-parameter-properties */\nimport type { RefObject } from 'react';\nimport { isFunction } from '../../utils';\nimport type { FetchState, Options, PluginReturn, Service, Subscribe } from './types';\n\nexport default class Fetch<TData, TParams extends any[]> {\n  pluginImpls: PluginReturn<TData, TParams>[] = [];\n\n  count: number = 0;\n\n  state: FetchState<TData, TParams> = {\n    loading: false,\n    params: undefined,\n    data: undefined,\n    error: undefined,\n  };\n\n  constructor(\n    public serviceRef: RefObject<Service<TData, TParams>>,\n    public options: Options<TData, TParams>,\n    public subscribe: Subscribe,\n    public initState: Partial<FetchState<TData, TParams>> = {},\n  ) {\n    this.state = {\n      ...this.state,\n      loading: !options.manual,\n      ...initState,\n    };\n  }\n\n  setState(s: Partial<FetchState<TData, TParams>> = {}) {\n    this.state = {\n      ...this.state,\n      ...s,\n    };\n    this.subscribe();\n  }\n\n  runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {\n    // @ts-ignore\n    const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);\n    return Object.assign({}, ...r);\n  }\n\n  async runAsync(...params: TParams): Promise<TData> {\n    this.count += 1;\n    const currentCount = this.count;\n\n    const {\n      stopNow = false,\n      returnNow = false,\n      ...state\n    } = this.runPluginHandler('onBefore', params);\n\n    // stop request\n    if (stopNow) {\n      return Promise.resolve(state.data);\n    }\n\n    this.setState({\n      loading: true,\n      params,\n      ...state,\n    });\n\n    // return now\n    if (returnNow) {\n      return Promise.resolve(state.data);\n    }\n\n    this.options.onBefore?.(params);\n\n    try {\n      // replace service\n      let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);\n\n      if (!servicePromise) {\n        servicePromise = this.serviceRef.current(...params);\n      }\n\n      const res = await servicePromise;\n\n      if (currentCount !== this.count) {\n        // prevent run.then when request is canceled\n        return new Promise(() => {});\n      }\n\n      // const formattedResult = this.options.formatResultRef.current ? this.options.formatResultRef.current(res) : res;\n\n      this.setState({\n        data: res,\n        error: undefined,\n        loading: false,\n      });\n\n      this.options.onSuccess?.(res, params);\n      this.runPluginHandler('onSuccess', res, params);\n\n      this.options.onFinally?.(params, res, undefined);\n\n      if (currentCount === this.count) {\n        this.runPluginHandler('onFinally', params, res, undefined);\n      }\n\n      return res;\n    } catch (error) {\n      if (currentCount !== this.count) {\n        // prevent run.then when request is canceled\n        return new Promise(() => {});\n      }\n\n      this.setState({\n        error: error as Error | undefined,\n        loading: false,\n      });\n\n      this.options.onError?.(error as Error, params);\n      this.runPluginHandler('onError', error, params);\n\n      this.options.onFinally?.(params, undefined, error as Error | undefined);\n\n      if (currentCount === this.count) {\n        this.runPluginHandler('onFinally', params, undefined, error);\n      }\n\n      throw error;\n    }\n  }\n\n  run(...params: TParams) {\n    this.runAsync(...params).catch((error) => {\n      if (!this.options.onError) {\n        console.error(error);\n      }\n    });\n  }\n\n  cancel() {\n    this.count += 1;\n    this.setState({\n      loading: false,\n    });\n\n    this.runPluginHandler('onCancel');\n  }\n\n  refresh() {\n    // @ts-ignore\n    this.run(...(this.state.params || []));\n  }\n\n  refreshAsync() {\n    // @ts-ignore\n    return this.runAsync(...(this.state.params || []));\n  }\n\n  mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {\n    const targetData = isFunction(data) ? data(this.state.data) : data;\n    this.runPluginHandler('onMutate', targetData);\n    this.setState({\n      data: targetData,\n    });\n  }\n}\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/plugins/useAutoRunPlugin.ts",
    "content": "import { useRef } from 'react';\nimport useUpdateEffect from '../../../useUpdateEffect';\nimport type { Plugin } from '../types';\n\n// support refreshDeps & ready\nconst useAutoRunPlugin: Plugin<any, any[]> = (\n  fetchInstance,\n  { manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction },\n) => {\n  const hasAutoRun = useRef<boolean>(false);\n  hasAutoRun.current = false;\n\n  useUpdateEffect(() => {\n    if (!manual && ready) {\n      hasAutoRun.current = true;\n      fetchInstance.run(...defaultParams);\n    }\n  }, [ready]);\n\n  useUpdateEffect(() => {\n    if (hasAutoRun.current) {\n      return;\n    }\n    if (!manual) {\n      hasAutoRun.current = true;\n      if (refreshDepsAction) {\n        refreshDepsAction();\n      } else {\n        fetchInstance.refresh();\n      }\n    }\n  }, [...refreshDeps]);\n\n  return {\n    onBefore: () => {\n      if (!ready) {\n        return {\n          stopNow: true,\n        };\n      }\n    },\n  };\n};\n\nuseAutoRunPlugin.onInit = ({ ready = true, manual }) => {\n  return {\n    loading: !manual && ready,\n  };\n};\n\nexport default useAutoRunPlugin;\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/plugins/useCachePlugin.ts",
    "content": "import { useRef } from 'react';\nimport useCreation from '../../../useCreation';\nimport useUnmount from '../../../useUnmount';\nimport type { Plugin } from '../types';\nimport { setCache, getCache } from '../utils/cache';\nimport type { CachedData } from '../utils/cache';\nimport { setCachePromise, getCachePromise } from '../utils/cachePromise';\nimport { trigger, subscribe } from '../utils/cacheSubscribe';\n\nconst useCachePlugin: Plugin<any, any[]> = (\n  fetchInstance,\n  {\n    cacheKey,\n    cacheTime = 5 * 60 * 1000,\n    staleTime = 0,\n    setCache: customSetCache,\n    getCache: customGetCache,\n  },\n) => {\n  const unSubscribeRef = useRef<() => void>(undefined);\n\n  const currentPromiseRef = useRef<Promise<any>>(undefined);\n\n  const _setCache = (key: string, cachedData: CachedData) => {\n    if (customSetCache) {\n      customSetCache(cachedData);\n    } else {\n      setCache(key, cacheTime, cachedData);\n    }\n    trigger(key, cachedData.data);\n  };\n\n  const _getCache = (key: string, params: any[] = []) => {\n    if (customGetCache) {\n      return customGetCache(params);\n    }\n    return getCache(key);\n  };\n\n  useCreation(() => {\n    if (!cacheKey) {\n      return;\n    }\n\n    // get data from cache when init\n    const cacheData = _getCache(cacheKey);\n    if (cacheData && Object.hasOwnProperty.call(cacheData, 'data')) {\n      fetchInstance.state.data = cacheData.data;\n      fetchInstance.state.params = cacheData.params;\n      if (staleTime === -1 || Date.now() - cacheData.time <= staleTime) {\n        fetchInstance.state.loading = false;\n      }\n    }\n\n    // subscribe same cachekey update, trigger update\n    unSubscribeRef.current = subscribe(cacheKey, (data) => {\n      fetchInstance.setState({ data });\n    });\n  }, []);\n\n  useUnmount(() => {\n    unSubscribeRef.current?.();\n  });\n\n  if (!cacheKey) {\n    return {};\n  }\n\n  return {\n    onBefore: (params) => {\n      const cacheData = _getCache(cacheKey, params);\n\n      if (!cacheData || !Object.hasOwnProperty.call(cacheData, 'data')) {\n        return {};\n      }\n\n      // If the data is fresh, stop request\n      if (staleTime === -1 || Date.now() - cacheData.time <= staleTime) {\n        return {\n          loading: false,\n          data: cacheData?.data,\n          error: undefined,\n          returnNow: true,\n        };\n      } else {\n        // If the data is stale, return data, and request continue\n        return {\n          data: cacheData?.data,\n          error: undefined,\n        };\n      }\n    },\n    onRequest: (service, args) => {\n      let servicePromise = getCachePromise(cacheKey);\n\n      // If has servicePromise, and is not trigger by self, then use it\n      if (servicePromise && servicePromise !== currentPromiseRef.current) {\n        return { servicePromise };\n      }\n\n      servicePromise = service(...args);\n      currentPromiseRef.current = servicePromise;\n      setCachePromise(cacheKey, servicePromise);\n      return { servicePromise };\n    },\n    onSuccess: (data, params) => {\n      if (cacheKey) {\n        // cancel subscribe, avoid trgger self\n        unSubscribeRef.current?.();\n        _setCache(cacheKey, {\n          data,\n          params,\n          time: Date.now(),\n        });\n        // resubscribe\n        unSubscribeRef.current = subscribe(cacheKey, (d) => {\n          fetchInstance.setState({ data: d });\n        });\n      }\n    },\n    onMutate: (data) => {\n      if (cacheKey) {\n        // cancel subscribe, avoid trigger self\n        unSubscribeRef.current?.();\n        _setCache(cacheKey, {\n          data,\n          params: fetchInstance.state.params,\n          time: Date.now(),\n        });\n        // resubscribe\n        unSubscribeRef.current = subscribe(cacheKey, (d) => {\n          fetchInstance.setState({ data: d });\n        });\n      }\n    },\n  };\n};\n\nexport default useCachePlugin;\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/plugins/useDebouncePlugin.ts",
    "content": "import type { DebouncedFunc, DebounceSettings } from 'lodash';\nimport debounce from 'lodash/debounce';\nimport { useEffect, useMemo, useRef } from 'react';\nimport type { Plugin } from '../types';\n\nconst useDebouncePlugin: Plugin<any, any[]> = (\n  fetchInstance,\n  { debounceWait, debounceLeading, debounceTrailing, debounceMaxWait },\n) => {\n  const debouncedRef = useRef<DebouncedFunc<any>>(undefined);\n\n  const options = useMemo(() => {\n    const ret: DebounceSettings = {};\n    if (debounceLeading !== undefined) {\n      ret.leading = debounceLeading;\n    }\n    if (debounceTrailing !== undefined) {\n      ret.trailing = debounceTrailing;\n    }\n    if (debounceMaxWait !== undefined) {\n      ret.maxWait = debounceMaxWait;\n    }\n    return ret;\n  }, [debounceLeading, debounceTrailing, debounceMaxWait]);\n\n  useEffect(() => {\n    if (debounceWait) {\n      const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);\n\n      debouncedRef.current = debounce(\n        (callback: (...args: any[]) => void) => {\n          callback();\n        },\n        debounceWait,\n        options,\n      );\n\n      // debounce runAsync should be promise\n      // https://github.com/lodash/lodash/issues/4400#issuecomment-834800398\n      fetchInstance.runAsync = (...args) => {\n        return new Promise<void>((resolve, reject) => {\n          debouncedRef.current?.(() => {\n            _originRunAsync(...args)\n              .then(resolve)\n              .catch(reject);\n          });\n        });\n      };\n\n      return () => {\n        debouncedRef.current?.cancel();\n        fetchInstance.runAsync = _originRunAsync;\n      };\n    }\n  }, [debounceWait, options]);\n\n  if (!debounceWait) {\n    return {};\n  }\n\n  return {\n    onCancel: () => {\n      debouncedRef.current?.cancel();\n    },\n  };\n};\n\nexport default useDebouncePlugin;\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/plugins/useLoadingDelayPlugin.ts",
    "content": "import { useRef } from 'react';\nimport type { Plugin, Timeout } from '../types';\n\nconst useLoadingDelayPlugin: Plugin<any, any[]> = (fetchInstance, { loadingDelay, ready }) => {\n  const timerRef = useRef<Timeout>(undefined);\n\n  if (!loadingDelay) {\n    return {};\n  }\n\n  const cancelTimeout = () => {\n    if (timerRef.current) {\n      clearTimeout(timerRef.current);\n    }\n  };\n\n  return {\n    onBefore: () => {\n      cancelTimeout();\n\n      // Two cases:\n      // 1. ready === undefined\n      // 2. ready === true\n      if (ready !== false) {\n        timerRef.current = setTimeout(() => {\n          fetchInstance.setState({\n            loading: true,\n          });\n        }, loadingDelay);\n      }\n\n      return {\n        loading: false,\n      };\n    },\n    onFinally: () => {\n      cancelTimeout();\n    },\n    onCancel: () => {\n      cancelTimeout();\n    },\n  };\n};\n\nexport default useLoadingDelayPlugin;\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/plugins/usePollingPlugin.ts",
    "content": "import { useRef } from 'react';\nimport useUpdateEffect from '../../../useUpdateEffect';\nimport type { Plugin, Timeout } from '../types';\nimport isDocumentVisible from '../utils/isDocumentVisible';\nimport subscribeReVisible from '../utils/subscribeReVisible';\n\nconst usePollingPlugin: Plugin<any, any[]> = (\n  fetchInstance,\n  { pollingInterval, pollingWhenHidden = true, pollingErrorRetryCount = -1 },\n) => {\n  const timerRef = useRef<Timeout>(undefined);\n  const unsubscribeRef = useRef<() => void>(undefined);\n  const countRef = useRef<number>(0);\n\n  const stopPolling = () => {\n    if (timerRef.current) {\n      clearTimeout(timerRef.current);\n    }\n    unsubscribeRef.current?.();\n  };\n\n  useUpdateEffect(() => {\n    if (!pollingInterval) {\n      stopPolling();\n    }\n  }, [pollingInterval]);\n\n  if (!pollingInterval) {\n    return {};\n  }\n\n  return {\n    onBefore: () => {\n      stopPolling();\n    },\n    onError: () => {\n      countRef.current += 1;\n    },\n    onSuccess: () => {\n      countRef.current = 0;\n    },\n    onFinally: () => {\n      if (\n        pollingErrorRetryCount === -1 ||\n        // When an error occurs, the request is not repeated after pollingErrorRetryCount retries\n        (pollingErrorRetryCount !== -1 && countRef.current <= pollingErrorRetryCount)\n      ) {\n        timerRef.current = setTimeout(() => {\n          // if pollingWhenHidden = false && document is hidden, then stop polling and subscribe revisible\n          if (!pollingWhenHidden && !isDocumentVisible()) {\n            unsubscribeRef.current = subscribeReVisible(() => {\n              fetchInstance.refresh();\n            });\n          } else {\n            fetchInstance.refresh();\n          }\n        }, pollingInterval);\n      } else {\n        countRef.current = 0;\n      }\n    },\n    onCancel: () => {\n      stopPolling();\n    },\n  };\n};\n\nexport default usePollingPlugin;\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/plugins/useRefreshOnWindowFocusPlugin.ts",
    "content": "import { useEffect, useRef } from 'react';\nimport useUnmount from '../../../useUnmount';\nimport type { Plugin } from '../types';\nimport limit from '../utils/limit';\nimport subscribeFocus from '../utils/subscribeFocus';\n\nconst useRefreshOnWindowFocusPlugin: Plugin<any, any[]> = (\n  fetchInstance,\n  { refreshOnWindowFocus, focusTimespan = 5000 },\n) => {\n  const unsubscribeRef = useRef<() => void>(undefined);\n\n  const stopSubscribe = () => {\n    unsubscribeRef.current?.();\n  };\n\n  useEffect(() => {\n    if (refreshOnWindowFocus) {\n      const limitRefresh = limit(fetchInstance.refresh.bind(fetchInstance), focusTimespan);\n      unsubscribeRef.current = subscribeFocus(() => {\n        limitRefresh();\n      });\n    }\n    return () => {\n      stopSubscribe();\n    };\n  }, [refreshOnWindowFocus, focusTimespan]);\n\n  useUnmount(() => {\n    stopSubscribe();\n  });\n\n  return {};\n};\n\nexport default useRefreshOnWindowFocusPlugin;\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/plugins/useRetryPlugin.ts",
    "content": "import { useRef } from 'react';\nimport type { Plugin } from '../types';\n\nconst useRetryPlugin: Plugin<any, any[]> = (fetchInstance, { retryInterval, retryCount }) => {\n  const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);\n  const countRef = useRef<number>(0);\n\n  const triggerByRetry = useRef(false);\n\n  if (!retryCount) {\n    return {};\n  }\n\n  return {\n    onBefore: () => {\n      if (!triggerByRetry.current) {\n        countRef.current = 0;\n      }\n      triggerByRetry.current = false;\n\n      if (timerRef.current) {\n        clearTimeout(timerRef.current);\n      }\n    },\n    onSuccess: () => {\n      countRef.current = 0;\n    },\n    onError: () => {\n      countRef.current += 1;\n      if (retryCount === -1 || countRef.current <= retryCount) {\n        // Exponential backoff\n        const timeout = retryInterval ?? Math.min(1000 * 2 ** countRef.current, 30000);\n        timerRef.current = setTimeout(() => {\n          triggerByRetry.current = true;\n          fetchInstance.refresh();\n        }, timeout);\n      } else {\n        countRef.current = 0;\n      }\n    },\n    onCancel: () => {\n      countRef.current = 0;\n      if (timerRef.current) {\n        clearTimeout(timerRef.current);\n      }\n    },\n  };\n};\n\nexport default useRetryPlugin;\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/plugins/useThrottlePlugin.ts",
    "content": "import type { DebouncedFunc, ThrottleSettings } from 'lodash';\nimport throttle from 'lodash/throttle';\nimport { useEffect, useRef } from 'react';\nimport type { Plugin } from '../types';\n\nconst useThrottlePlugin: Plugin<any, any[]> = (\n  fetchInstance,\n  { throttleWait, throttleLeading, throttleTrailing },\n) => {\n  const throttledRef = useRef<DebouncedFunc<any>>(undefined);\n\n  const options: ThrottleSettings = {};\n\n  if (throttleLeading !== undefined) {\n    options.leading = throttleLeading;\n  }\n  if (throttleTrailing !== undefined) {\n    options.trailing = throttleTrailing;\n  }\n\n  useEffect(() => {\n    if (throttleWait) {\n      const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);\n\n      throttledRef.current = throttle(\n        (callback) => {\n          callback();\n        },\n        throttleWait,\n        options,\n      );\n\n      // throttle runAsync should be promise\n      // https://github.com/lodash/lodash/issues/4400#issuecomment-834800398\n      fetchInstance.runAsync = (...args) => {\n        return new Promise((resolve, reject) => {\n          throttledRef.current?.(() => {\n            _originRunAsync(...args)\n              .then(resolve)\n              .catch(reject);\n          });\n        });\n      };\n\n      return () => {\n        fetchInstance.runAsync = _originRunAsync;\n        throttledRef.current?.cancel();\n      };\n    }\n  }, [throttleWait, throttleLeading, throttleTrailing]);\n\n  if (!throttleWait) {\n    return {};\n  }\n\n  return {\n    onCancel: () => {\n      throttledRef.current?.cancel();\n    },\n  };\n};\n\nexport default useThrottlePlugin;\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/types.ts",
    "content": "import type { DependencyList } from 'react';\nimport type Fetch from './Fetch';\nimport type { CachedData } from './utils/cache';\n\nexport type Service<TData, TParams extends any[]> = (...args: TParams) => Promise<TData>;\nexport type Subscribe = () => void;\n\n// for Fetch\n\nexport interface FetchState<TData, TParams extends any[]> {\n  loading: boolean;\n  params?: TParams;\n  data?: TData;\n  error?: Error;\n}\n\nexport interface PluginReturn<TData, TParams extends any[]> {\n  onBefore?: (params: TParams) =>\n    | ({\n        stopNow?: boolean;\n        returnNow?: boolean;\n      } & Partial<FetchState<TData, TParams>>)\n    | void;\n\n  onRequest?: (\n    service: Service<TData, TParams>,\n    params: TParams,\n  ) => {\n    servicePromise?: Promise<TData>;\n  };\n\n  onSuccess?: (data: TData, params: TParams) => void;\n  onError?: (e: Error, params: TParams) => void;\n  onFinally?: (params: TParams, data?: TData, e?: Error) => void;\n  onCancel?: () => void;\n  onMutate?: (data: TData) => void;\n}\n\n// for useRequestImplement\n\nexport interface Options<TData, TParams extends any[]> {\n  manual?: boolean;\n\n  onBefore?: (params: TParams) => void;\n  onSuccess?: (data: TData, params: TParams) => void;\n  onError?: (e: Error, params: TParams) => void;\n  // formatResult?: (res: any) => TData;\n  onFinally?: (params: TParams, data?: TData, e?: Error) => void;\n\n  defaultParams?: TParams;\n\n  // refreshDeps\n  refreshDeps?: DependencyList;\n  refreshDepsAction?: () => void;\n\n  // loading delay\n  loadingDelay?: number;\n\n  // polling\n  pollingInterval?: number;\n  pollingWhenHidden?: boolean;\n  pollingErrorRetryCount?: number;\n\n  // refresh on window focus\n  refreshOnWindowFocus?: boolean;\n  focusTimespan?: number;\n\n  // debounce\n  debounceWait?: number;\n  debounceLeading?: boolean;\n  debounceTrailing?: boolean;\n  debounceMaxWait?: number;\n\n  // throttle\n  throttleWait?: number;\n  throttleLeading?: boolean;\n  throttleTrailing?: boolean;\n\n  // cache\n  cacheKey?: string;\n  cacheTime?: number;\n  staleTime?: number;\n  setCache?: (data: CachedData<TData, TParams>) => void;\n  getCache?: (params: TParams) => CachedData<TData, TParams> | undefined;\n\n  // retry\n  retryCount?: number;\n  retryInterval?: number;\n\n  // ready\n  ready?: boolean;\n\n  // [key: string]: any;\n}\n\nexport type Plugin<TData, TParams extends any[]> = {\n  (\n    fetchInstance: Fetch<TData, TParams>,\n    options: Options<TData, TParams>,\n  ): PluginReturn<TData, TParams>;\n  onInit?: (options: Options<TData, TParams>) => Partial<FetchState<TData, TParams>>;\n};\n\n// for index\n// export type OptionsWithoutFormat<TData, TParams extends any[]> = Omit<Options<TData, TParams>, 'formatResult'>;\n\n// export interface OptionsWithFormat<TData, TParams extends any[], TFormated, TTFormated extends TFormated = any> extends Omit<Options<TTFormated, TParams>, 'formatResult'> {\n//   formatResult: (res: TData) => TFormated;\n// };\n\nexport interface Result<TData, TParams extends any[]> {\n  loading: boolean;\n  data?: TData;\n  error?: Error;\n  params: TParams | [];\n  cancel: Fetch<TData, TParams>['cancel'];\n  refresh: Fetch<TData, TParams>['refresh'];\n  refreshAsync: Fetch<TData, TParams>['refreshAsync'];\n  run: Fetch<TData, TParams>['run'];\n  runAsync: Fetch<TData, TParams>['runAsync'];\n  mutate: Fetch<TData, TParams>['mutate'];\n}\n\nexport type Timeout = ReturnType<typeof setTimeout>;\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/useRequest.ts",
    "content": "import useAutoRunPlugin from './plugins/useAutoRunPlugin';\nimport useCachePlugin from './plugins/useCachePlugin';\nimport useDebouncePlugin from './plugins/useDebouncePlugin';\nimport useLoadingDelayPlugin from './plugins/useLoadingDelayPlugin';\nimport usePollingPlugin from './plugins/usePollingPlugin';\nimport useRefreshOnWindowFocusPlugin from './plugins/useRefreshOnWindowFocusPlugin';\nimport useRetryPlugin from './plugins/useRetryPlugin';\nimport useThrottlePlugin from './plugins/useThrottlePlugin';\nimport type { Options, Plugin, Service } from './types';\nimport useRequestImplement from './useRequestImplement';\n\n// function useRequest<TData, TParams extends any[], TFormated, TTFormated extends TFormated = any>(\n//   service: Service<TData, TParams>,\n//   options: OptionsWithFormat<TData, TParams, TFormated, TTFormated>,\n//   plugins?: Plugin<TData, TParams>[],\n// ): Result<TFormated, TParams>\n// function useRequest<TData, TParams extends any[]>(\n//   service: Service<TData, TParams>,\n//   options?: OptionsWithoutFormat<TData, TParams>,\n//   plugins?: Plugin<TData, TParams>[],\n// ): Result<TData, TParams>\nfunction useRequest<TData, TParams extends any[]>(\n  service: Service<TData, TParams>,\n  options?: Options<TData, TParams>,\n  plugins?: Plugin<TData, TParams>[],\n) {\n  return useRequestImplement<TData, TParams>(service, options, [\n    ...(plugins || []),\n    useDebouncePlugin,\n    useLoadingDelayPlugin,\n    usePollingPlugin,\n    useRefreshOnWindowFocusPlugin,\n    useThrottlePlugin,\n    useAutoRunPlugin,\n    useCachePlugin,\n    useRetryPlugin,\n  ] as Plugin<TData, TParams>[]);\n}\n\nexport default useRequest;\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/useRequestImplement.ts",
    "content": "import useCreation from '../../useCreation';\nimport useLatest from '../../useLatest';\nimport useMemoizedFn from '../../useMemoizedFn';\nimport useMount from '../../useMount';\nimport useUnmount from '../../useUnmount';\nimport useUpdate from '../../useUpdate';\nimport isDev from '../../utils/isDev';\n\nimport Fetch from './Fetch';\nimport type { Options, Plugin, Result, Service } from './types';\n\nfunction useRequestImplement<TData, TParams extends any[]>(\n  service: Service<TData, TParams>,\n  options: Options<TData, TParams> = {},\n  plugins: Plugin<TData, TParams>[] = [],\n) {\n  const { manual = false, ready = true, ...rest } = options;\n\n  if (isDev) {\n    if (options.defaultParams && !Array.isArray(options.defaultParams)) {\n      console.warn(`expected defaultParams is array, got ${typeof options.defaultParams}`);\n    }\n  }\n\n  const fetchOptions = {\n    manual,\n    ready,\n    ...rest,\n  };\n\n  const serviceRef = useLatest(service);\n\n  const update = useUpdate();\n\n  const fetchInstance = useCreation(() => {\n    const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);\n\n    return new Fetch<TData, TParams>(\n      serviceRef,\n      fetchOptions,\n      update,\n      Object.assign({}, ...initState),\n    );\n  }, []);\n  fetchInstance.options = fetchOptions;\n  // run all plugins hooks\n  fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));\n\n  useMount(() => {\n    if (!manual && ready) {\n      // useCachePlugin can set fetchInstance.state.params from cache when init\n      const params = fetchInstance.state.params || options.defaultParams || [];\n      // @ts-ignore\n      fetchInstance.run(...params);\n    }\n  });\n\n  useUnmount(() => {\n    fetchInstance.cancel();\n  });\n\n  return {\n    loading: fetchInstance.state.loading,\n    data: fetchInstance.state.data,\n    error: fetchInstance.state.error,\n    params: fetchInstance.state.params || [],\n    cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)),\n    refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)),\n    refreshAsync: useMemoizedFn(fetchInstance.refreshAsync.bind(fetchInstance)),\n    run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)),\n    runAsync: useMemoizedFn(fetchInstance.runAsync.bind(fetchInstance)),\n    mutate: useMemoizedFn(fetchInstance.mutate.bind(fetchInstance)),\n  } as Result<TData, TParams>;\n}\n\nexport default useRequestImplement;\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/utils/cache.ts",
    "content": "type Timer = ReturnType<typeof setTimeout>;\ntype CachedKey = string | number;\n\nexport interface CachedData<TData = any, TParams = any> {\n  data: TData;\n  params: TParams;\n  time: number;\n}\ninterface RecordData extends CachedData {\n  timer: Timer | undefined;\n}\n\nconst cache = new Map<CachedKey, RecordData>();\n\nconst setCache = (key: CachedKey, cacheTime: number, cachedData: CachedData) => {\n  const currentCache = cache.get(key);\n  if (currentCache?.timer) {\n    clearTimeout(currentCache.timer);\n  }\n\n  let timer: Timer | undefined = undefined;\n\n  if (cacheTime > -1) {\n    // if cache out, clear it\n    timer = setTimeout(() => {\n      cache.delete(key);\n    }, cacheTime);\n  }\n\n  cache.set(key, {\n    ...cachedData,\n    timer,\n  });\n};\n\nconst getCache = (key: CachedKey) => {\n  return cache.get(key);\n};\n\nconst clearCache = (key?: string | string[]) => {\n  if (key) {\n    const cacheKeys = Array.isArray(key) ? key : [key];\n    cacheKeys.forEach((cacheKey) => cache.delete(cacheKey));\n  } else {\n    cache.clear();\n  }\n};\n\nexport { getCache, setCache, clearCache };\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/utils/cachePromise.ts",
    "content": "type CachedKey = string | number;\nconst cachePromise = new Map<CachedKey, Promise<any>>();\n\nconst getCachePromise = (cacheKey: CachedKey) => {\n  return cachePromise.get(cacheKey);\n};\n\nconst setCachePromise = (cacheKey: CachedKey, promise: Promise<any>) => {\n  // Should cache the same promise, cannot be promise.finally\n  // Because the promise.finally will change the reference of the promise\n  cachePromise.set(cacheKey, promise);\n\n  // no use promise.finally for compatibility\n  promise\n    .then((res) => {\n      cachePromise.delete(cacheKey);\n      return res;\n    })\n    .catch(() => {\n      cachePromise.delete(cacheKey);\n    });\n};\n\nexport { getCachePromise, setCachePromise };\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/utils/cacheSubscribe.ts",
    "content": "type Listener = (data: any) => void;\nconst listeners: Record<string, Listener[]> = {};\n\nconst trigger = (key: string, data: any) => {\n  if (listeners[key]) {\n    listeners[key].forEach((item) => item(data));\n  }\n};\n\nconst subscribe = (key: string, listener: Listener) => {\n  if (!listeners[key]) {\n    listeners[key] = [];\n  }\n  listeners[key].push(listener);\n\n  return function unsubscribe() {\n    const index = listeners[key].indexOf(listener);\n    listeners[key].splice(index, 1);\n  };\n};\n\nexport { trigger, subscribe };\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/utils/isDocumentVisible.ts",
    "content": "import isBrowser from '../../../utils/isBrowser';\n\nexport default function isDocumentVisible(): boolean {\n  if (isBrowser) {\n    return document.visibilityState !== 'hidden';\n  }\n  return true;\n}\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/utils/isOnline.ts",
    "content": "import isBrowser from '../../../utils/isBrowser';\n\nconst isOnline = () => {\n  if (isBrowser && typeof navigator.onLine !== 'undefined') {\n    return navigator.onLine;\n  }\n  return true;\n};\n\nexport default isOnline;\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/utils/limit.ts",
    "content": "export default function limit(fn: any, timespan: number) {\n  let pending = false;\n  return (...args: any[]) => {\n    if (pending) return;\n    pending = true;\n    fn(...args);\n    setTimeout(() => {\n      pending = false;\n    }, timespan);\n  };\n}\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/utils/subscribeFocus.ts",
    "content": "// from swr\nimport isBrowser from '../../../utils/isBrowser';\nimport isDocumentVisible from './isDocumentVisible';\nimport isOnline from './isOnline';\n\ntype Listener = () => void;\n\nconst listeners = new Set<Listener>();\n\nfunction subscribe(listener: Listener) {\n  listeners.add(listener);\n  return function unsubscribe() {\n    listeners.has(listener) && listeners.delete(listener);\n  };\n}\n\nif (isBrowser) {\n  const revalidate = () => {\n    if (!isDocumentVisible() || !isOnline()) return;\n    listeners.forEach((listener) => listener());\n  };\n  window.addEventListener('visibilitychange', revalidate, false);\n  window.addEventListener('focus', revalidate, false);\n}\n\nexport default subscribe;\n"
  },
  {
    "path": "packages/hooks/src/useRequest/src/utils/subscribeReVisible.ts",
    "content": "import isBrowser from '../../../utils/isBrowser';\nimport isDocumentVisible from './isDocumentVisible';\n\ntype Listener = () => void;\n\nconst listeners = new Set<Listener>();\n\nfunction subscribe(listener: Listener) {\n  listeners.add(listener);\n  return function unsubscribe() {\n    listeners.has(listener) && listeners.delete(listener);\n  };\n}\n\nif (isBrowser) {\n  const revalidate = () => {\n    if (!isDocumentVisible()) return;\n    listeners.forEach((listener) => listener());\n  };\n  window.addEventListener('visibilitychange', revalidate, false);\n}\n\nexport default subscribe;\n"
  },
  {
    "path": "packages/hooks/src/useResetState/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useResetState from '../index';\n\ndescribe('useResetState', () => {\n  const setUp = <S>(initialValue: S | (() => S)) =>\n    renderHook(() => {\n      const [state, setState, resetState] = useResetState(initialValue);\n\n      return {\n        state,\n        setState,\n        resetState,\n      } as const;\n    });\n\n  test('should support initialValue', () => {\n    const hook = setUp({\n      hello: 'world',\n    });\n    expect(hook.result.current.state).toEqual({ hello: 'world' });\n  });\n\n  test('should support functional initialValue', () => {\n    const hook = setUp(() => ({\n      hello: 'world',\n    }));\n    expect(hook.result.current.state).toEqual({ hello: 'world' });\n  });\n\n  test('should reset state', () => {\n    const hook = setUp({\n      hello: '',\n      count: 0,\n    });\n\n    act(() => {\n      hook.result.current.setState({\n        hello: 'world',\n        count: 1,\n      });\n    });\n\n    act(() => {\n      hook.result.current.resetState();\n    });\n\n    expect(hook.result.current.state).toEqual({ hello: '', count: 0 });\n  });\n\n  test('should support function update', () => {\n    const hook = setUp({\n      count: 0,\n    });\n    act(() => {\n      hook.result.current.setState((prev) => ({ count: prev.count + 1 }));\n    });\n    expect(hook.result.current.state).toEqual({ count: 1 });\n  });\n\n  test('should keep random initial state', () => {\n    const random = Math.random();\n    const hook = setUp({\n      count: random,\n    });\n\n    act(() => {\n      hook.result.current.setState({ count: Math.random() });\n    });\n\n    act(() => {\n      hook.result.current.resetState();\n    });\n\n    expect(hook.result.current.state).toEqual({ count: random });\n  });\n\n  test('should support random functional initialValue', () => {\n    const random = Math.random();\n    const hook = setUp(() => ({\n      count: random,\n    }));\n\n    act(() => {\n      hook.result.current.setState({ count: Math.random() });\n    });\n\n    act(() => {\n      hook.result.current.resetState();\n    });\n\n    expect(hook.result.current.state).toEqual({ count: random });\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useResetState/demo/demo1.tsx",
    "content": "import { useMemo } from 'react';\nimport { Button, Space } from 'antd';\nimport { useResetState } from 'ahooks';\n\nexport default () => {\n  const initialValue = {\n    hello: '',\n    value: Math.random(),\n  };\n  const initialValueMemo = useMemo(() => {\n    return initialValue;\n  }, []);\n\n  const [state, setState, resetState] = useResetState(initialValue);\n\n  return (\n    <div>\n      <div>initial state: </div>\n      <pre>{JSON.stringify(initialValueMemo, null, 2)}</pre>\n      <div>current state: </div>\n      <pre>{JSON.stringify(state, null, 2)}</pre>\n      <Space>\n        <Button\n          onClick={() =>\n            setState(() => ({\n              hello: 'world',\n              value: Math.random(),\n            }))\n          }\n        >\n          set hello and value\n        </Button>\n        <Button onClick={resetState}>resetState</Button>\n      </Space>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useResetState/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useResetState\n\nuseResetState works similar to `React.useState`, it provides a `reset` method\n\n## Examples\n\n### Default Usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst [state, setState, resetState] = useResetState<S>(\n  initialState: S | (() => S),\n): [S, Dispatch<SetStateAction<S>>, () => void]\n```\n"
  },
  {
    "path": "packages/hooks/src/useResetState/index.ts",
    "content": "import { useRef, useState } from 'react';\nimport type { Dispatch, SetStateAction } from 'react';\nimport { isFunction } from '../utils';\nimport useMemoizedFn from '../useMemoizedFn';\nimport useCreation from '../useCreation';\n\ntype ResetState = () => void;\n\nconst useResetState = <S>(\n  initialState: S | (() => S),\n): [S, Dispatch<SetStateAction<S>>, ResetState] => {\n  const initialStateRef = useRef(initialState);\n  const initialStateMemo = useCreation(\n    () =>\n      isFunction(initialStateRef.current) ? initialStateRef.current() : initialStateRef.current,\n    [],\n  );\n\n  const [state, setState] = useState(initialStateMemo);\n\n  const resetState = useMemoizedFn(() => {\n    setState(initialStateMemo);\n  });\n\n  return [state, setState, resetState];\n};\n\nexport default useResetState;\n"
  },
  {
    "path": "packages/hooks/src/useResetState/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useResetState\n\n提供重置 state 方法的 Hooks，用法与 `React.useState` 基本一致。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst [state, setState, resetState] = useResetState<S>(\n  initialState: S | (() => S),\n): [S, Dispatch<SetStateAction<S>>, () => void]\n```\n"
  },
  {
    "path": "packages/hooks/src/useResponsive/__tests__/__snapshots__/index.spec.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`useResponsive > should response to window width changes 1`] = `\n{\n  \"lg\": true,\n  \"md\": true,\n  \"sm\": true,\n  \"xl\": false,\n  \"xs\": true,\n}\n`;\n\nexports[`useResponsive > should response to window width changes 2`] = `\n{\n  \"lg\": false,\n  \"md\": false,\n  \"sm\": false,\n  \"xl\": false,\n  \"xs\": true,\n}\n`;\n\nexports[`useResponsive > should response to window width changes 3`] = `\n{\n  \"lg\": false,\n  \"md\": false,\n  \"sm\": true,\n  \"xl\": false,\n  \"xs\": true,\n}\n`;\n\nexports[`useResponsive > should response to window width changes 4`] = `\n{\n  \"lg\": false,\n  \"md\": true,\n  \"sm\": true,\n  \"xl\": false,\n  \"xs\": true,\n}\n`;\n\nexports[`useResponsive > should response to window width changes 5`] = `\n{\n  \"lg\": true,\n  \"md\": true,\n  \"sm\": true,\n  \"xl\": false,\n  \"xs\": true,\n}\n`;\n\nexports[`useResponsive > should response to window width changes 6`] = `\n{\n  \"lg\": true,\n  \"md\": true,\n  \"sm\": true,\n  \"xl\": true,\n  \"xs\": true,\n}\n`;\n\nexports[`useResponsive should response to window width changes 1`] = `\n{\n  \"lg\": true,\n  \"md\": true,\n  \"sm\": true,\n  \"xl\": false,\n  \"xs\": true,\n}\n`;\n\nexports[`useResponsive should response to window width changes 2`] = `\n{\n  \"lg\": false,\n  \"md\": false,\n  \"sm\": false,\n  \"xl\": false,\n  \"xs\": true,\n}\n`;\n\nexports[`useResponsive should response to window width changes 3`] = `\n{\n  \"lg\": false,\n  \"md\": false,\n  \"sm\": true,\n  \"xl\": false,\n  \"xs\": true,\n}\n`;\n\nexports[`useResponsive should response to window width changes 4`] = `\n{\n  \"lg\": false,\n  \"md\": true,\n  \"sm\": true,\n  \"xl\": false,\n  \"xs\": true,\n}\n`;\n\nexports[`useResponsive should response to window width changes 5`] = `\n{\n  \"lg\": true,\n  \"md\": true,\n  \"sm\": true,\n  \"xl\": false,\n  \"xs\": true,\n}\n`;\n\nexports[`useResponsive should response to window width changes 6`] = `\n{\n  \"lg\": true,\n  \"md\": true,\n  \"sm\": true,\n  \"xl\": true,\n  \"xs\": true,\n}\n`;\n"
  },
  {
    "path": "packages/hooks/src/useResponsive/__tests__/index.spec.ts",
    "content": "import { describe, expect, test } from 'vitest';\nimport { act, renderHook } from '../../utils/tests';\nimport useResponsive from '../';\n\ndescribe('useResponsive', () => {\n  function changeWidth(width: number) {\n    act(() => {\n      (global as any).innerWidth = width;\n      (global as any).dispatchEvent(new Event('resize'));\n    });\n  }\n  changeWidth(1024);\n\n  const hook = renderHook(() => useResponsive());\n\n  test('should response to window width changes', () => {\n    expect(hook.result.current).toMatchSnapshot();\n    changeWidth(300);\n    expect(hook.result.current).toMatchSnapshot();\n    changeWidth(700);\n    expect(hook.result.current).toMatchSnapshot();\n    changeWidth(800);\n    expect(hook.result.current).toMatchSnapshot();\n    changeWidth(1000);\n    expect(hook.result.current).toMatchSnapshot();\n    changeWidth(1200);\n    expect(hook.result.current).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useResponsive/demo/demo1.tsx",
    "content": "/**\n * title: Get responsive info in components\n * desc: By calling useResponsive in components, you can retrieve the responsive infomation of the browser page and subscribe to it at the same time.\n *\n * title.zh-CN: 在组件中获取响应式信息\n * desc.zh-CN: 在组件中调用 useResponsive 可以获取并订阅浏览器窗口的响应式信息。\n */\n\nimport { configResponsive, useResponsive } from 'ahooks';\n\nconfigResponsive({\n  small: 0,\n  middle: 800,\n  large: 1200,\n});\n\nexport default function () {\n  const responsive = useResponsive();\n  return (\n    <>\n      <p>Please change the width of the browser window to see the effect: </p>\n      {Object.keys(responsive).map((key) => (\n        <p key={key}>\n          {key} {responsive[key] ? '✔' : '✘'}\n        </p>\n      ))}\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/hooks/src/useResponsive/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useResponsive\n\nReact Hook for getting responsive info.\n\n## Examples\n\n### Get responsive info in components\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\ninterface ResponsiveConfig {\n  [key: string]: number;\n}\ninterface ResponsiveInfo {\n  [key: string]: boolean;\n}\nfunction configResponsive(config: ResponsiveConfig): void;\nfunction useResponsive(): ResponsiveInfo;\n```\n\n### Config\n\nThe default config is the same as bootstrap:\n\n```javascript\n{\n  'xs': 0,\n  'sm': 576,\n  'md': 768,\n  'lg': 992,\n  'xl': 1200,\n}\n```\n\nIf you want to config your own responsive breakpoints, you can use `configResponsive`:\n\n(Caution: You only need to config it once. Don't call this config function repeatedly.)\n\n```javascript\nconfigResponsive({\n  small: 0,\n  middle: 800,\n  large: 1200,\n});\n```\n"
  },
  {
    "path": "packages/hooks/src/useResponsive/index.ts",
    "content": "import { useEffect, useState } from 'react';\nimport isBrowser from '../utils/isBrowser';\n\ntype Subscriber = () => void;\n\nconst subscribers = new Set<Subscriber>();\n\ntype ResponsiveConfig = Record<string, number>;\ntype ResponsiveInfo = Record<string, boolean>;\n\nlet info: ResponsiveInfo;\n\nlet responsiveConfig: ResponsiveConfig = {\n  xs: 0,\n  sm: 576,\n  md: 768,\n  lg: 992,\n  xl: 1200,\n};\n\nfunction handleResize() {\n  const oldInfo = info;\n  calculate();\n  if (oldInfo === info) {\n    return;\n  }\n  for (const subscriber of subscribers) {\n    subscriber();\n  }\n}\n\nlet listening = false;\n\nfunction calculate() {\n  const width = window.innerWidth;\n  const newInfo = {} as ResponsiveInfo;\n  let shouldUpdate = false;\n  for (const key of Object.keys(responsiveConfig)) {\n    newInfo[key] = width >= responsiveConfig[key];\n    if (newInfo[key] !== info[key]) {\n      shouldUpdate = true;\n    }\n  }\n  if (shouldUpdate) {\n    info = newInfo;\n  }\n}\n\nexport function configResponsive(config: ResponsiveConfig) {\n  responsiveConfig = config;\n  if (info) calculate();\n}\n\nfunction useResponsive() {\n  if (isBrowser && !listening) {\n    info = {};\n    calculate();\n    window.addEventListener('resize', handleResize);\n    listening = true;\n  }\n  const [state, setState] = useState<ResponsiveInfo>(info);\n\n  useEffect(() => {\n    if (!isBrowser) {\n      return;\n    }\n\n    // In React 18's StrictMode, useEffect perform twice, resize listener is remove, so handleResize is never perform.\n    // https://github.com/alibaba/hooks/issues/1910\n    if (!listening) {\n      window.addEventListener('resize', handleResize);\n    }\n\n    const subscriber = () => {\n      setState(info);\n    };\n\n    subscribers.add(subscriber);\n    return () => {\n      subscribers.delete(subscriber);\n      if (subscribers.size === 0) {\n        window.removeEventListener('resize', handleResize);\n        listening = false;\n      }\n    };\n  }, []);\n\n  return state;\n}\n\nexport default useResponsive;\n"
  },
  {
    "path": "packages/hooks/src/useResponsive/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useResponsive\n\n获取响应式信息。\n\n## 代码演示\n\n### 在组件中获取响应式信息\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\ninterface ResponsiveConfig {\n  [key: string]: number;\n}\ninterface ResponsiveInfo {\n  [key: string]: boolean;\n}\nfunction configResponsive(config: ResponsiveConfig): void;\nfunction useResponsive(): ResponsiveInfo;\n```\n\n### 配置\n\n默认的响应式配置和 bootstrap 是一致的：\n\n```javascript\n{\n  'xs': 0,\n  'sm': 576,\n  'md': 768,\n  'lg': 992,\n  'xl': 1200,\n}\n```\n\n如果你想配置自己的响应式断点，可以使用 `configResponsive` ：\n\n（注意：只需配置一次，请勿在组件中重复调用该方法）\n\n```javascript\nconfigResponsive({\n  small: 0,\n  middle: 800,\n  large: 1200,\n});\n```\n"
  },
  {
    "path": "packages/hooks/src/useSafeState/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useSafeState from '../index';\n\ndescribe('useSetState', () => {\n  const setUp = <S>(initialValue: S | (() => S)) =>\n    renderHook(() => {\n      const [state, setState] = useSafeState(initialValue);\n      return {\n        state,\n        setState,\n      } as const;\n    });\n\n  test('should support initialValue', () => {\n    const hook = setUp({\n      hello: 'world',\n    });\n    expect(hook.result.current.state).toEqual({ hello: 'world' });\n  });\n\n  test('should support update', () => {\n    const hook = setUp(0);\n    act(() => {\n      hook.result.current.setState(5);\n    });\n    expect(hook.result.current.state).toBe(5);\n  });\n\n  test('should not support update when unmount', () => {\n    const hook = setUp(0);\n    hook.unmount();\n    act(() => {\n      hook.result.current.setState(5);\n    });\n    expect(hook.result.current.state).toBe(0);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useSafeState/demo/demo1.tsx",
    "content": "import { useSafeState } from 'ahooks';\nimport { useEffect, useState } from 'react';\n\nconst Child = () => {\n  const [value, setValue] = useSafeState<string>();\n\n  useEffect(() => {\n    setTimeout(() => {\n      setValue('data loaded from server');\n    }, 5000);\n  }, []);\n\n  const text = value || 'Loading...';\n\n  return <div>{text}</div>;\n};\n\nexport default () => {\n  const [visible, setVisible] = useState(true);\n\n  return (\n    <div>\n      <button onClick={() => setVisible(false)}>Unmount</button>\n      {visible && <Child />}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useSafeState/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useSafeState\n\nIt is exactly the same with `React.useState` , but after the component is unmounted, the `setState` in the asynchronous callback will no longer be executed to avoid memory leakage caused by updating the state after the component is unmounted.\n\n## Examples\n\n### Basic usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst [state, setState] = useSafeState(initialState);\n```\n"
  },
  {
    "path": "packages/hooks/src/useSafeState/index.ts",
    "content": "import { useCallback, useState } from 'react';\nimport type { Dispatch, SetStateAction } from 'react';\nimport useUnmountedRef from '../useUnmountedRef';\n\nfunction useSafeState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];\n\nfunction useSafeState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];\n\nfunction useSafeState<S>(initialState?: S | (() => S)) {\n  const unmountedRef = useUnmountedRef();\n  const [state, setState] = useState(initialState);\n  const setCurrentState = useCallback((currentState: S) => {\n    /** if component is unmounted, stop update */\n    if (unmountedRef.current) {\n      return;\n    }\n    setState(currentState);\n  }, []);\n\n  return [state, setCurrentState] as const;\n}\n\nexport default useSafeState;\n"
  },
  {
    "path": "packages/hooks/src/useSafeState/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useSafeState\n\n用法与 `React.useState` 完全一样，但是在组件卸载后异步回调内的 `setState` 不再执行，避免因组件卸载后更新状态而导致的内存泄漏。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst [state, setState] = useSafeState(initialState);\n```\n"
  },
  {
    "path": "packages/hooks/src/useScroll/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useScroll from '../index';\n\ndescribe('useScroll', () => {\n  test('document body', () => {\n    const hook = renderHook(() => useScroll(document));\n    expect(hook.result.current).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useScroll/demo/demo1.tsx",
    "content": "/**\n * title: Basic Usage\n * desc: Try to scroll the box below.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 尝试滚动一下文字内容。\n */\n\nimport { useRef } from 'react';\nimport { useScroll } from 'ahooks';\n\nexport default () => {\n  const ref = useRef(null);\n  const scroll = useScroll(ref);\n  return (\n    <>\n      <p>{JSON.stringify(scroll)}</p>\n      <div\n        style={{\n          height: '160px',\n          width: '160px',\n          border: 'solid 1px #000',\n          overflow: 'scroll',\n          whiteSpace: 'nowrap',\n          fontSize: '32px',\n        }}\n        ref={ref}\n      >\n        <div>\n          Lorem ipsum dolor sit amet, consectetur adipisicing elit. A aspernatur atque, debitis ex\n          excepturi explicabo iste iure labore molestiae neque optio perspiciatis\n        </div>\n        <div>\n          Aspernatur cupiditate, deleniti id incidunt mollitia omnis! A aspernatur assumenda\n          consequuntur culpa cumque dignissimos enim eos, et fugit natus nemo nesciunt\n        </div>\n        <div>\n          Alias aut deserunt expedita, inventore maiores minima officia porro rem. Accusamus ducimus\n          magni modi mollitia nihil nisi provident\n        </div>\n        <div>\n          Alias aut autem consequuntur doloremque esse facilis id molestiae neque officia placeat,\n          quia quisquam repellendus reprehenderit.\n        </div>\n        <div>\n          Adipisci blanditiis facere nam perspiciatis sit soluta ullam! Architecto aut blanditiis,\n          consectetur corporis cum deserunt distinctio dolore eius est exercitationem\n        </div>\n        <div>Ab aliquid asperiores assumenda corporis cumque dolorum expedita</div>\n        <div>\n          Culpa cumque eveniet natus totam! Adipisci, animi at commodi delectus distinctio dolore\n          earum, eum expedita facilis\n        </div>\n        <div>\n          Quod sit, temporibus! Amet animi fugit officiis perspiciatis, quis unde. Cumque\n          dignissimos distinctio, dolor eaque est fugit nisi non pariatur porro possimus, quas quasi\n        </div>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useScroll/demo/demo2.tsx",
    "content": "/**\n * title: Listen Page Scroll\n * desc: Try to scroll this webpage.\n *\n * title.zh-CN: 监测整页的滚动\n * desc.zh-CN: 尝试滚动一下页面。\n */\n\nimport { useScroll } from 'ahooks';\n\nexport default () => {\n  const scroll = useScroll(document);\n  return (\n    <div>\n      <div>{JSON.stringify(scroll)}</div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useScroll/demo/demo3.tsx",
    "content": "/**\n * title: Custom update\n * desc: listen on scroll event between 100px ~ 200px in vertical direction\n *\n * title.zh-CN: 自定义更新\n * desc.zh-CN: 在垂直方向 100px 到 200px 的滚动范围内监听\n */\n\nimport { useRef } from 'react';\nimport { useScroll } from 'ahooks';\n\nexport default () => {\n  const ref = useRef(null);\n\n  const scroll = useScroll(ref, (val) => val.top > 100 && val.top < 200);\n\n  return (\n    <>\n      <p>{JSON.stringify(scroll)}</p>\n      <div\n        style={{\n          height: '160px',\n          width: '160px',\n          border: 'solid 1px #000',\n          overflow: 'scroll',\n          whiteSpace: 'nowrap',\n          fontSize: '36px',\n        }}\n        ref={ref}\n      >\n        <div>\n          Lorem ipsum dolor sit amet, consectetur adipisicing elit. A aspernatur atque, debitis ex\n          excepturi explicabo iste iure labore molestiae neque optio perspiciatis\n        </div>\n        <div>\n          Aspernatur cupiditate, deleniti id incidunt mollitia omnis! A aspernatur assumenda\n          consequuntur culpa cumque dignissimos enim eos, et fugit natus nemo nesciunt\n        </div>\n        <div>\n          Alias aut deserunt expedita, inventore maiores minima officia porro rem. Accusamus ducimus\n          magni modi mollitia nihil nisi provident\n        </div>\n        <div>\n          Alias aut autem consequuntur doloremque esse facilis id molestiae neque officia placeat,\n          quia quisquam repellendus reprehenderit.\n        </div>\n        <div>\n          Adipisci blanditiis facere nam perspiciatis sit soluta ullam! Architecto aut blanditiis,\n          consectetur corporis cum deserunt distinctio dolore eius est exercitationem\n        </div>\n        <div>Ab aliquid asperiores assumenda corporis cumque dolorum expedita</div>\n        <div>\n          Culpa cumque eveniet natus totam! Adipisci, animi at commodi delectus distinctio dolore\n          earum, eum expedita facilis\n        </div>\n        <div>\n          Quod sit, temporibus! Amet animi fugit officiis perspiciatis, quis unde. Cumque\n          dignissimos distinctio, dolor eaque est fugit nisi non pariatur porro possimus, quas quasi\n        </div>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useScroll/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useScroll\n\nGet the scroll position of an element.\n\n## Examples\n\n### Basic Usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Detect Whole Page Scroll\n\n<code src=\"./demo/demo2.tsx\" />\n\n### Control listen on scroll status\n\n<code src=\"./demo/demo3.tsx\" />\n\n## API\n\n```typescript\nconst position = useScroll(target, shouldUpdate);\n```\n\n### Params\n\n| Property     | Description               | Type                                                                        | Default      |\n| ------------ | ------------------------- | --------------------------------------------------------------------------- | ------------ |\n| target       | DOM element or ref object | `Element` \\| `Document` \\| `(() => Element)` \\| `MutableRefObject<Element>` | `document`   |\n| shouldUpdate | Whether update position   | `({ top: number, left: number }) => boolean`                                | `() => true` |\n\n### Result\n\n| Property | Description                                 | Type                                         |\n| -------- | ------------------------------------------- | -------------------------------------------- |\n| position | The current scroll position of the element. | `{ left: number, top: number } \\| undefined` |\n"
  },
  {
    "path": "packages/hooks/src/useScroll/index.ts",
    "content": "import useRafState from '../useRafState';\nimport useLatest from '../useLatest';\nimport type { BasicTarget } from '../utils/domTarget';\nimport { getTargetElement } from '../utils/domTarget';\nimport useEffectWithTarget from '../utils/useEffectWithTarget';\n\ntype Position = { left: number; top: number };\n\nexport type Target = BasicTarget<Element | Document>;\nexport type ScrollListenController = (val: Position) => boolean;\n\nfunction useScroll(\n  target?: Target,\n  shouldUpdate: ScrollListenController = () => true,\n): Position | undefined {\n  const [position, setPosition] = useRafState<Position>();\n\n  const shouldUpdateRef = useLatest(shouldUpdate);\n\n  useEffectWithTarget(\n    () => {\n      const el = getTargetElement(target, document);\n      if (!el) {\n        return;\n      }\n      const updatePosition = () => {\n        let newPosition: Position;\n        if (el === document) {\n          if (document.scrollingElement) {\n            newPosition = {\n              left: document.scrollingElement.scrollLeft,\n              top: document.scrollingElement.scrollTop,\n            };\n          } else {\n            // When in quirks mode, the scrollingElement attribute returns the HTML body element if it exists and is potentially scrollable, otherwise it returns null.\n            // https://developer.mozilla.org/zh-CN/docs/Web/API/Document/scrollingElement\n            // https://stackoverflow.com/questions/28633221/document-body-scrolltop-firefox-returns-0-only-js\n            newPosition = {\n              left: Math.max(\n                window.pageXOffset,\n                document.documentElement.scrollLeft,\n                document.body.scrollLeft,\n              ),\n              top: Math.max(\n                window.pageYOffset,\n                document.documentElement.scrollTop,\n                document.body.scrollTop,\n              ),\n            };\n          }\n        } else {\n          newPosition = {\n            left: (el as Element).scrollLeft,\n            top: (el as Element).scrollTop,\n          };\n        }\n        if (shouldUpdateRef.current(newPosition)) {\n          setPosition(newPosition);\n        }\n      };\n\n      updatePosition();\n\n      el.addEventListener('scroll', updatePosition);\n      return () => {\n        el.removeEventListener('scroll', updatePosition);\n      };\n    },\n    [],\n    target,\n  );\n\n  return position;\n}\n\nexport default useScroll;\n"
  },
  {
    "path": "packages/hooks/src/useScroll/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useScroll\n\n监听元素的滚动位置。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 监测整页的滚动\n\n<code src=\"./demo/demo2.tsx\" />\n\n### 控制滚动状态的监听\n\n<code src=\"./demo/demo3.tsx\" />\n\n## API\n\n```typescript\nconst position = useScroll(target, shouldUpdate);\n```\n\n### Params\n\n| 参数         | 说明                 | 类型                                                                        | 默认值       |\n| ------------ | -------------------- | --------------------------------------------------------------------------- | ------------ |\n| target       | DOM 节点或者 ref     | `Element` \\| `Document` \\| `(() => Element)` \\| `MutableRefObject<Element>` | `document`   |\n| shouldUpdate | 控制是否更新滚动信息 | `({ top: number, left: number }) => boolean`                                | `() => true` |\n\n### Result\n\n| 参数     | 说明                   | 类型                                         |\n| -------- | ---------------------- | -------------------------------------------- |\n| position | 滚动容器当前的滚动位置 | `{ left: number, top: number } \\| undefined` |\n"
  },
  {
    "path": "packages/hooks/src/useSelections/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { useState } from 'react';\nimport { describe, expect, test } from 'vitest';\nimport type { Options } from '../index';\nimport useSelections from '../index';\n\nconst _data = [1, 2, 3];\nconst _selected = [1];\nconst _selectedItem = 1;\n\nconst _dataObj = [{ id: 1 }, { id: 2 }, { id: 3 }];\nconst _selectedObj = [{ id: 1 }];\nconst _selectedItemObj = { id: 1 };\n\nconst setup = <T>(items: T[], options?: T[] | Options<T>) => {\n  return renderHook(() => useSelections(items, options));\n};\n\ntype CaseCallback<T = number | object> = (data: T[], selected: T[], selectedItem: T) => void;\n\nconst runCaseCallback = (\n  dataCallback: CaseCallback<number>,\n  objDataCallback: CaseCallback<object>,\n) => {\n  dataCallback(_data, _selected, _selectedItem);\n  objDataCallback(_dataObj, _selectedObj, _selectedItemObj);\n};\n\ndescribe('useSelections', () => {\n  test('defaultSelected should work correct', () => {\n    const caseCallback: CaseCallback = (data, selected, selectedItem) => {\n      const { result } = setup(data, {\n        defaultSelected: selected,\n        itemKey: 'id',\n      });\n\n      expect(result.current.selected).toEqual(selected);\n      expect(result.current.isSelected(selectedItem)).toBe(true);\n    };\n\n    runCaseCallback(caseCallback, caseCallback);\n  });\n\n  test('select and unSelect should work correct', () => {\n    const caseCallback: CaseCallback = (data, selected, selectedItem) => {\n      const { result } = setup(data, {\n        defaultSelected: selected,\n        itemKey: 'id',\n      });\n      const { unSelect, select } = result.current;\n\n      act(() => {\n        unSelect(selectedItem);\n      });\n      expect(result.current.selected).toEqual([]);\n      expect(result.current.isSelected(selectedItem)).toBe(false);\n      expect(result.current.allSelected).toBe(false);\n\n      act(() => {\n        select(selectedItem);\n      });\n      expect(result.current.selected).toEqual(selected);\n      expect(result.current.isSelected(selectedItem)).toBe(true);\n      expect(result.current.allSelected).toBe(false);\n    };\n\n    runCaseCallback(caseCallback, caseCallback);\n  });\n\n  test('toggle should work correct', () => {\n    const caseCallback: CaseCallback = (data, selected, selectedItem) => {\n      const { result } = setup(data, {\n        itemKey: 'id',\n      });\n      const { toggle } = result.current;\n\n      act(() => {\n        toggle(selectedItem);\n      });\n      expect(result.current.selected).toEqual(selected);\n      expect(result.current.isSelected(selectedItem)).toBe(true);\n      expect(result.current.allSelected).toBe(false);\n\n      act(() => {\n        toggle(selectedItem);\n      });\n      expect(result.current.selected).toEqual([]);\n      expect(result.current.isSelected(selectedItem)).toBe(false);\n      expect(result.current.allSelected).toBe(false);\n    };\n\n    runCaseCallback(caseCallback, caseCallback);\n  });\n\n  test('selectAll and unSelectAll should work correct', async () => {\n    const caseCallback: CaseCallback = (data) => {\n      const { result } = setup(data, {\n        itemKey: 'id',\n      });\n      const { selectAll, unSelectAll } = result.current;\n\n      expect(result.current.noneSelected).toBe(true);\n      act(() => {\n        selectAll();\n      });\n      expect(result.current.selected).toEqual(data);\n      expect(result.current.allSelected).toBe(true);\n      expect(result.current.noneSelected).toBe(false);\n      expect(result.current.partiallySelected).toBe(false);\n\n      act(() => {\n        unSelectAll();\n      });\n      expect(result.current.selected).toEqual([]);\n      expect(result.current.allSelected).toBe(false);\n      expect(result.current.noneSelected).toBe(true);\n      expect(result.current.partiallySelected).toBe(false);\n    };\n\n    runCaseCallback(caseCallback, caseCallback);\n  });\n\n  test('toggleAll should work correct', async () => {\n    const caseCallback: CaseCallback = (data) => {\n      const { result } = setup(data, {\n        itemKey: 'id',\n      });\n      const { toggleAll } = result.current;\n\n      expect(result.current.noneSelected).toBe(true);\n      act(() => {\n        toggleAll();\n      });\n      expect(result.current.selected).toEqual(data);\n      expect(result.current.allSelected).toBe(true);\n      expect(result.current.noneSelected).toBe(false);\n      expect(result.current.partiallySelected).toBe(false);\n\n      act(() => {\n        toggleAll();\n      });\n      expect(result.current.selected).toEqual([]);\n      expect(result.current.allSelected).toBe(false);\n      expect(result.current.noneSelected).toBe(true);\n      expect(result.current.partiallySelected).toBe(false);\n    };\n\n    runCaseCallback(caseCallback, caseCallback);\n  });\n\n  test('setSelected should work correct', async () => {\n    const caseCallback: CaseCallback = (data, selected, selectedItem) => {\n      const { result } = setup(data, {\n        itemKey: 'id',\n      });\n      const { setSelected } = result.current;\n\n      expect(result.current.noneSelected).toBe(true);\n      act(() => {\n        setSelected(selected);\n      });\n      expect(result.current.selected).toEqual(selected);\n      expect(result.current.isSelected(selectedItem)).toBe(true);\n      expect(result.current.noneSelected).toBe(false);\n      expect(result.current.allSelected).toBe(false);\n      expect(result.current.partiallySelected).toBe(true);\n\n      act(() => {\n        setSelected([]);\n      });\n      expect(result.current.selected).toEqual([]);\n      expect(result.current.isSelected(selectedItem)).toBe(false);\n      expect(result.current.noneSelected).toBe(true);\n      expect(result.current.allSelected).toBe(false);\n      expect(result.current.partiallySelected).toBe(false);\n\n      act(() => {\n        setSelected(data);\n      });\n      expect(result.current.selected).toEqual(data);\n      expect(result.current.isSelected(selectedItem)).toBe(true);\n      expect(result.current.noneSelected).toBe(false);\n      expect(result.current.allSelected).toBe(true);\n      expect(result.current.partiallySelected).toBe(false);\n\n      // Keep compatible with older versions.\n      act(() => {\n        expect(() => setSelected(undefined!)).not.toThrowError();\n        expect(() => setSelected(null!)).not.toThrowError();\n      });\n    };\n\n    runCaseCallback(caseCallback, caseCallback);\n  });\n\n  test('legacy parameter should work in <4.0', async () => {\n    const { result } = setup(_data, _selected);\n\n    expect(result.current.selected).toEqual(_selected);\n    expect(result.current.isSelected(_selectedItem)).toBe(true);\n  });\n\n  test('clearAll should work correct', async () => {\n    const runCase = (data: any, newData: any, remainData: any) => {\n      const { result } = renderHook(() => {\n        const [list, setList] = useState(data);\n        const hook = useSelections(list, {\n          itemKey: 'id',\n        });\n\n        return { setList, hook };\n      });\n      const { setSelected, unSelectAll, clearAll } = result.current.hook;\n\n      act(() => {\n        setSelected(data);\n      });\n      expect(result.current.hook.selected).toEqual(data);\n      expect(result.current.hook.allSelected).toBe(true);\n\n      act(() => {\n        result.current.setList(newData);\n      });\n      expect(result.current.hook.allSelected).toBe(false);\n\n      act(() => {\n        unSelectAll();\n      });\n      expect(result.current.hook.selected).toEqual(remainData);\n\n      act(() => {\n        clearAll();\n      });\n      expect(result.current.hook.selected).toEqual([]);\n      expect(result.current.hook.allSelected).toEqual(false);\n      expect(result.current.hook.noneSelected).toBe(true);\n      expect(result.current.hook.partiallySelected).toBe(false);\n    };\n\n    runCase(_data, [3, 4, 5], [1, 2]);\n    runCase(_dataObj, [{ id: 3 }, { id: 4 }, { id: 5 }], [{ id: 1 }, { id: 2 }]);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useSelections/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Checkbox group.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 常见的 Checkbox 联动\n */\n\nimport { Checkbox, Col, Row } from 'antd';\nimport { useMemo, useState } from 'react';\nimport { useSelections } from 'ahooks';\n\nexport default () => {\n  const [hideOdd, setHideOdd] = useState(false);\n  const list = useMemo(() => {\n    if (hideOdd) {\n      return [2, 4, 6, 8];\n    }\n    return [1, 2, 3, 4, 5, 6, 7, 8];\n  }, [hideOdd]);\n\n  const { selected, allSelected, isSelected, toggle, toggleAll, partiallySelected } = useSelections(\n    list,\n    {\n      defaultSelected: [1],\n    },\n  );\n\n  return (\n    <div>\n      <div>Selected: {selected.join(',')}</div>\n      <div style={{ borderBottom: '1px solid #E9E9E9', padding: '10px 0' }}>\n        <Checkbox checked={allSelected} onClick={toggleAll} indeterminate={partiallySelected}>\n          Check all\n        </Checkbox>\n        <Checkbox checked={hideOdd} onClick={() => setHideOdd((v) => !v)}>\n          Hide Odd\n        </Checkbox>\n      </div>\n      <Row style={{ padding: '10px 0' }}>\n        {list.map((o) => (\n          <Col span={12} key={o}>\n            <Checkbox checked={isSelected(o)} onClick={() => toggle(o)}>\n              {o}\n            </Checkbox>\n          </Col>\n        ))}\n      </Row>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useSelections/demo/demo2.tsx",
    "content": "/**\n * title: Object array\n * desc: When array items are object, you need to specify the field name for the unique key.\n *\n * title.zh-CN: 对象数组\n * desc.zh-CN: 数组项是对象时，需要指定唯一 key 的字段名称。\n */\n\nimport { Checkbox, Col, Row } from 'antd';\nimport { useMemo, useState } from 'react';\nimport { useSelections } from 'ahooks';\n\nexport default () => {\n  const [hideOdd, setHideOdd] = useState(false);\n  const list = useMemo(() => {\n    if (hideOdd) {\n      return [2, 4, 6, 8].map((id) => ({ id }));\n    }\n    return [1, 2, 3, 4, 5, 6, 7, 8].map((id) => ({ id }));\n  }, [hideOdd]);\n\n  const { selected, allSelected, isSelected, toggle, toggleAll, partiallySelected } = useSelections(\n    list,\n    {\n      defaultSelected: [{ id: 1 }],\n      itemKey: 'id',\n    },\n  );\n\n  return (\n    <div>\n      <div>Selected: {JSON.stringify(selected)}</div>\n      <div style={{ borderBottom: '1px solid #E9E9E9', padding: '10px 0' }}>\n        <Checkbox checked={allSelected} onClick={toggleAll} indeterminate={partiallySelected}>\n          Check all\n        </Checkbox>\n        <Checkbox checked={hideOdd} onClick={() => setHideOdd((v) => !v)}>\n          Hide Odd\n        </Checkbox>\n      </div>\n      <Row style={{ padding: '10px 0' }}>\n        {list.map((item) => (\n          <Col span={12} key={item.id}>\n            <Checkbox checked={isSelected(item)} onClick={() => toggle(item)}>\n              {item.id}\n            </Checkbox>\n          </Col>\n        ))}\n      </Row>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useSelections/demo/demo3.tsx",
    "content": "/**\n * title: Pagination\n * desc: Load data with pagination and enable cross-page selection.\n *\n * title.zh-CN: 分页多选\n * desc.zh-CN: 分页加载数据，并跨页选择。\n */\n\nimport { Checkbox, Divider, Pagination, Spin } from 'antd';\nimport { useEffect, useState } from 'react';\nimport { useSelections } from 'ahooks';\n\ninterface DataType {\n  id: number;\n  title: string;\n}\n\ninterface PaginationType {\n  current: number;\n  pageSize: number;\n  total?: number;\n}\n\nconst dataSource = Array.from({ length: 50 }, (item, index) => ({\n  id: index,\n  title: `title ${index}`,\n}));\n\nconst getDataFromServer = (props: PaginationType) => {\n  const { current, pageSize } = props;\n  const data = dataSource.slice((current - 1) * pageSize, current * pageSize);\n\n  return new Promise<{\n    data: DataType[];\n    total: PaginationType['total'];\n  }>((resolve) => {\n    setTimeout(\n      () =>\n        resolve({\n          data,\n          total: dataSource.length,\n        }),\n      500,\n    );\n  });\n};\n\nexport default () => {\n  const [dataList, setDataList] = useState<DataType[]>([]);\n  const [loading, setLoading] = useState<boolean>(false);\n  const [pagination, setPagination] = useState<PaginationType>({\n    current: 1,\n    pageSize: 10,\n    total: 0,\n  });\n\n  const getData = async (params: PaginationType) => {\n    setLoading(true);\n\n    const { data, total } = await getDataFromServer(params);\n\n    setLoading(false);\n    setDataList(data);\n    setPagination({ ...params, total });\n  };\n\n  useEffect(() => {\n    getData(pagination);\n  }, []);\n\n  const { selected, allSelected, isSelected, toggle, toggleAll, partiallySelected } = useSelections(\n    dataList,\n    {\n      itemKey: 'id',\n    },\n  );\n\n  return (\n    <Spin spinning={loading}>\n      {dataList.map((item) => {\n        const { id, title } = item;\n\n        return (\n          <div key={id} style={{ display: 'flex', flexDirection: 'row' }}>\n            <Checkbox\n              style={{ padding: '4px 8px' }}\n              onClick={() => toggle(item)}\n              checked={isSelected(item)}\n            >\n              {title}\n            </Checkbox>\n          </div>\n        );\n      })}\n      <Pagination\n        style={{ margin: '12px 0 16px 0' }}\n        size=\"small\"\n        showSizeChanger\n        current={pagination.current}\n        pageSize={pagination.pageSize}\n        total={pagination.total}\n        onChange={(page, size) => {\n          getData({\n            current: page,\n            pageSize: size,\n          });\n        }}\n      />\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'row',\n          alignItems: 'center',\n          paddingLeft: '8px',\n        }}\n      >\n        <Checkbox checked={allSelected} indeterminate={partiallySelected} onClick={toggleAll}>\n          Check all\n        </Checkbox>\n        <span style={{ marginLeft: '90px' }}>Selected: {selected.length}</span>\n      </div>\n      {!!selected.length && (\n        <>\n          <Divider />\n          {JSON.stringify(selected)}\n        </>\n      )}\n    </Spin>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useSelections/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useSelections\n\nThis hook is used for Checkbox group, supports multiple selection, single selection, select-all, select-none and semi-selected etc.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Object array\n\n<code src=\"./demo/demo2.tsx\" />\n\n### Pagination\n\n<code src=\"./demo/demo3.tsx\" />\n\n## API\n\n```typescript\ninterface Options<T> {\n  defaultSelected?: T[];\n  itemKey?: string | ((item: T) => Key);\n}\n\n// works when >=3.8.0, recommended ✅\nconst result: Result = useSelections<T>(items: T[], options?: Options<T>);\n\n// works when <4.0.0, will be removed in ahooks 4.0 🙅🏻‍♀️\nconst result: Result = useSelections<T>(items: T[], defaultSelected?: T[]);\n```\n\n### Params\n\n| Property | Description | Type | Default |\n| --- | --- | --- | --- |\n| items | Data items | `T[]` | - |\n| options | Optional configuration | `Options` | - |\n\n### Options\n\n| Property | Description | Type | Default |\n| --- | --- | --- | --- |\n| defaultSelected | Default selected data | `T[]` | `[]` |\n| itemKey | The unique key of data item. Typically, this parameter needs to be specified when the data source is an array of object | `string` \\| `(item: T) => React.Key` | - |\n\n### Result\n\n| Property          | Description                                                                                                                                                                                                                                               | Type                                                                |\n| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |\n| selected          | Selected items                                                                                                                                                                                                                                            | `T[]`                                                               |\n| allSelected       | Is all items selected                                                                                                                                                                                                                                     | `boolean`                                                           |\n| noneSelected      | Is no item selected                                                                                                                                                                                                                                       | `boolean`                                                           |\n| partiallySelected | Is partially items selected                                                                                                                                                                                                                               | `boolean`                                                           |\n| isSelected        | Whether item is selected                                                                                                                                                                                                                                  | `(value: T) => boolean`                                             |\n| setSelected       | Select multiple items. When executed multiple times, the later return value overwrites the previous one, so if you want to merge the results of multiple operations, you need to do this manually: `setSelected((oldArray) => oldArray.concat(newArray))` | `(value: T[]) => void  \\| (value: (prevState: T[]) => T[]) => void` |\n| select            | Select single item                                                                                                                                                                                                                                        | `(value: T) => void`                                                |\n| unSelect          | UnSelect single item                                                                                                                                                                                                                                      | `(value: T) => void`                                                |\n| toggle            | Toggle single item select status                                                                                                                                                                                                                          | `(value: T) => void`                                                |\n| selectAll         | Select all items                                                                                                                                                                                                                                          | `() => void`                                                        |\n| unSelectAll       | UnSelect all items                                                                                                                                                                                                                                        | `() => void`                                                        |\n| toggleAll         | Toggle select all items                                                                                                                                                                                                                                   | `() => void`                                                        |\n| clearAll          | Clear all selected (In general, `clearAll` is equivalent to `unSelectAll`. If the items is dynamic, `clearAll` will clear \"all selected data\", while `unSelectAll` will only clear \"the currently selected data in the items\")                            | `() => void`                                                        |\n"
  },
  {
    "path": "packages/hooks/src/useSelections/index.ts",
    "content": "import isPlainObject from 'lodash/isPlainObject';\nimport useMemoizedFn from '../useMemoizedFn';\nimport { isFunction, isString } from '../utils';\nimport { useMemo, useState } from 'react';\n\nexport interface Options<T> {\n  defaultSelected?: T[];\n  itemKey?: string | ((item: T) => React.Key);\n}\n\nfunction useSelections<T>(items: T[], options?: T[] | Options<T>) {\n  let defaultSelected: T[] = [];\n  let itemKey: Options<T>['itemKey'];\n\n  if (Array.isArray(options)) {\n    defaultSelected = options;\n  } else if (isPlainObject(options)) {\n    defaultSelected = options?.defaultSelected ?? defaultSelected;\n    itemKey = options?.itemKey ?? itemKey;\n  }\n\n  const getKey = (item: T) => {\n    if (isFunction(itemKey)) {\n      return itemKey(item);\n    }\n    if (isString(itemKey) && isPlainObject(item)) {\n      return (item as any)[itemKey];\n    }\n\n    return item as React.Key;\n  };\n\n  const [selected, setSelected] = useState<T[]>(defaultSelected);\n\n  const selectedMap = useMemo(() => {\n    const keyToItemMap = new Map<React.Key, T>();\n\n    if (!Array.isArray(selected)) {\n      return keyToItemMap;\n    }\n\n    selected.forEach((item) => {\n      keyToItemMap.set(getKey(item), item);\n    });\n\n    return keyToItemMap;\n  }, [selected]);\n\n  const isSelected = (item: T) => selectedMap.has(getKey(item));\n\n  const select = (item: T) => {\n    selectedMap.set(getKey(item), item);\n    setSelected(Array.from(selectedMap.values()));\n  };\n\n  const unSelect = (item: T) => {\n    selectedMap.delete(getKey(item));\n    setSelected(Array.from(selectedMap.values()));\n  };\n\n  const toggle = (item: T) => {\n    if (isSelected(item)) {\n      unSelect(item);\n    } else {\n      select(item);\n    }\n  };\n\n  const selectAll = () => {\n    items.forEach((item) => {\n      selectedMap.set(getKey(item), item);\n    });\n    setSelected(Array.from(selectedMap.values()));\n  };\n\n  const unSelectAll = () => {\n    items.forEach((item) => {\n      selectedMap.delete(getKey(item));\n    });\n    setSelected(Array.from<T>(selectedMap.values()));\n  };\n\n  const noneSelected = useMemo<boolean>(\n    () => items.every((item) => !selectedMap.has(getKey(item))),\n    [items, selectedMap],\n  );\n\n  const allSelected = useMemo<boolean>(\n    () => items.every((item) => selectedMap.has(getKey(item))) && !noneSelected,\n    [items, selectedMap, noneSelected],\n  );\n\n  const partiallySelected = useMemo<boolean>(\n    () => !noneSelected && !allSelected,\n    [noneSelected, allSelected],\n  );\n\n  const toggleAll = () => (allSelected ? unSelectAll() : selectAll());\n\n  const clearAll = () => {\n    selectedMap.clear();\n    setSelected([]);\n  };\n\n  return {\n    selected,\n    noneSelected,\n    allSelected,\n    partiallySelected,\n    setSelected,\n    isSelected,\n    select: useMemoizedFn(select),\n    unSelect: useMemoizedFn(unSelect),\n    toggle: useMemoizedFn(toggle),\n    selectAll: useMemoizedFn(selectAll),\n    unSelectAll: useMemoizedFn(unSelectAll),\n    clearAll: useMemoizedFn(clearAll),\n    toggleAll: useMemoizedFn(toggleAll),\n  } as const;\n}\n\nexport default useSelections;\n"
  },
  {
    "path": "packages/hooks/src/useSelections/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useSelections\n\n常见联动 Checkbox 逻辑封装，支持多选，单选，全选逻辑，还提供了是否选择，是否全选，是否半选的状态。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 对象数组\n\n<code src=\"./demo/demo2.tsx\" />\n\n### 分页多选\n\n<code src=\"./demo/demo3.tsx\" />\n\n## API\n\n```typescript\ninterface Options<T> {\n  defaultSelected?: T[];\n  itemKey?: string | ((item: T) => Key);\n}\n\n// >=3.8.0 可用，推荐的写法 ✅\nconst result: Result = useSelections<T>(items: T[], options?: Options<T>);\n\n// <4.0.0 可用，将会在 ahooks 4.0 中移除 🙅🏻‍♀️\nconst result: Result = useSelections<T>(items: T[], defaultSelected?: T[]);\n```\n\n### Params\n\n| 参数 | 说明 | 类型 | 默认值 |\n| --- | --- | --- | --- |\n| items | 元素列表 | `T[]` | - |\n| options | 可选配置项 | `Options` | - |\n\n### Options\n\n| 参数 | 说明 | 类型 | 默认值 |\n| --- | --- | --- | --- |\n| defaultSelected | 默认选择的数据 | `T[]` | `[]` |\n| itemKey | 数据项的唯一 key。一般来说，数据源是对象数组时，才需要指定该参数 | `string` \\| `(item: T) => React.Key` | - |\n\n### Result\n\n| 参数              | 说明                                                                                                                                                                                   | 类型                                                                |\n| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |\n| selected          | 已经选择的元素                                                                                                                                                                         | `T[]`                                                               |\n| allSelected       | 是否全选                                                                                                                                                                               | `boolean`                                                           |\n| noneSelected      | 是否一个都没有选择                                                                                                                                                                     | `boolean`                                                           |\n| partiallySelected | 是否半选                                                                                                                                                                               | `boolean`                                                           |\n| isSelected        | 是否被选择                                                                                                                                                                             | `(value: T) => boolean`                                             |\n| setSelected       | 选择多个元素。多次执行时，后面的返回值会覆盖前面的，因此如果希望合并多次操作的结果，需要手动处理：`setSelected((oldArray) => oldArray.concat(newArray))`                               | `(value: T[]) => void  \\| (value: (prevState: T[]) => T[]) => void` |\n| select            | 选择单个元素                                                                                                                                                                           | `(value: T) => void`                                                |\n| unSelect          | 取消选择单个元素                                                                                                                                                                       | `(value: T) => void`                                                |\n| toggle            | 反选单个元素                                                                                                                                                                           | `(value: T) => void`                                                |\n| selectAll         | 选择全部元素                                                                                                                                                                           | `() => void`                                                        |\n| unSelectAll       | 取消选择全部元素                                                                                                                                                                       | `() => void`                                                        |\n| toggleAll         | 反选全部元素                                                                                                                                                                           | `() => void`                                                        |\n| clearAll          | 清除所有选中元素（一般情况下，`clearAll` 等价于 `unSelectAll`。如果元素列表是动态的，则 `clearAll` 会清除掉“所有选中过的元素”，而 `unSelectAll` 只会清除掉“当前元素列表里选中的元素”） | `() => void`                                                        |\n"
  },
  {
    "path": "packages/hooks/src/useSessionStorageState/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useSessionStorageState from '../index';\n\ndescribe('useSessionStorageState', () => {\n  const setUp = <T>(key: string, value: T) =>\n    renderHook(() => {\n      const [state, setState] = useSessionStorageState<T>(key, { defaultValue: value });\n      return {\n        state,\n        setState,\n      } as const;\n    });\n\n  test('should support object', () => {\n    const LOCAL_STORAGE_KEY = 'test-object-key';\n    const hook = setUp<{ name: string }>(LOCAL_STORAGE_KEY, {\n      name: 'A',\n    });\n    expect(hook.result.current.state).toEqual({ name: 'A' });\n    act(() => {\n      hook.result.current.setState({ name: 'B' });\n    });\n    expect(hook.result.current.state).toEqual({ name: 'B' });\n    const anotherHook = setUp(LOCAL_STORAGE_KEY, {\n      name: 'C',\n    });\n    expect(anotherHook.result.current.state).toEqual({ name: 'B' });\n    act(() => {\n      anotherHook.result.current.setState({\n        name: 'C',\n      });\n    });\n    expect(anotherHook.result.current.state).toEqual({ name: 'C' });\n    expect(hook.result.current.state).toEqual({ name: 'B' });\n  });\n\n  test('should support function updater', () => {\n    const LOCAL_STORAGE_KEY = 'test-func-updater';\n    const hook = setUp<string | null>(LOCAL_STORAGE_KEY, 'hello world');\n    expect(hook.result.current.state).toBe('hello world');\n    act(() => {\n      hook.result.current.setState((state) => `${state}, zhangsan`);\n    });\n    expect(hook.result.current.state).toBe('hello world, zhangsan');\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useSessionStorageState/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useSessionStorageState\n\nA Hook for store state into sessionStorage.\n\nUsage is exactly the same as [useLocalStorageState](./use-local-storage-state).\n"
  },
  {
    "path": "packages/hooks/src/useSessionStorageState/index.ts",
    "content": "import { createUseStorageState } from '../createUseStorageState';\nimport isBrowser from '../utils/isBrowser';\n\nconst useSessionStorageState = createUseStorageState(() =>\n  isBrowser ? sessionStorage : undefined,\n);\n\nexport default useSessionStorageState;\n"
  },
  {
    "path": "packages/hooks/src/useSessionStorageState/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useSessionStorageState\n\n将状态存储在 sessionStorage 中的 Hook。\n\n用法与 [useLocalStorageState](./use-local-storage-state) 一致。\n"
  },
  {
    "path": "packages/hooks/src/useSet/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useSet from '../index';\n\nconst setUp = <K>(initialSet?: Iterable<K>) => renderHook(() => useSet(initialSet));\n\ndescribe('useSet', () => {\n  test('should init set and utils', () => {\n    const { result } = setUp([1, 2]);\n    const [set, utils] = result.current;\n\n    expect(set).toEqual(new Set([1, 2]));\n    expect(utils).toStrictEqual({\n      add: expect.any(Function),\n      remove: expect.any(Function),\n      reset: expect.any(Function),\n    });\n  });\n\n  test('should init empty set if no initial set provided', () => {\n    const { result } = setUp();\n    expect(result.current[0]).toEqual(new Set());\n\n    const { result: result1 } = setUp(undefined);\n    expect(result1.current[0]).toEqual(new Set());\n  });\n\n  test('should have an initially provided key', () => {\n    const { result } = setUp(['a']);\n    const [set] = result.current;\n\n    let value = false;\n    act(() => {\n      value = set.has('a');\n    });\n\n    expect(value).toBe(true);\n  });\n\n  test('should have an added key', () => {\n    const { result } = setUp();\n\n    act(() => {\n      result.current[1].add('newKey');\n    });\n\n    let value = false;\n    act(() => {\n      value = result.current[0].has('newKey');\n    });\n\n    expect(value).toBe(true);\n  });\n\n  test('should get false for non-existing key', () => {\n    const { result } = setUp(['a']);\n    const [set] = result.current;\n\n    let value = true;\n    act(() => {\n      value = set.has('nonExisting');\n    });\n\n    expect(value).toBe(false);\n  });\n\n  test('should add a new key', () => {\n    const { result } = setUp(['oldKey']);\n    const [, utils] = result.current;\n\n    act(() => {\n      utils.add('newKey');\n    });\n\n    expect(result.current[0]).toEqual(new Set(['oldKey', 'newKey']));\n  });\n\n  test('should work if setting existing key', () => {\n    const { result } = setUp(['oldKey']);\n    const [, utils] = result.current;\n\n    act(() => {\n      utils.add('oldKey');\n    });\n\n    expect(result.current[0]).toEqual(new Set(['oldKey']));\n  });\n\n  test('should remove existing key', () => {\n    const { result } = setUp([1, 2]);\n    const [, utils] = result.current;\n\n    act(() => {\n      utils.remove(2);\n    });\n\n    expect(result.current[0]).toEqual(new Set([1]));\n  });\n\n  test('should do nothing if removing non-existing key', () => {\n    const { result } = setUp(['a', 'b']);\n    const [, utils] = result.current;\n\n    act(() => {\n      utils.remove('nonExisting');\n    });\n\n    expect(result.current[0]).toEqual(new Set(['a', 'b']));\n  });\n\n  test('should reset to initial set provided', () => {\n    const { result } = setUp([1]);\n    const [, utils] = result.current;\n\n    act(() => {\n      utils.add(2);\n    });\n\n    expect(result.current[0]).toEqual(new Set([1, 2]));\n\n    act(() => {\n      utils.reset();\n    });\n\n    expect(result.current[0]).toEqual(new Set([1]));\n  });\n\n  test('should memoized its utils methods', () => {\n    const { result } = setUp(['a', 'b']);\n    const [, utils] = result.current;\n    const { add, remove, reset } = utils;\n\n    act(() => {\n      add('foo');\n    });\n\n    expect(result.current[1].add).toBe(add);\n    expect(result.current[1].remove).toBe(remove);\n    expect(result.current[1].reset).toBe(reset);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useSet/demo/demo1.tsx",
    "content": "import { useSet } from 'ahooks';\n\nexport default () => {\n  const [set, { add, remove, reset }] = useSet(['Hello']);\n\n  return (\n    <div>\n      <button type=\"button\" onClick={() => add(String(Date.now()))}>\n        Add Timestamp\n      </button>\n      <button\n        type=\"button\"\n        onClick={() => remove('Hello')}\n        disabled={!set.has('Hello')}\n        style={{ margin: '0 8px' }}\n      >\n        Remove Hello\n      </button>\n      <button type=\"button\" onClick={() => reset()}>\n        Reset\n      </button>\n      <div style={{ marginTop: 16 }}>\n        <pre>{JSON.stringify(Array.from(set), null, 2)}</pre>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useSet/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useSet\n\nA hook that can manage the state of Set.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst [\n  set,\n  {\n    add,\n    remove,\n    reset\n  }\n] = useSet<K>(initialValue);\n```\n\n### Result\n\n| Property | Description      | Type               |\n| -------- | ---------------- | ------------------ |\n| set      | Set object       | `Set<K>`           |\n| add      | Add item         | `(key: K) => void` |\n| remove   | Remove item      | `(key: K) => void` |\n| reset    | Reset to default | `() => void`       |\n\n### Params\n\n| Property     | Description                 | Type          | Default |\n| ------------ | --------------------------- | ------------- | ------- |\n| initialValue | Optional, set default value | `Iterable<K>` | -       |\n"
  },
  {
    "path": "packages/hooks/src/useSet/index.ts",
    "content": "import { useState } from 'react';\nimport useMemoizedFn from '../useMemoizedFn';\n\nfunction useSet<K>(initialValue?: Iterable<K>) {\n  const getInitValue = () => new Set(initialValue);\n  const [set, setSet] = useState<Set<K>>(getInitValue);\n\n  const updateSet = (updater: (set: Set<K>) => Set<K>) => {\n    setSet((prevSet) => updater(new Set(prevSet)));\n  };\n\n  const add = (key: K) => {\n    if (set.has(key)) {\n      return;\n    }\n    updateSet((newSet) => {\n      newSet.add(key);\n      return newSet;\n    });\n  };\n\n  const remove = (key: K) => {\n    if (!set.has(key)) {\n      return;\n    }\n    updateSet((newSet) => {\n      newSet.delete(key);\n      return newSet;\n    });\n  };\n\n  const reset = () => setSet(getInitValue());\n\n  return [\n    set,\n    {\n      add: useMemoizedFn(add),\n      remove: useMemoizedFn(remove),\n      reset: useMemoizedFn(reset),\n    },\n  ] as const;\n}\n\nexport default useSet;\n"
  },
  {
    "path": "packages/hooks/src/useSet/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useSet\n\n管理 Set 类型状态的 Hook。\n\n## 代码演示\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst [\n  set,\n  {\n    add,\n    remove,\n    reset\n  }\n] = useSet<K>(initialValue);\n```\n\n### Result\n\n| 参数   | 说明         | 类型               |\n| ------ | ------------ | ------------------ |\n| set    | Set 对象     | `Set<K>`           |\n| add    | 添加元素     | `(key: K) => void` |\n| remove | 移除元素     | `(key: K) => void` |\n| reset  | 重置为默认值 | `() => void`       |\n\n### Params\n\n| 参数         | 说明                        | 类型          | 默认值 |\n| ------------ | --------------------------- | ------------- | ------ |\n| initialValue | 可选项，传入默认的 Set 参数 | `Iterable<K>` | -      |\n"
  },
  {
    "path": "packages/hooks/src/useSetState/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useSetState from '../index';\n\ndescribe('useSetState', () => {\n  const setUp = <T extends object>(initialValue: T) =>\n    renderHook(() => {\n      const [state, setState] = useSetState<T>(initialValue);\n      return {\n        state,\n        setState,\n      } as const;\n    });\n\n  test('should support initialValue', () => {\n    const hook = setUp({\n      hello: 'world',\n    });\n    expect(hook.result.current.state).toEqual({ hello: 'world' });\n  });\n\n  test('should support object', () => {\n    const hook = setUp<any>({\n      hello: 'world',\n    });\n    act(() => {\n      hook.result.current.setState({ foo: 'bar' });\n    });\n    expect(hook.result.current.state).toEqual({ hello: 'world', foo: 'bar' });\n  });\n\n  test('should support function update', () => {\n    const hook = setUp({\n      count: 0,\n    });\n    act(() => {\n      hook.result.current.setState((prev) => ({ count: prev.count + 1 }));\n    });\n    expect(hook.result.current.state).toEqual({ count: 1 });\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useSetState/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Automatically merge object.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 自动合并对象。\n */\n\nimport { useSetState } from 'ahooks';\n\ninterface State {\n  hello: string;\n  [key: string]: any;\n}\n\nexport default () => {\n  const [state, setState] = useSetState<State>({\n    hello: '',\n  });\n\n  return (\n    <div>\n      <pre>{JSON.stringify(state, null, 2)}</pre>\n      <p>\n        <button type=\"button\" onClick={() => setState({ hello: 'world' })}>\n          set hello\n        </button>\n        <button type=\"button\" onClick={() => setState({ foo: 'bar' })} style={{ margin: '0 8px' }}>\n          set foo\n        </button>\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useSetState/demo/demo2.tsx",
    "content": "/**\n * title: Updating with callback\n * desc: When using the callback to update, the previous state can be received, and the return value will be automatically merged.\n *\n * title.zh-CN: 使用回调更新\n * desc.zh-CN: 通过回调进行更新，可以获取上一次的状态，并且也会自动合并返回的对象。\n */\n\nimport { useSetState } from 'ahooks';\n\ninterface State {\n  hello: string;\n  count: number;\n}\n\nexport default () => {\n  const [state, setState] = useSetState<State>({\n    hello: 'world',\n    count: 0,\n  });\n\n  return (\n    <div>\n      <pre>{JSON.stringify(state, null, 2)}</pre>\n      <p>\n        <button type=\"button\" onClick={() => setState((prev) => ({ count: prev.count + 1 }))}>\n          count + 1\n        </button>\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useSetState/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useSetState\n\nuseSetState works similar to `this.setState` of class component, used to manage the state of object type.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Updating with callback\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst [state, setState] = useSetState<T>(initialState);\n```\n\n### Result\n\n| Property | Description          | Type                                                                                      | Default |\n| -------- | -------------------- | ----------------------------------------------------------------------------------------- | ------- |\n| state    | Current state        | `T`                                                                                       | -       |\n| setState | Update current state | `(state: Partial<T> \\| null) => void` \\| `((prevState: T) => Partial<T> \\| null) => void` | -       |\n\n### Params\n\n| Property     | Description   | Type           | Default |\n| ------------ | ------------- | -------------- | ------- |\n| initialState | Initial state | `T \\| () => T` | -       |\n"
  },
  {
    "path": "packages/hooks/src/useSetState/index.ts",
    "content": "import { useState } from 'react';\nimport useMemoizedFn from '../useMemoizedFn';\nimport { isFunction } from '../utils';\n\nexport type SetState<S extends Record<string, any>> = <K extends keyof S>(\n  state: Pick<S, K> | null | ((prevState: Readonly<S>) => Pick<S, K> | S | null),\n) => void;\n\nconst useSetState = <S extends Record<string, any>>(\n  initialState: S | (() => S),\n): [S, SetState<S>] => {\n  const [state, setState] = useState<S>(initialState);\n\n  const setMergeState = useMemoizedFn((patch) => {\n    setState((prevState) => {\n      const newState = isFunction(patch) ? patch(prevState) : patch;\n      return newState ? { ...prevState, ...newState } : prevState;\n    });\n  });\n\n  return [state, setMergeState];\n};\n\nexport default useSetState;\n"
  },
  {
    "path": "packages/hooks/src/useSetState/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useSetState\n\n管理 object 类型 state 的 Hooks，用法与 class 组件的 `this.setState` 基本一致。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 使用回调更新\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst [state, setState] = useSetState<T>(initialState);\n```\n\n### Result\n\n| 参数     | 说明         | 类型                                                                                      | 默认值 |\n| -------- | ------------ | ----------------------------------------------------------------------------------------- | ------ |\n| state    | 当前状态     | `T`                                                                                       | -      |\n| setState | 设置当前状态 | `(state: Partial<T> \\| null) => void` \\| `((prevState: T) => Partial<T> \\| null) => void` | -      |\n\n### Params\n\n| 参数         | 说明     | 类型           | 默认值 |\n| ------------ | -------- | -------------- | ------ |\n| initialState | 初始状态 | `T \\| () => T` | -      |\n"
  },
  {
    "path": "packages/hooks/src/useSize/__tests__/index.spec.tsx",
    "content": "import { act, render, renderHook, screen } from '@testing-library/react';\nimport { useRef } from 'react';\nimport { describe, expect, test, vi } from 'vitest';\nimport useSize from '../index';\n\nlet callback: any;\nvi.mock('resize-observer-polyfill', () => {\n  return {\n    default: vi.fn().mockImplementation((cb) => {\n      callback = cb;\n      return {\n        observe: () => {},\n        disconnect: () => {},\n      };\n    }),\n  };\n});\n\n// test about Resize Observer see https://github.com/que-etc/resize-observer-polyfill/tree/master/tests\ndescribe('useSize', () => {\n  test('should work when target is a mounted DOM', () => {\n    const hook = renderHook(() => useSize(document.body));\n    expect(hook.result.current).toEqual({ height: 0, width: 0 });\n  });\n\n  test('should work when target is a `MutableRefObject`', async () => {\n    const mockRaf = vi\n      .spyOn(window, 'requestAnimationFrame')\n      .mockImplementation((cb: FrameRequestCallback) => {\n        cb(0);\n        return 0;\n      });\n\n    function Setup() {\n      const ref = useRef(null);\n      const size = useSize(ref);\n\n      return (\n        <div ref={ref}>\n          <div>width: {String(size?.width)}</div>\n          <div>height: {String(size?.height)}</div>\n        </div>\n      );\n    }\n\n    render(<Setup />);\n    expect((await screen.findByText(/^width/)).textContent).toBe('width: undefined');\n    expect((await screen.findByText(/^height/)).textContent).toBe('height: undefined');\n\n    act(() => callback([{ target: { clientWidth: 10, clientHeight: 10 } }]));\n    expect((await screen.findByText(/^width/)).textContent).toBe('width: 10');\n    expect((await screen.findByText(/^height/)).textContent).toBe('height: 10');\n    mockRaf.mockRestore();\n  });\n\n  test('should not work when target is null', () => {\n    expect(() => {\n      renderHook(() => useSize(null));\n    }).not.toThrowError();\n  });\n\n  test('should work', () => {\n    const mockRaf = vi\n      .spyOn(window, 'requestAnimationFrame')\n      .mockImplementation((cb: FrameRequestCallback) => {\n        cb(0);\n        return 0;\n      });\n    const targetEl = document.createElement('div');\n    const { result } = renderHook(() => useSize(targetEl));\n\n    act(() => {\n      callback([\n        {\n          target: {\n            clientWidth: 100,\n            clientHeight: 50,\n          },\n        },\n      ]);\n    });\n\n    expect(result.current).toMatchObject({\n      width: 100,\n      height: 50,\n    });\n\n    mockRaf.mockRestore();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useSize/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: useSize can receive ref as argument\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: useSize 可以接收 ref 参数\n */\n\nimport { useRef } from 'react';\nimport { useSize } from 'ahooks';\n\nexport default () => {\n  const ref = useRef(null);\n  const size = useSize(ref);\n  return (\n    <div ref={ref}>\n      <p>Try to resize the preview window </p>\n      <p>\n        width: {size?.width}px, height: {size?.height}px\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useSize/demo/demo2.tsx",
    "content": "/**\n * title: pass in the DOM element\n * desc: useSize can receive a dom element as parameter. In SSR scenarios, you can pass in function `() => dom`\n *\n * title.zh-CN: 传入 DOM 元素\n * desc.zh-CN: useSize 可以接收 dom，在 SSR 场景可以传入函数 `() => dom`\n */\n\nimport { useSize } from 'ahooks';\n\nexport default () => {\n  const size = useSize(document.querySelector('body'));\n  return (\n    <div>\n      <p>Try to resize the preview window </p>\n      <p>\n        width: {size?.width}px, height: {size?.height}px\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useSize/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useSize\n\nA hook that observes size change of an element.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Pass in the DOM element\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst size = useSize(target);\n```\n\n### Params\n\n| Property | Description               | Type                                                          | Default |\n| -------- | ------------------------- | ------------------------------------------------------------- | ------- |\n| target   | DOM element or ref object | `Element` \\| `(() => Element)` \\| `MutableRefObject<Element>` | -       |\n\n### Result\n\n| Property | Description         | Type                                             | Default                                                                   |\n| -------- | ------------------- | ------------------------------------------------ | ------------------------------------------------------------------------- |\n| size     | Size of the element | `{ width: number, height: number } \\| undefined` | `{ width: target.clientWidth, height: target.clientHeight } \\| undefined` |\n"
  },
  {
    "path": "packages/hooks/src/useSize/index.ts",
    "content": "import ResizeObserver from 'resize-observer-polyfill';\nimport useRafState from '../useRafState';\nimport type { BasicTarget } from '../utils/domTarget';\nimport { getTargetElement } from '../utils/domTarget';\nimport useIsomorphicLayoutEffectWithTarget from '../utils/useIsomorphicLayoutEffectWithTarget';\n\ntype Size = { width: number; height: number };\n\nfunction useSize(target: BasicTarget): Size | undefined {\n  const [state, setState] = useRafState<Size | undefined>(() => {\n    const el = getTargetElement(target);\n    return el ? { width: el.clientWidth, height: el.clientHeight } : undefined;\n  });\n\n  useIsomorphicLayoutEffectWithTarget(\n    () => {\n      const el = getTargetElement(target);\n\n      if (!el) {\n        return;\n      }\n\n      const resizeObserver = new ResizeObserver((entries) => {\n        entries.forEach((entry) => {\n          const { clientWidth, clientHeight } = entry.target;\n          setState({ width: clientWidth, height: clientHeight });\n        });\n      });\n      resizeObserver.observe(el);\n      return () => {\n        resizeObserver.disconnect();\n      };\n    },\n    [],\n    target,\n  );\n\n  return state;\n}\n\nexport default useSize;\n"
  },
  {
    "path": "packages/hooks/src/useSize/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useSize\n\n监听 DOM 节点尺寸变化的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 传入 DOM 节点\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst size = useSize(target);\n```\n\n### Params\n\n| 参数   | 说明             | 类型                                                          | 默认值 |\n| ------ | ---------------- | ------------------------------------------------------------- | ------ |\n| target | DOM 节点或者 ref | `Element` \\| `(() => Element)` \\| `MutableRefObject<Element>` | -      |\n\n### Result\n\n| 参数 | 说明           | 类型                                             | 默认值                                                                    |\n| ---- | -------------- | ------------------------------------------------ | ------------------------------------------------------------------------- |\n| size | DOM 节点的尺寸 | `{ width: number, height: number } \\| undefined` | `{ width: target.clientWidth, height: target.clientHeight } \\| undefined` |\n"
  },
  {
    "path": "packages/hooks/src/useTextSelection/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useTextSelection from '../index';\n\n// test about Resize Observer see https://github.com/que-etc/resize-observer-polyfill/tree/master/tests\ndescribe('useTextSelection', () => {\n  function downMouse(x: number, y: number, options?: MouseEventInit) {\n    act(() => {\n      document.dispatchEvent(\n        new MouseEvent('mousedown', {\n          clientX: x,\n          clientY: y,\n          screenX: x,\n          screenY: y,\n          ...options,\n        }),\n      );\n    });\n  }\n\n  function upMouse(x: number, y: number) {\n    act(() => {\n      document.dispatchEvent(\n        new MouseEvent('mouseup', {\n          clientX: x,\n          clientY: y,\n          screenX: x,\n          screenY: y,\n        }),\n      );\n    });\n  }\n\n  function initGetSelection({ top = 0, left = 0, height = 0, width = 0, text = 'hello world!' }) {\n    // TODO\n    // @ts-ignore\n    window.getSelection = () => {\n      return {\n        toString: () => {\n          return text;\n        },\n        rangeCount: text.length,\n        removeAllRanges: () => {},\n        getRangeAt: (index: number) => {\n          return {\n            getBoundingClientRect: () => {\n              return {\n                top,\n                left,\n                bottom: top + height,\n                right: left + width,\n                height,\n                width,\n              };\n            },\n          };\n        },\n      };\n    };\n  }\n\n  test('on textSelection', async () => {\n    initGetSelection({ left: 10, top: 10, height: 100, width: 100, text: 'on textSelection' });\n\n    // TODO\n    // @ts-ignore\n    const hook = renderHook(() => useTextSelection(() => document));\n\n    expect(hook.result.current.text).toBe('');\n    expect(hook.result.current.left).toBeNaN();\n    expect(hook.result.current.right).toBeNaN();\n    expect(hook.result.current.top).toBeNaN();\n    expect(hook.result.current.bottom).toBeNaN();\n    expect(hook.result.current.height).toBeNaN();\n    expect(hook.result.current.width).toBeNaN();\n\n    downMouse(0, 0);\n    upMouse(100, 100);\n\n    expect(hook.result.current.left).toBe(10);\n    expect(hook.result.current.text).toBe('on textSelection');\n    hook.unmount();\n  });\n\n  test('keep/cancel the selected text range', async () => {\n    initGetSelection({ text: 'aaa' });\n\n    const hook = renderHook(() => useTextSelection(() => document));\n\n    expect(hook.result.current.text).toBe('');\n    downMouse(0, 0);\n    upMouse(100, 100);\n    expect(hook.result.current.text).toBe('aaa');\n\n    // trigger the secondary button of mouse (usually the right button)\n    downMouse(0, 0, { button: 2 });\n    expect(hook.result.current.text).toBe('aaa');\n\n    // // trigger the main button of mouse (usually the left button)\n    downMouse(0, 0, { button: 0 });\n    expect(hook.result.current.text).toBe('');\n\n    hook.unmount();\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useTextSelection/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Tracking content of user text selection\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 实时获取页面上选择的文本\n */\n\nimport { useTextSelection } from 'ahooks';\n\nexport default () => {\n  const { text } = useTextSelection();\n  return (\n    <div>\n      <p>You can select text all page.</p>\n      <p>Result：{text}</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useTextSelection/demo/demo2.tsx",
    "content": "/**\n * title: Translate user text selection\n * desc: Use Antd.Popover to translate user text selection\n *\n * title.zh-CN: 划词翻译\n * desc.zh-CN: 配合 Popover 做划词翻译\n */\n\nimport { useRequest, useTextSelection } from 'ahooks';\nimport { Popover, Spin } from 'antd';\nimport { useEffect, useState } from 'react';\n\nconst getResult = (keyword: string): Promise<string> => {\n  const trimedText = keyword.trim() !== '';\n  if (!trimedText) {\n    return Promise.resolve('');\n  }\n  return new Promise((resolve) => {\n    setTimeout(() => resolve(`[translate result] ${keyword}`), 2000);\n  });\n};\n\nexport default () => {\n  const {\n    text = '',\n    left = 0,\n    top = 0,\n    height = 0,\n    width = 0,\n  } = useTextSelection(() => document.querySelector('#translate-dom'));\n\n  const [open, setOpen] = useState<boolean>(false);\n\n  const { data, run, loading } = useRequest(getResult, {\n    manual: true,\n  });\n\n  useEffect(() => {\n    if (text.trim() === '') {\n      setOpen(false);\n      return;\n    }\n    setOpen(true);\n    run(text);\n  }, [text]);\n\n  return (\n    <div>\n      <p id=\"translate-dom\" style={{ padding: 20, border: '1px solid' }}>\n        Translation of this paragraph;Translation of this paragraph;Translation of this paragraph;\n      </p>\n      <Popover\n        content={<Spin spinning={loading}>{loading ? 'Translating……' : data}</Spin>}\n        open={open}\n      >\n        <span\n          style={{\n            position: 'fixed',\n            top: `${top}px`,\n            left: `${left}px`,\n            height: `${height}px`,\n            width: `${width}px`,\n            pointerEvents: 'none',\n          }}\n        />\n      </Popover>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useTextSelection/demo/demo3.tsx",
    "content": "/**\n * title: Listen specified area\n * desc: useTextSelection can receive dom or ref, for listen specified area.\n *\n * title.zh-CN: 监听特定区域文本选择\n * desc.zh-CN: useTextSelection 可以接收 dom 或 ref，指定监听区域。\n */\n\nimport { useRef } from 'react';\nimport { useTextSelection } from 'ahooks';\n\nexport default () => {\n  const ref = useRef(null);\n  const selection = useTextSelection(ref);\n  return (\n    <div>\n      <div ref={ref} style={{ border: '1px solid', padding: 20 }}>\n        <p>Please swipe your mouse to select any text on this paragraph.</p>\n      </div>\n      <p style={{ wordWrap: 'break-word' }}>Result：{JSON.stringify(selection)}</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useTextSelection/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useTextSelection\n\nTracking content, size, position of user text selection.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Listen for specified area\n\n<code src=\"./demo/demo3.tsx\" />\n\n### Translate user text selection\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst state = useTextSelection(target?);\n```\n\n### Params\n\n| Property | Description        | Type                                                                                 | Default    |\n| -------- | ------------------ | ------------------------------------------------------------------------------------ | ---------- |\n| target   | DOM element or ref | `Element` \\| `Document` \\| `(() => Element\\Document)` \\| `MutableRefObject<Element>` | `document` |\n\n### Result\n\n| Property | Description                                    | Type    |\n| -------- | ---------------------------------------------- | ------- |\n| state    | Content, size, position of user text selection | `State` |\n\n### State\n\n| Property | Description                         | Type     |\n| -------- | ----------------------------------- | -------- |\n| text     | Selected text                       | `string` |\n| left     | The left coordinate value of text   | `number` |\n| right    | The right coordinate value of text  | `number` |\n| top      | The top coordinate value of text    | `number` |\n| bottom   | The bottom coordinate value of text | `number` |\n| height   | The height of text                  | `number` |\n| width    | The width of text                   | `number` |\n"
  },
  {
    "path": "packages/hooks/src/useTextSelection/index.ts",
    "content": "import { useRef, useState } from 'react';\nimport type { BasicTarget } from '../utils/domTarget';\nimport { getTargetElement } from '../utils/domTarget';\nimport useEffectWithTarget from '../utils/useEffectWithTarget';\n\ninterface Rect {\n  top: number;\n  left: number;\n  bottom: number;\n  right: number;\n  height: number;\n  width: number;\n}\nexport interface State extends Rect {\n  text: string;\n}\n\nconst initRect: Rect = {\n  top: NaN,\n  left: NaN,\n  bottom: NaN,\n  right: NaN,\n  height: NaN,\n  width: NaN,\n};\n\nconst initState: State = {\n  text: '',\n  ...initRect,\n};\n\nfunction getRectFromSelection(selection: Selection | null): Rect {\n  if (!selection) {\n    return initRect;\n  }\n\n  if (selection.rangeCount < 1) {\n    return initRect;\n  }\n  const range = selection.getRangeAt(0);\n  const { height, width, top, left, right, bottom } = range.getBoundingClientRect();\n  return {\n    height,\n    width,\n    top,\n    left,\n    right,\n    bottom,\n  };\n}\n\nfunction useTextSelection(target?: BasicTarget<Document | Element>): State {\n  const [state, setState] = useState(initState);\n\n  const stateRef = useRef(state);\n  const isInRangeRef = useRef(false);\n  stateRef.current = state;\n\n  useEffectWithTarget(\n    () => {\n      const el = getTargetElement(target, document);\n      if (!el) {\n        return;\n      }\n\n      const mouseupHandler = () => {\n        let selObj: Selection | null = null;\n        let text = '';\n        let rect = initRect;\n        if (!window.getSelection) {\n          return;\n        }\n        selObj = window.getSelection();\n        text = selObj ? selObj.toString() : '';\n        if (text && isInRangeRef.current) {\n          rect = getRectFromSelection(selObj);\n          setState({ ...state, text, ...rect });\n        }\n      };\n\n      // 任意点击都需要清空之前的 range\n      const mousedownHandler = (e: MouseEvent) => {\n        // 如果是鼠标右键需要跳过 这样选中的数据就不会被清空\n        if (e.button === 2) {\n          return;\n        }\n        if (!window.getSelection) {\n          return;\n        }\n        if (stateRef.current.text) {\n          setState({ ...initState });\n        }\n        isInRangeRef.current = false;\n        const selObj = window.getSelection();\n        if (!selObj) {\n          return;\n        }\n        selObj.removeAllRanges();\n        isInRangeRef.current = el.contains(e.target as Node);\n      };\n\n      el.addEventListener('mouseup', mouseupHandler);\n\n      document.addEventListener('mousedown', mousedownHandler);\n\n      return () => {\n        el.removeEventListener('mouseup', mouseupHandler);\n        document.removeEventListener('mousedown', mousedownHandler);\n      };\n    },\n    [],\n    target,\n  );\n\n  return state;\n}\n\nexport default useTextSelection;\n"
  },
  {
    "path": "packages/hooks/src/useTextSelection/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useTextSelection\n\n实时获取用户当前选取的文本内容及位置。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 监听特定区域文本选择\n\n<code src=\"./demo/demo3.tsx\" />\n\n### 划词翻译\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst state = useTextSelection(target?);\n```\n\n### Params\n\n| 参数   | 说明               | 类型                                                                                 | 默认值     |\n| ------ | ------------------ | ------------------------------------------------------------------------------------ | ---------- |\n| target | DOM element or ref | `Element` \\| `Document` \\| `(() => Element\\Document)` \\| `MutableRefObject<Element>` | `document` |\n\n### Result\n\n| 参数  | 说明                           | 类型    |\n| ----- | ------------------------------ | ------- |\n| state | DOM 节点内选取文本的内容和位置 | `State` |\n\n### State\n\n| 参数   | 说明             | 类型     |\n| ------ | ---------------- | -------- |\n| text   | 用户选取的文本值 | `string` |\n| left   | 文本的左坐标     | `number` |\n| right  | 文本的右坐标     | `number` |\n| top    | 文本的顶坐标     | `number` |\n| bottom | 文本的底坐标     | `number` |\n| height | 文本的高度       | `number` |\n| width  | 文本的宽度       | `number` |\n"
  },
  {
    "path": "packages/hooks/src/useTheme/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { beforeAll, describe, expect, test, vi } from 'vitest';\nimport useTheme from '../index';\n\ndescribe('useTheme', () => {\n  beforeAll(() => {\n    // Mock window.matchMedia\n    Object.defineProperty(window, 'matchMedia', {\n      writable: true,\n      value: vi.fn().mockImplementation((query) => ({\n        matches: false, // Default value, can be overridden for specific tests\n        media: query,\n        onchange: null,\n        addListener: vi.fn(), // Deprecated but often still present\n        removeListener: vi.fn(), // Deprecated\n        addEventListener: vi.fn(),\n        removeEventListener: vi.fn(),\n        dispatchEvent: vi.fn(),\n      })),\n    });\n  });\n\n  test('themeMode init', () => {\n    const { result } = renderHook(useTheme);\n    expect(result.current.themeMode).toBe('system');\n  });\n\n  test('setThemeMode light', () => {\n    const { result } = renderHook(useTheme);\n    act(() => result.current.setThemeMode('light'));\n    expect(result.current.theme).toBe('light');\n    expect(result.current.themeMode).toBe('light');\n  });\n\n  test('setThemeMode dark', () => {\n    const { result } = renderHook(useTheme);\n    act(() => result.current.setThemeMode('dark'));\n    expect(result.current.theme).toBe('dark');\n    expect(result.current.themeMode).toBe('dark');\n  });\n\n  test('setThemeMode system', () => {\n    const { result } = renderHook(useTheme);\n    act(() => result.current.setThemeMode('system'));\n    expect(result.current.themeMode).toBe('system');\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useTheme/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: The 'theme' is the system display theme (\"light\" or \"dark\"), the 'themeMode' can set 'theme' to \"light\" or \"dark\" or follow the system setting.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 'theme' 为系统当前显示主题（\"light\" 或 \"dark\"），'themeMode' 为当前主题设置（\"light\" 或 \"dark\" 或 \"system\"）。\n */\n\nimport { useTheme } from 'ahooks';\nexport default () => {\n  const { theme, themeMode, setThemeMode } = useTheme({\n    localStorageKey: 'themeMode',\n  });\n\n  return (\n    <>\n      <div>theme: {theme}</div>\n      <div>themeMode: {themeMode}</div>\n      <button\n        type=\"button\"\n        onClick={() => {\n          setThemeMode('dark');\n        }}\n      >\n        use dark theme\n      </button>\n      <button\n        type=\"button\"\n        onClick={() => {\n          setThemeMode('light');\n        }}\n      >\n        use light theme\n      </button>\n      <button\n        type=\"button\"\n        onClick={() => {\n          setThemeMode('system');\n        }}\n      >\n        follow the system\n      </button>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useTheme/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useTheme\n\nThis hook is used to get and set the theme, and store the `themeMode` into `localStorage`.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst { theme, themeMode, setThemeMode } = useTheme({\n  localStorageKey?: string;\n});\n```\n\n### Params\n\n| Property        | Description                                           | Type     | Default   |\n| --------------- | ----------------------------------------------------- | -------- | --------- |\n| localStorageKey | The key in localStorage to store selected theme mode | `string` | `undefined` |\n\n### Result\n\n| Property     | Description           | Type                                            | Default                                                                               |\n| ------------ | --------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------- |\n| theme        | current display theme | `\"light\"   \\| \"dark\"`                           | if themeMode is \"system\" then equals to system setting，otherwise equals to themeMode |\n| themeMode    | selected theme mode   | `\"light\" \\| \"dark\" \\| \"system\"`                 | equals to localStorage \"themeMode\", otherwise equals to \"system\"                      |\n| setThemeMode | select theme mode     | `(mode: \"light\" \\| \"dark\" \\| \"system\") => void` |                                                                                       |\n"
  },
  {
    "path": "packages/hooks/src/useTheme/index.ts",
    "content": "import { useEffect, useState } from 'react';\nimport useMemoizedFn from '../useMemoizedFn';\nimport isBrowser from '../utils/isBrowser';\n\nexport enum ThemeMode {\n  LIGHT = 'light',\n  DARK = 'dark',\n  SYSTEM = 'system',\n}\n\nexport type ThemeModeType = `${ThemeMode}`;\n\nexport type ThemeType = 'light' | 'dark';\n\nconst useCurrentTheme = () => {\n  const matchMedia = isBrowser ? window.matchMedia('(prefers-color-scheme: dark)') : undefined;\n  const [theme, setTheme] = useState<ThemeType>(() => {\n    if (isBrowser) {\n      return matchMedia?.matches ? ThemeMode.DARK : ThemeMode.LIGHT;\n    } else {\n      return ThemeMode.LIGHT;\n    }\n  });\n\n  useEffect(() => {\n    const onThemeChange: MediaQueryList['onchange'] = (event) => {\n      if (event.matches) {\n        setTheme(ThemeMode.DARK);\n      } else {\n        setTheme(ThemeMode.LIGHT);\n      }\n    };\n\n    matchMedia?.addEventListener('change', onThemeChange);\n\n    return () => {\n      matchMedia?.removeEventListener('change', onThemeChange);\n    };\n  }, []);\n\n  return theme;\n};\n\ntype Options = {\n  localStorageKey?: string;\n};\n\nexport default function useTheme(options: Options = {}) {\n  const { localStorageKey } = options;\n\n  const [themeMode, setThemeMode] = useState<ThemeModeType>(() => {\n    const preferredThemeMode =\n      localStorageKey?.length && (localStorage.getItem(localStorageKey) as ThemeModeType | null);\n\n    return preferredThemeMode || ThemeMode.SYSTEM;\n  });\n\n  const setThemeModeWithLocalStorage = (mode: ThemeModeType) => {\n    setThemeMode(mode);\n\n    if (localStorageKey?.length) {\n      localStorage.setItem(localStorageKey, mode);\n    }\n  };\n\n  const currentTheme = useCurrentTheme();\n  const theme = themeMode === ThemeMode.SYSTEM ? currentTheme : themeMode;\n\n  return {\n    theme,\n    themeMode,\n    setThemeMode: useMemoizedFn(setThemeModeWithLocalStorage),\n  };\n}\n"
  },
  {
    "path": "packages/hooks/src/useTheme/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useTheme\n\n获取并设置当前主题，并将 `themeMode` 存储在 `localStorage` 中。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst { theme, themeMode, setThemeMode } = useTheme({\n  localStorageKey?: string;\n});\n```\n\n### 参数\n\n| 参数            | 说明                                 | 类型     | 默认值    |\n| --------------- | ------------------------------------ | -------- | --------- |\n| localStorageKey | localStorage 中用于存放主题模式的键 | `string` | `undefined` |\n\n### 返回值\n\n| 值           | 说明           | 类型                                            | 默认值                                                                 |\n| ------------ | -------------- | ----------------------------------------------- | ---------------------------------------------------------------------- |\n| theme        | 当前显示的主题 | `\"light\"   \\| \"dark\"`                           | 若 themeMode 为 \"system\" 则为系统当前使用主题，否则与 themeMode 值相同 |\n| themeMode    | 选择的主题模式 | `\"light\" \\| \"dark\" \\| \"system\"`                 | 等于 localStorage \"themeMode\" 字段的值，否则为 \"system\"                |\n| setThemeMode | 选择主题模式   | `(mode: \"light\" \\| \"dark\" \\| \"system\") => void` |                                                                        |\n"
  },
  {
    "path": "packages/hooks/src/useThrottle/__tests__/index.spec.ts",
    "content": "import { act, type RenderHookResult, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport { sleep } from '../../utils/testingHelpers';\nimport useThrottle from '../index';\n\nlet hook: RenderHookResult<any, any>;\n\ndescribe('useThrottle', () => {\n  test('default useThrottle should work', async () => {\n    let mountedState = 1;\n    act(() => {\n      hook = renderHook(() => useThrottle(mountedState, { wait: 500 }));\n    });\n\n    expect(hook.result.current).toBe(1);\n    mountedState = 2;\n    hook.rerender();\n    mountedState = 3;\n    hook.rerender();\n    await act(async () => {\n      await sleep(250);\n    });\n    expect(hook.result.current).toBe(1);\n    mountedState = 4;\n    hook.rerender();\n    await act(async () => {\n      await sleep(260);\n    });\n    expect(hook.result.current).toBe(4);\n  });\n\n  test('leading:false & trailing:false of options useThrottle should work', async () => {\n    let mountedState = 0;\n    act(() => {\n      hook = renderHook(() =>\n        useThrottle(mountedState, {\n          wait: 500,\n          leading: false,\n          trailing: false,\n        }),\n      );\n    });\n\n    //Never get the latest value\n    mountedState = 1;\n    expect(hook.result.current).toBe(0);\n    mountedState = 2;\n    hook.rerender();\n    mountedState = 3;\n    hook.rerender();\n    await sleep(250);\n    expect(hook.result.current).toBe(0);\n    mountedState = 4;\n    hook.rerender();\n    await sleep(260);\n    expect(hook.result.current).toBe(0);\n  });\n\n  test('leading:true & trailing:false of options useThrottle should work', async () => {\n    let mountedState = 0;\n    act(() => {\n      hook = renderHook(() =>\n        useThrottle(mountedState, { wait: 500, leading: true, trailing: false }),\n      );\n    });\n\n    expect(hook.result.current).toBe(0);\n    mountedState = 1;\n    hook.rerender();\n    await sleep(0);\n    expect(hook.result.current).toBe(0);\n\n    mountedState = 2;\n    await sleep(200);\n    hook.rerender();\n    await sleep(0);\n    expect(hook.result.current).toBe(0);\n\n    mountedState = 3;\n    //Need to wait more than 500ms to get the latest value\n    await act(async () => {\n      await sleep(300);\n    });\n    hook.rerender();\n    await sleep(0);\n    expect(hook.result.current).toBe(3);\n  });\n\n  test('leading:false & trailing:true of options useThrottle should work', async () => {\n    let mountedState = 0;\n    act(() => {\n      hook = renderHook(() =>\n        useThrottle(mountedState, { wait: 500, leading: false, trailing: true }),\n      );\n    });\n\n    expect(hook.result.current).toBe(0);\n    mountedState = 1;\n    hook.rerender();\n    await sleep(0);\n    expect(hook.result.current).toBe(0);\n\n    mountedState = 2;\n    hook.rerender();\n    await sleep(250);\n    expect(hook.result.current).toBe(0);\n\n    mountedState = 3;\n    hook.rerender();\n    await act(async () => {\n      await sleep(260);\n    });\n    await sleep(260);\n    expect(hook.result.current).toBe(3);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useThrottle/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: ThrottledValue will change every 500ms.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: ThrottledValue 每隔 500ms 变化一次。\n */\n\nimport { useState } from 'react';\nimport { useThrottle } from 'ahooks';\n\nexport default () => {\n  const [value, setValue] = useState<string>();\n  const throttledValue = useThrottle(value, { wait: 500 });\n\n  return (\n    <div>\n      <input\n        value={value}\n        onChange={(e) => setValue(e.target.value)}\n        placeholder=\"Typed value\"\n        style={{ width: 280 }}\n      />\n      <p style={{ marginTop: 16 }}>throttledValue: {throttledValue}</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useThrottle/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useThrottle\n\nA hook that deal with the throttled value.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst throttledValue = useThrottle(\n  value: any,\n  options?: Options\n);\n```\n\n### Params\n\n| Property | Description                        | Type      | Default |\n| -------- | ---------------------------------- | --------- | ------- |\n| value    | The value to throttle.             | `any`     | -       |\n| options  | Config for the throttle behaviors. | `Options` | -       |\n\n### Options\n\n| Property | Description                                           | Type      | Default |\n| -------- | ----------------------------------------------------- | --------- | ------- |\n| wait     | The number of milliseconds to delay.                  | `number`  | `1000`  |\n| leading  | Specify invoking on the leading edge of the timeout.  | `boolean` | `true`  |\n| trailing | Specify invoking on the trailing edge of the timeout. | `boolean` | `true`  |\n"
  },
  {
    "path": "packages/hooks/src/useThrottle/index.ts",
    "content": "import { useEffect, useState } from 'react';\nimport useThrottleFn from '../useThrottleFn';\nimport type { ThrottleOptions } from './throttleOptions';\n\nfunction useThrottle<T>(value: T, options?: ThrottleOptions) {\n  const [throttled, setThrottled] = useState(value);\n\n  const { run } = useThrottleFn(() => {\n    setThrottled(value);\n  }, options);\n\n  useEffect(() => {\n    run();\n  }, [value]);\n\n  return throttled;\n}\n\nexport default useThrottle;\n"
  },
  {
    "path": "packages/hooks/src/useThrottle/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useThrottle\n\n用来处理节流值的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst throttledValue = useThrottle(\n  value: any,\n  options?: Options\n);\n```\n\n### Params\n\n| 参数    | 说明           | 类型      | 默认值 |\n| ------- | -------------- | --------- | ------ |\n| value   | 需要节流的值   | `any`     | -      |\n| options | 配置节流的行为 | `Options` | -      |\n\n### Options\n\n| 参数     | 说明                     | 类型      | 默认值 |\n| -------- | ------------------------ | --------- | ------ |\n| wait     | 等待时间，单位为毫秒     | `number`  | `1000` |\n| leading  | 是否在延迟开始前调用函数 | `boolean` | `true` |\n| trailing | 是否在延迟开始后调用函数 | `boolean` | `true` |\n"
  },
  {
    "path": "packages/hooks/src/useThrottle/throttleOptions.ts",
    "content": "export interface ThrottleOptions {\n  wait?: number;\n  leading?: boolean;\n  trailing?: boolean;\n}\n"
  },
  {
    "path": "packages/hooks/src/useThrottleEffect/__tests__/index.spec.ts",
    "content": "import { act, type RenderHookResult, renderHook } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\nimport { sleep } from '../../utils/testingHelpers';\nimport useThrottleEffect from '../index';\n\nlet hook: RenderHookResult<any, any>;\n\ndescribe('useThrottleEffect', () => {\n  test('useThrottleEffect should work', async () => {\n    const mockEffect = vi.fn(() => {});\n    const mockCleanUp = vi.fn(() => {});\n    act(() => {\n      hook = renderHook(\n        ({ value, wait }) =>\n          useThrottleEffect(\n            () => {\n              mockEffect();\n              return () => {\n                mockCleanUp();\n              };\n            },\n            [value],\n            { wait },\n          ),\n        { initialProps: { value: 1, wait: 200 } },\n      );\n    });\n\n    hook.rerender({ value: 2, wait: 200 });\n    await sleep(100);\n    expect(mockEffect.mock.calls.length).toBe(1);\n    expect(mockCleanUp.mock.calls.length).toBe(0);\n    await act(async () => {\n      await sleep(150);\n    });\n    expect(mockEffect.mock.calls.length).toBe(2);\n    expect(mockCleanUp.mock.calls.length).toBe(1);\n\n    hook.rerender({ value: 3, wait: 100 });\n    await sleep(50);\n    expect(mockEffect.mock.calls.length).toBe(3);\n    expect(mockCleanUp.mock.calls.length).toBe(2);\n    await act(async () => {\n      await sleep(100);\n    });\n    expect(mockEffect.mock.calls.length).toBe(3);\n    expect(mockCleanUp.mock.calls.length).toBe(2);\n  });\n\n  test('should cancel timeout on unmount', async () => {\n    const mockEffect = vi.fn(() => {});\n    const mockCleanUp = vi.fn(() => {});\n\n    const hook2 = renderHook(\n      (props) =>\n        useThrottleEffect(\n          () => {\n            mockEffect();\n            return () => {\n              mockCleanUp();\n            };\n          },\n          [props],\n          { wait: 200 },\n        ),\n      { initialProps: 0 },\n    );\n\n    await act(async () => {\n      expect(mockEffect.mock.calls.length).toBe(1);\n      expect(mockCleanUp.mock.calls.length).toBe(0);\n\n      hook2.rerender(1);\n      await sleep(50);\n      hook2.unmount();\n\n      expect(mockEffect.mock.calls.length).toBe(1);\n      expect(mockCleanUp.mock.calls.length).toBe(1);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useThrottleEffect/demo/demo1.tsx",
    "content": "import { useState } from 'react';\nimport { useThrottleEffect } from 'ahooks';\n\nexport default () => {\n  const [value, setValue] = useState('hello');\n  const [records, setRecords] = useState<string[]>([]);\n  useThrottleEffect(\n    () => {\n      setRecords((val) => [...val, value]);\n    },\n    [value],\n    {\n      wait: 1000,\n    },\n  );\n  return (\n    <div>\n      <input\n        value={value}\n        onChange={(e) => setValue(e.target.value)}\n        placeholder=\"Typed value\"\n        style={{ width: 280 }}\n      />\n      <p style={{ marginTop: 16 }}>\n        <ul>\n          {records.map((record, index) => (\n            <li key={index}>{record}</li>\n          ))}\n        </ul>\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useThrottleEffect/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useThrottleEffect\n\nThrottle your `useEffect`.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseThrottleEffect(\n  effect: EffectCallback,\n  deps?: DependencyList,\n  options?: Options\n);\n```\n\n### Params\n\n| Property | Description                                                  | Type             | Default |\n| -------- | ------------------------------------------------------------ | ---------------- | ------- |\n| effect   | The effect callback.                                         | `EffectCallback` | -       |\n| deps     | The dependencies list.                                       | `DependencyList` | -       |\n| options  | Config the throttle behavior. See the Options section below. | `Options`        | -       |\n\n### Options\n\n| Property | Description                                           | Type      | Default |\n| -------- | ----------------------------------------------------- | --------- | ------- |\n| wait     | The number of milliseconds to wait.                   | `number`  | `1000`  |\n| leading  | Specify invoking on the leading edge of the timeout.  | `boolean` | `true`  |\n| trailing | Specify invoking on the trailing edge of the timeout. | `boolean` | `true`  |\n"
  },
  {
    "path": "packages/hooks/src/useThrottleEffect/index.ts",
    "content": "import { useEffect, useState } from 'react';\nimport type { DependencyList, EffectCallback } from 'react';\nimport type { ThrottleOptions } from '../useThrottle/throttleOptions';\nimport useThrottleFn from '../useThrottleFn';\nimport useUpdateEffect from '../useUpdateEffect';\n\nfunction useThrottleEffect(\n  effect: EffectCallback,\n  deps?: DependencyList,\n  options?: ThrottleOptions,\n) {\n  const [flag, setFlag] = useState({});\n\n  const { run } = useThrottleFn(() => {\n    setFlag({});\n  }, options);\n\n  useEffect(() => {\n    return run();\n  }, deps);\n\n  useUpdateEffect(effect, [flag]);\n}\n\nexport default useThrottleEffect;\n"
  },
  {
    "path": "packages/hooks/src/useThrottleEffect/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useThrottleEffect\n\n为 `useEffect` 增加节流的能力。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseThrottleEffect(\n  effect: EffectCallback,\n  deps?: DependencyList,\n  options?: Options\n);\n```\n\n### Params\n\n| 参数    | 说明                               | 类型             | 默认值 |\n| ------- | ---------------------------------- | ---------------- | ------ |\n| effect  | 执行函数                           | `EffectCallback` | -      |\n| deps    | 依赖数组                           | `DependencyList` | -      |\n| options | 配置节流的行为，详见下面的 Options | `Options`        | -      |\n\n### Options\n\n| 参数     | 说明                   | 类型      | 默认值 |\n| -------- | ---------------------- | --------- | ------ |\n| wait     | 等待时间，单位为毫秒   | `number`  | `1000` |\n| leading  | 是否在在延迟开始前调用 | `boolean` | `true` |\n| trailing | 是否在在延迟结束后调用 | `boolean` | `true` |\n"
  },
  {
    "path": "packages/hooks/src/useThrottleFn/__tests__/index.spec.ts",
    "content": "import { act, type RenderHookResult, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport { sleep } from '../../utils/testingHelpers';\nimport useThrottleFn from '../index';\n\ninterface ParamsObj {\n  fn: (...arg: any) => any;\n  deps?: any[];\n  wait: number;\n}\n\nconst setUp = ({ fn, wait }: ParamsObj) => renderHook(() => useThrottleFn(fn, { wait }));\n\nlet hook: RenderHookResult<any, any>;\n\ndescribe('useThrottleFn', () => {\n  test('run, cancel and flush should work', async () => {\n    let count = 0;\n    const throttleFn = (gap: number) => {\n      count += gap;\n    };\n    act(() => {\n      hook = setUp({\n        fn: throttleFn,\n        wait: 500,\n      });\n    });\n    await act(async () => {\n      hook.result.current.run(1);\n      expect(count).toBe(1);\n      hook.result.current.run(1);\n      hook.result.current.run(1);\n      hook.result.current.run(1);\n      expect(count).toBe(1);\n      await sleep(450); // t: 450\n      hook.result.current.run(2);\n      expect(count).toBe(1);\n      await sleep(100); // t: 550\n      hook.result.current.run(2);\n      expect(count).toBe(3);\n      hook.result.current.run(3);\n      hook.result.current.run(3);\n      await sleep(500); // t: 1050\n      expect(count).toBe(6);\n      hook.result.current.run(1);\n      hook.result.current.run(4);\n      hook.result.current.cancel();\n      await sleep(500); // t: 1550\n      expect(count).toBe(7);\n      hook.result.current.run(1);\n      hook.result.current.run(1);\n      expect(count).toBe(8);\n      hook.result.current.flush();\n      expect(count).toBe(9);\n      await sleep(550); // t: 2100\n      expect(count).toBe(9);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useThrottleFn/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Frequent calls run, but the function is only executed every 500ms.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 频繁调用 run，但只会每隔 500ms 执行一次相关函数。\n */\n\nimport { useState } from 'react';\nimport { useThrottleFn } from 'ahooks';\n\nexport default () => {\n  const [value, setValue] = useState(0);\n  const { run } = useThrottleFn(\n    () => {\n      setValue(value + 1);\n    },\n    { wait: 500 },\n  );\n\n  return (\n    <div>\n      <p style={{ marginTop: 16 }}> Clicked count: {value} </p>\n      <button type=\"button\" onClick={run}>\n        Click fast!\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useThrottleFn/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useThrottleFn\n\nA hook that deal with the throttled function.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst {\n  run,\n  cancel,\n  flush\n} = useThrottleFn(\n  fn: (...args: any[]) => any,\n  options?: Options\n);\n```\n\n### Params\n\n| Property | Description                       | Type                      | Default |\n| -------- | --------------------------------- | ------------------------- | ------- |\n| fn       | The function to throttle.         | `(...args: any[]) => any` | -       |\n| options  | Config for the throttle behaviors | `Options`                 | -       |\n\n### Options\n\n| Property | Description                                           | Type      | Default |\n| -------- | ----------------------------------------------------- | --------- | ------- |\n| wait     | The number of milliseconds to delay.                  | `number`  | `1000`  |\n| leading  | Specify invoking on the leading edge of the timeout.  | `boolean` | `true`  |\n| trailing | Specify invoking on the trailing edge of the timeout. | `boolean` | `true`  |\n\n### Result\n\n| Property | Description                                            | Type                      |\n| -------- | ------------------------------------------------------ | ------------------------- |\n| run      | Invoke and pass parameters to fn.                      | `(...args: any[]) => any` |\n| cancel   | Cancel the invocation of currently throttled function. | `() => void`              |\n| flush    | Immediately invoke currently throttled function        | `() => void`              |\n"
  },
  {
    "path": "packages/hooks/src/useThrottleFn/index.ts",
    "content": "import throttle from 'lodash/throttle';\nimport { useMemo } from 'react';\nimport useLatest from '../useLatest';\nimport type { ThrottleOptions } from '../useThrottle/throttleOptions';\nimport useUnmount from '../useUnmount';\nimport { isFunction } from '../utils';\nimport isDev from '../utils/isDev';\n\ntype noop = (...args: any[]) => any;\n\nfunction useThrottleFn<T extends noop>(fn: T, options?: ThrottleOptions) {\n  if (isDev) {\n    if (!isFunction(fn)) {\n      console.error(`useThrottleFn expected parameter is a function, got ${typeof fn}`);\n    }\n  }\n\n  const fnRef = useLatest(fn);\n\n  const wait = options?.wait ?? 1000;\n\n  const throttled = useMemo(\n    () =>\n      throttle(\n        (...args: Parameters<T>): ReturnType<T> => {\n          return fnRef.current(...args);\n        },\n        wait,\n        options,\n      ),\n    [],\n  );\n\n  useUnmount(() => {\n    throttled.cancel();\n  });\n\n  return {\n    run: throttled,\n    cancel: throttled.cancel,\n    flush: throttled.flush,\n  };\n}\n\nexport default useThrottleFn;\n"
  },
  {
    "path": "packages/hooks/src/useThrottleFn/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useThrottleFn\n\n用来处理函数节流的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst {\n  run,\n  cancel,\n  flush\n} = useThrottleFn(\n  fn: (...args: any[]) => any,\n  options?: Options\n);\n```\n\n### Params\n\n| 参数    | 说明           | 类型                      | 默认值 |\n| ------- | -------------- | ------------------------- | ------ |\n| fn      | 需要节流的函数 | `(...args: any[]) => any` | -      |\n| options | 配置节流的行为 | `Options`                 | -      |\n\n### Options\n\n| 参数     | 说明                     | 类型      | 默认值 |\n| -------- | ------------------------ | --------- | ------ |\n| wait     | 等待时间，单位为毫秒     | `number`  | `1000` |\n| leading  | 是否在延迟开始前调用函数 | `boolean` | `true` |\n| trailing | 是否在延迟开始后调用函数 | `boolean` | `true` |\n\n### Result\n\n| 参数   | 说明                               | 类型                      |\n| ------ | ---------------------------------- | ------------------------- |\n| run    | 触发执行 fn，函数参数将会传递给 fn | `(...args: any[]) => any` |\n| cancel | 取消当前节流                       | `() => void`              |\n| flush  | 当前节流立即调用                   | `() => void`              |\n"
  },
  {
    "path": "packages/hooks/src/useTimeout/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\nimport useTimeout from '../index';\n\ninterface ParamsObj {\n  fn: (...arg: any) => any;\n  delay?: number;\n}\n\nconst setUp = ({ fn, delay }: ParamsObj) => renderHook(() => useTimeout(fn, delay));\n\ndescribe('useTimeout', () => {\n  vi.useFakeTimers();\n  vi.spyOn(global, 'clearTimeout');\n\n  test('timeout should work', () => {\n    const callback = vi.fn();\n\n    setUp({ fn: callback, delay: 20 });\n\n    expect(callback).not.toBeCalled();\n    vi.advanceTimersByTime(70);\n    expect(callback).toHaveBeenCalledTimes(1);\n  });\n\n  test('timeout should stop', () => {\n    const callback = vi.fn();\n\n    setUp({ fn: callback, delay: undefined });\n    vi.advanceTimersByTime(50);\n    expect(callback).toHaveBeenCalledTimes(0);\n\n    setUp({ fn: callback, delay: -2 });\n    vi.advanceTimersByTime(50);\n    expect(callback).toHaveBeenCalledTimes(0);\n  });\n\n  test('timeout should be clear', () => {\n    const callback = vi.fn();\n\n    const hook = setUp({ fn: callback, delay: 20 });\n    expect(callback).not.toBeCalled();\n\n    hook.result.current();\n    vi.advanceTimersByTime(30);\n    expect(callback).toHaveBeenCalledTimes(0);\n    expect(clearTimeout).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useTimeout/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Execute once after 3000ms\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 3000ms 后执行一次\n */\n\nimport { useState } from 'react';\nimport { useTimeout } from 'ahooks';\n\nexport default () => {\n  const [state, setState] = useState(1);\n  useTimeout(() => {\n    setState(state + 1);\n  }, 3000);\n\n  return <div>{state}</div>;\n};\n"
  },
  {
    "path": "packages/hooks/src/useTimeout/demo/demo2.tsx",
    "content": "/**\n * title: Advanced usage\n * desc: Modify the delay to realize the timer timeout change and pause.\n *\n * title.zh-CN: 进阶使用\n * desc.zh-CN: 动态修改 delay 以实现定时器间隔变化与暂停。\n */\n\nimport { useState } from 'react';\nimport { useTimeout } from 'ahooks';\n\nexport default () => {\n  const [count, setCount] = useState(0);\n  const [delay, setDelay] = useState<number | undefined>(1000);\n\n  const clear = useTimeout(() => {\n    setCount(count + 1);\n  }, delay);\n\n  return (\n    <div>\n      <p> count: {count} </p>\n      <p style={{ marginTop: 16 }}> Delay: {delay} </p>\n      <button onClick={() => setDelay((t) => (!!t ? t + 1000 : 1000))} style={{ marginRight: 8 }}>\n        Delay + 1000\n      </button>\n      <button\n        style={{ marginRight: 8 }}\n        onClick={() => {\n          setDelay(1000);\n        }}\n      >\n        reset Delay\n      </button>\n      <button onClick={clear}>clear</button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useTimeout/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useTimeout\n\nA hook that handles the `setTimeout` timer function.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nuseTimeout(\n  fn: () => void,\n  delay?: number | undefined\n): fn: () => void;\n```\n\n### Params\n\n| Property | Description                                                                                                            | Type                    |\n| -------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------- |\n| fn       | The function to be executed after `delay` milliseconds.                                                                | `() => void`            |\n| delay    | The number of milliseconds to wait before executing the function. The timer will be cancelled if delay is `undefined`. | `number` \\| `undefined` |\n\n### Result\n\n| Property     | Description   | Type         |\n| ------------ | ------------- | ------------ |\n| clearTimeout | clear timeout | `() => void` |\n"
  },
  {
    "path": "packages/hooks/src/useTimeout/index.ts",
    "content": "import { useCallback, useEffect, useRef } from 'react';\nimport useMemoizedFn from '../useMemoizedFn';\nimport { isNumber } from '../utils';\n\nconst useTimeout = (fn: () => void, delay?: number) => {\n  const timerCallback = useMemoizedFn(fn);\n  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const clear = useCallback(() => {\n    if (timerRef.current) {\n      clearTimeout(timerRef.current);\n    }\n  }, []);\n\n  useEffect(() => {\n    if (!isNumber(delay) || delay < 0) {\n      return;\n    }\n    timerRef.current = setTimeout(timerCallback, delay);\n    return clear;\n  }, [delay]);\n\n  return clear;\n};\n\nexport default useTimeout;\n"
  },
  {
    "path": "packages/hooks/src/useTimeout/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useTimeout\n\n一个可以处理 setTimeout 计时器函数的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nuseTimeout(\n  fn: () => void,\n  delay?: number | undefined\n): fn: () => void;\n```\n\n### Params\n\n| 参数  | 说明                                                                       | 类型                    |\n| ----- | -------------------------------------------------------------------------- | ----------------------- |\n| fn    | 待执行函数                                                                 | `() => void`            |\n| delay | 定时时间（单位为毫秒）,支持动态变化，，当取值为 `undefined` 时会停止计时器 | `number` \\| `undefined` |\n\n### Result\n\n| 参数         | 说明       | 类型         |\n| ------------ | ---------- | ------------ |\n| clearTimeout | 清除定时器 | `() => void` |\n"
  },
  {
    "path": "packages/hooks/src/useTitle/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useTitle from '../index';\n\ndescribe('useTitle', () => {\n  test('should update document title', () => {\n    const hook = renderHook((props) => useTitle(props), { initialProps: 'Current Page Title' });\n\n    expect(document.title).toBe('Current Page Title');\n    act(() => {\n      hook.rerender('Other Page Title');\n    });\n    expect(document.title).toBe('Other Page Title');\n  });\n\n  test('should restore document title on unmount', () => {\n    document.title = 'Old Title';\n\n    const hook = renderHook((props) => useTitle(props, { restoreOnUnmount: true }), {\n      initialProps: 'Current Page Title',\n    });\n\n    expect(document.title).toBe('Current Page Title');\n\n    hook.unmount();\n    expect(document.title).toBe('Old Title');\n  });\n\n  test('should not restore document title on unmount', () => {\n    document.title = 'Old Title';\n\n    const hook = renderHook((props) => useTitle(props, { restoreOnUnmount: false }), {\n      initialProps: 'Current Page Title',\n    });\n\n    expect(document.title).toBe('Current Page Title');\n\n    hook.unmount();\n    expect(document.title).toBe('Current Page Title');\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useTitle/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Set title of the page.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 设置页面标题\n */\n\nimport { useTitle } from 'ahooks';\n\nexport default () => {\n  useTitle('Page Title');\n\n  return (\n    <div>\n      <p>Set title of the page.</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useTitle/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useTitle\n\nA hook that set title of the page.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseTitle(title: string, options?: Options);\n```\n\n### Params\n\n| Property | Description | Type     | Default |\n| -------- | ----------- | -------- | ------- |\n| title    | Page title  | `string` | -       |\n\n### Options\n\n| Property         | Description                                                                | Type      | Default |\n| ---------------- | -------------------------------------------------------------------------- | --------- | ------- |\n| restoreOnUnmount | Whether to restore the previous page title when the component is unmounted | `boolean` | `false` |\n"
  },
  {
    "path": "packages/hooks/src/useTitle/index.ts",
    "content": "import { useEffect, useRef } from 'react';\nimport useUnmount from '../useUnmount';\nimport isBrowser from '../utils/isBrowser';\n\nexport interface Options {\n  restoreOnUnmount?: boolean;\n}\n\nconst DEFAULT_OPTIONS: Options = {\n  restoreOnUnmount: false,\n};\n\nfunction useTitle(title: string, options: Options = DEFAULT_OPTIONS) {\n  const titleRef = useRef(isBrowser ? document.title : '');\n  useEffect(() => {\n    document.title = title;\n  }, [title]);\n\n  useUnmount(() => {\n    if (options.restoreOnUnmount) {\n      document.title = titleRef.current;\n    }\n  });\n}\n\nexport default useTitle;\n"
  },
  {
    "path": "packages/hooks/src/useTitle/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useTitle\n\n用于设置页面标题。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseTitle(title: string, options?: Options);\n```\n\n### Params\n\n| 参数  | 说明     | 类型     | 默认值 |\n| ----- | -------- | -------- | ------ |\n| title | 页面标题 | `string` | -      |\n\n### Options\n\n| 参数             | 说明                               | 类型      | 默认值  |\n| ---------------- | ---------------------------------- | --------- | ------- |\n| restoreOnUnmount | 组件卸载时，是否恢复上一个页面标题 | `boolean` | `false` |\n"
  },
  {
    "path": "packages/hooks/src/useToggle/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useToggle from '../index';\n\nconst callToggle = (hook: any) => {\n  act(() => {\n    hook.result.current[1].toggle();\n  });\n};\n\ndescribe('useToggle', () => {\n  test('test on init', async () => {\n    const hook = renderHook(() => useToggle());\n    expect(hook.result.current[0]).toBeFalsy();\n  });\n\n  test('test on methods', async () => {\n    const hook = renderHook(() => useToggle('Hello'));\n    expect(hook.result.current[0]).toBe('Hello');\n    callToggle(hook);\n    expect(hook.result.current[0]).toBeFalsy();\n    act(() => {\n      hook.result.current[1].setLeft();\n    });\n    expect(hook.result.current[0]).toBe('Hello');\n    act(() => {\n      hook.result.current[1].setRight();\n    });\n    expect(hook.result.current[0]).toBeFalsy();\n  });\n\n  test('test on optional', () => {\n    const hook = renderHook(() => useToggle('Hello', 'World'));\n    callToggle(hook);\n    expect(hook.result.current[0]).toBe('World');\n    act(() => {\n      hook.result.current[1].set('World');\n    });\n    expect(hook.result.current[0]).toBe('World');\n    callToggle(hook);\n    expect(hook.result.current[0]).toBe('Hello');\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useToggle/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Default value is boolean，alike useBoolean.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 默认为 boolean 切换，基础用法与 useBoolean 一致。\n */\n\nimport { useToggle } from 'ahooks';\n\nexport default () => {\n  const [state, { toggle, setLeft, setRight }] = useToggle();\n\n  return (\n    <div>\n      <p>Effects：{`${state}`}</p>\n      <p>\n        <button type=\"button\" onClick={toggle}>\n          Toggle\n        </button>\n        <button type=\"button\" onClick={setLeft} style={{ margin: '0 8px' }}>\n          Toggle False\n        </button>\n        <button type=\"button\" onClick={setRight}>\n          Toggle True\n        </button>\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useToggle/demo/demo2.tsx",
    "content": "/**\n * title: Toggle between any two values\n * desc: Accept two optional parameters and toggle between them.\n *\n * title.zh-CN: 在任意两个值之间切换\n * desc.zh-CN: 接受两个可选参数，在它们之间进行切换。\n */\n\nimport { useToggle } from 'ahooks';\n\nexport default () => {\n  const [state, { toggle, set, setLeft, setRight }] = useToggle('Hello', 'World');\n\n  return (\n    <div>\n      <p>Effects：{state}</p>\n      <p>\n        <button type=\"button\" onClick={toggle}>\n          Toggle\n        </button>\n        <button type=\"button\" onClick={() => set('Hello')} style={{ margin: '0 8px' }}>\n          Set Hello\n        </button>\n        <button type=\"button\" onClick={() => set('World')}>\n          Set World\n        </button>\n        <button type=\"button\" onClick={setLeft} style={{ margin: '0 8px' }}>\n          Set Left\n        </button>\n        <button type=\"button\" onClick={setRight}>\n          Set Right\n        </button>\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useToggle/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useToggle\n\nA hook that toggle states.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Advanced usage\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst [state, { toggle, set, setLeft, setRight }] = useToggle(defaultValue?: boolean);\n\nconst [state, { toggle, set, setLeft, setRight }] = useToggle<T>(defaultValue: T);\n\nconst [state, { toggle, set, setLeft, setRight }] = useToggle<T, U>(defaultValue: T, reverseValue: U)\n```\n\n### Params\n\n| Property     | Description                 | Type | Default |\n| ------------ | --------------------------- | ---- | ------- |\n| defaultValue | The default value. Optional | `T`  | `false` |\n| reverseValue | The reverse value. Optional | `U`  | -       |\n\n### Result\n\n| Property | Description                            | Type      |\n| -------- | -------------------------------------- | --------- |\n| state    | Current state                          | -         |\n| actions  | A set of methods to update state value | `Actions` |\n\n### Actions\n\n| Property | Description                                                                                                   | Type                      |\n| -------- | ------------------------------------------------------------------------------------------------------------- | ------------------------- |\n| toggle   | Toggle state                                                                                                  | `() => void`              |\n| set      | Set state                                                                                                     | `(state: T \\| U) => void` |\n| setLeft  | Set state to `defaultValue`                                                                                   | `() => void`              |\n| setRight | Set state to `reverseValue` if `reverseValue` is available. Otherwise set it to the reverse of `defaultValue` | `() => void`              |\n"
  },
  {
    "path": "packages/hooks/src/useToggle/index.ts",
    "content": "import { useMemo, useState } from 'react';\n\nexport interface Actions<T> {\n  setLeft: () => void;\n  setRight: () => void;\n  set: (value: T) => void;\n  toggle: () => void;\n}\n\nfunction useToggle<T = boolean>(): [boolean, Actions<T>];\n\nfunction useToggle<T>(defaultValue: T): [T, Actions<T>];\n\nfunction useToggle<T, U>(defaultValue: T, reverseValue: U): [T | U, Actions<T | U>];\n\nfunction useToggle<D, R>(defaultValue: D = false as unknown as D, reverseValue?: R) {\n  const [state, setState] = useState<D | R>(defaultValue);\n\n  const actions = useMemo(() => {\n    const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R;\n\n    const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));\n    const set = (value: D | R) => setState(value);\n    const setLeft = () => setState(defaultValue);\n    const setRight = () => setState(reverseValueOrigin);\n\n    return {\n      toggle,\n      set,\n      setLeft,\n      setRight,\n    };\n    // useToggle ignore value change\n    // }, [defaultValue, reverseValue]);\n  }, []);\n\n  return [state, actions];\n}\n\nexport default useToggle;\n"
  },
  {
    "path": "packages/hooks/src/useToggle/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useToggle\n\n用于在两个状态值间切换的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 高级用法\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst [state, { toggle, set, setLeft, setRight }] = useToggle(defaultValue?: boolean);\n\nconst [state, { toggle, set, setLeft, setRight }] = useToggle<T>(defaultValue: T);\n\nconst [state, { toggle, set, setLeft, setRight }] = useToggle<T, U>(defaultValue: T, reverseValue: U);\n```\n\n### Params\n\n| 参数         | 说明                     | 类型 | 默认值  |\n| ------------ | ------------------------ | ---- | ------- |\n| defaultValue | 可选项，传入默认的状态值 | `T`  | `false` |\n| reverseValue | 可选项，传入取反的状态值 | `U`  | -       |\n\n### Result\n\n| 参数    | 说明     | 类型      |\n| ------- | -------- | --------- |\n| state   | 状态值   | -         |\n| actions | 操作集合 | `Actions` |\n\n### Actions\n\n| 参数     | 说明                                                                            | 类型                      |\n| -------- | ------------------------------------------------------------------------------- | ------------------------- |\n| toggle   | 切换 state                                                                      | `() => void`              |\n| set      | 修改 state                                                                      | `(state: T \\| U) => void` |\n| setLeft  | 设置为 defaultValue                                                             | `() => void`              |\n| setRight | 如果传入了 reverseValue, 则设置为 reverseValue。 否则设置为 defaultValue 的反值 | `() => void`              |\n"
  },
  {
    "path": "packages/hooks/src/useTrackedEffect/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { beforeEach, describe, expect, test, vi } from 'vitest';\nimport useTrackedEffect from '../index';\n\ndescribe('useTrackedEffect', () => {\n  //We use a array to store which dependency has changed\n  let changedDepIndexes: number[] = [];\n  let prevDependencies: any[] = [];\n  let currentDependencies: any[] = [];\n  let mockEffectCleanup: any;\n  let mockEffectCallback: any;\n  let mockEffectWithTracked: any;\n\n  beforeEach(() => {\n    changedDepIndexes = [];\n    prevDependencies = [];\n    currentDependencies = [];\n    mockEffectCleanup = vi.fn();\n    mockEffectCallback = vi.fn().mockReturnValue(mockEffectCleanup);\n    mockEffectWithTracked = vi.fn().mockImplementation((changes, prevDeps, curDeps) => {\n      //This effect callback accept an addition parameter which contains indexes of dependencies which changed their equalities.\n      changedDepIndexes = changes;\n      prevDependencies = prevDeps;\n      currentDependencies = curDeps;\n      return mockEffectCleanup;\n    });\n  });\n\n  test(\"should run provided effect and return single changed dependency's index \", () => {\n    const deps = { var1: 0, var2: '0', var3: { value: 0 } };\n    const { rerender } = renderHook(() =>\n      useTrackedEffect(mockEffectWithTracked, [deps.var1, deps.var2, deps.var3]),\n    );\n    expect(mockEffectWithTracked).toHaveBeenCalledTimes(1);\n    rerender();\n    expect(changedDepIndexes).toHaveLength(3);\n    changedDepIndexes = [];\n    deps.var1++;\n    rerender();\n    expect(changedDepIndexes).toHaveLength(1);\n    expect(changedDepIndexes[0]).toBe(0);\n  });\n  test('should run provided effect and return correct dependencies (previous and current)', () => {\n    const deps = { var1: 0, var2: '0', var3: { value: 0 } };\n    const { rerender } = renderHook(() =>\n      useTrackedEffect(mockEffectWithTracked, [deps.var1, deps.var2, deps.var3]),\n    );\n    expect(mockEffectWithTracked).toHaveBeenCalledTimes(1);\n    rerender();\n    expect(changedDepIndexes).toHaveLength(3);\n    changedDepIndexes = [];\n    deps.var1++;\n    deps.var2 = '1';\n    rerender();\n    expect(prevDependencies[0]).toBe(0);\n    expect(currentDependencies[0]).toBe(1);\n    expect(prevDependencies[1] === '0').toBe(true);\n    expect(currentDependencies[1] === '1').toBe(true);\n    changedDepIndexes = [];\n    deps.var2 = '2';\n    rerender();\n    expect(prevDependencies[1]).toBe('1');\n    expect(currentDependencies[1]).toBe('2');\n  });\n  test(\" should run provided effect and return multiple changed dependecy's indexes\", () => {\n    const deps = { var1: 0, var2: '0', var3: { value: 0 } };\n    const { rerender } = renderHook(() =>\n      useTrackedEffect(mockEffectWithTracked, [deps.var1, deps.var2, deps.var3]),\n    );\n    expect(mockEffectWithTracked).toHaveBeenCalledTimes(1);\n    rerender();\n    expect(changedDepIndexes).toHaveLength(3);\n    changedDepIndexes = [];\n    deps.var1++;\n    deps.var2 = '1';\n    rerender();\n    expect(changedDepIndexes).toHaveLength(2);\n    expect(changedDepIndexes[0]).toBe(0);\n    expect(changedDepIndexes[1]).toBe(1);\n    changedDepIndexes = [];\n    deps.var2 = '2';\n    rerender();\n    expect(changedDepIndexes).toHaveLength(1);\n    expect(changedDepIndexes[0]).toBe(1);\n  });\n  test('should run provided effect and return empty if no dependency changed', () => {\n    const deps = { var1: 0, var2: '0', var3: { value: 0 } };\n    const { rerender } = renderHook(() =>\n      useTrackedEffect(mockEffectWithTracked, [deps.var1, deps.var2, deps.var3]),\n    );\n    expect(mockEffectWithTracked).toHaveBeenCalledTimes(1);\n    rerender();\n    expect(changedDepIndexes).toHaveLength(3);\n    changedDepIndexes = [];\n    deps.var1 = 0;\n    rerender();\n    expect(changedDepIndexes).toHaveLength(0);\n  });\n  test('should run provided effect and make sure reference equality is correct', () => {\n    const deps = { var1: 0, var2: '0', var3: { value: 0 } };\n    const { rerender } = renderHook(() =>\n      useTrackedEffect(mockEffectWithTracked, [deps.var1, deps.var2, deps.var3]),\n    );\n    expect(mockEffectWithTracked).toHaveBeenCalledTimes(1);\n    rerender();\n    expect(changedDepIndexes).toHaveLength(3);\n    changedDepIndexes = [];\n    deps.var3.value = 123;\n    rerender();\n    expect(changedDepIndexes).toHaveLength(0);\n  });\n\n  test('should run clean-up provided on unmount as a normal useEffect', () => {\n    const { unmount } = renderHook(() => useTrackedEffect(mockEffectCallback));\n    expect(mockEffectCleanup).not.toHaveBeenCalled();\n\n    unmount();\n    expect(mockEffectCleanup).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useTrackedEffect/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Display the changed deps when effect function is executed.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 查看每次 effect 执行时发生变化的依赖项\n */\n\nimport { useState } from 'react';\nimport { useTrackedEffect } from 'ahooks';\n\nexport default () => {\n  const [count, setCount] = useState(0);\n  const [count2, setCount2] = useState(0);\n\n  useTrackedEffect(\n    (changes) => {\n      console.log('Index of changed dependencies: ', changes);\n    },\n    [count, count2],\n  );\n\n  return (\n    <div>\n      <p>Please open the browser console to view the output!</p>\n      <div>\n        <p>Count: {count}</p>\n        <button onClick={() => setCount((c) => c + 1)}>count + 1</button>\n      </div>\n      <div style={{ marginTop: 16 }}>\n        <p>Count2: {count2}</p>\n        <button onClick={() => setCount2((c) => c + 1)}>count + 1</button>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useTrackedEffect/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useTrackedEffect\n\nA hook of useEffect that allow us to track which dependencies caused the effect to trigger.\n\n## Examples\n\n### Basic usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseTrackedEffect(\n  effect: (changes: [], previousDeps: [], currentDeps: []) => (void | (() => void | undefined)),\n  deps?: deps,\n)\n```\n\nThe API is alike `React.useEffect`, but the first function will receive three parameters: `changes`, `previousDeps`, and `currentDeps`.\n\n- changes: Index of changed dependencies\n- previousDeps: Last deps\n- currentDeps: Current deps\n"
  },
  {
    "path": "packages/hooks/src/useTrackedEffect/index.ts",
    "content": "import type { DependencyList } from 'react';\nimport { useEffect, useRef } from 'react';\n\ntype Effect<T extends DependencyList> = (\n  changes?: number[],\n  previousDeps?: T,\n  currentDeps?: T,\n) => void | (() => void);\n\nconst diffTwoDeps = (deps1?: DependencyList, deps2?: DependencyList) => {\n  // Let's do a reference equality check on 2 dependency list.\n  // If deps1 is defined, we iterate over deps1 and do comparison on each element with equivalent element from deps2\n  // As this func is used only in this hook, we assume 2 deps always have same length.\n  return deps1\n    ? deps1\n        .map((_, idx) => (!Object.is(deps1[idx], deps2?.[idx]) ? idx : -1))\n        .filter((ele) => ele >= 0)\n    : deps2\n      ? deps2.map((_, idx) => idx)\n      : [];\n};\n\nconst useTrackedEffect = <T extends DependencyList>(effect: Effect<T>, deps?: [...T]) => {\n  const previousDepsRef = useRef<T>(undefined);\n  useEffect(() => {\n    const changes = diffTwoDeps(previousDepsRef.current, deps);\n    const previousDeps = previousDepsRef.current;\n    previousDepsRef.current = deps;\n    return effect(changes, previousDeps, deps);\n  }, deps);\n};\n\nexport default useTrackedEffect;\n"
  },
  {
    "path": "packages/hooks/src/useTrackedEffect/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useTrackedEffect\n\n追踪是哪个依赖变化触发了 `useEffect` 的执行。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseTrackedEffect(\n  effect: (changes: [], previousDeps: [], currentDeps: []) => (void | (() => void | undefined)),\n  deps?: deps,\n)\n```\n\nAPI 与 `React.useEffect` 基本一致，不过第一个函数会接收 `changes`、`previousDeps`、`currentDeps` 三个参数。\n\n- changes：变化的依赖 index 数组\n- previousDeps：上一个依赖\n- currentDeps：当前依赖\n"
  },
  {
    "path": "packages/hooks/src/useUnmount/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { describe, expect, test, vi } from 'vitest';\nimport useUnmount from '../index';\n\ndescribe('useUnmount', () => {\n  test('useUnmount should work', async () => {\n    const fn = vi.fn();\n    const hook = renderHook(() => useUnmount(fn));\n    expect(fn).toBeCalledTimes(0);\n    hook.rerender();\n    expect(fn).toBeCalledTimes(0);\n    hook.unmount();\n    expect(fn).toBeCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useUnmount/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: The function is called right before the component is unmounted.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 在组件卸载时，执行函数。\n */\n\nimport { useBoolean, useUnmount } from 'ahooks';\nimport { message } from 'antd';\nconst MyComponent = () => {\n  useUnmount(() => {\n    message.info('unmount');\n  });\n\n  return <p>Hello World!</p>;\n};\n\nexport default () => {\n  const [state, { toggle }] = useBoolean(true);\n\n  return (\n    <>\n      <button type=\"button\" onClick={toggle}>\n        {state ? 'unmount' : 'mount'}\n      </button>\n      {state && <MyComponent />}\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useUnmount/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useUnmount\n\nA hook that executes the function right before the component is unmounted.\n\n## Examples\n\n### Default Usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseUnmount(fn: () => void);\n```\n\n### Params\n\n| Property | Description                 | Type         | Default |\n| -------- | --------------------------- | ------------ | ------- |\n| fn       | The function to be executed | `() => void` | -       |\n"
  },
  {
    "path": "packages/hooks/src/useUnmount/index.ts",
    "content": "import { useEffect } from 'react';\nimport useLatest from '../useLatest';\nimport { isFunction } from '../utils';\nimport isDev from '../utils/isDev';\n\nconst useUnmount = (fn: () => void) => {\n  if (isDev) {\n    if (!isFunction(fn)) {\n      console.error(`useUnmount expected parameter is a function, got ${typeof fn}`);\n    }\n  }\n\n  const fnRef = useLatest(fn);\n\n  useEffect(\n    () => () => {\n      fnRef.current();\n    },\n    [],\n  );\n};\n\nexport default useUnmount;\n"
  },
  {
    "path": "packages/hooks/src/useUnmount/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useUnmount\n\n在组件卸载（unmount）时执行的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nuseUnmount(fn: () => void);\n```\n\n### 参数\n\n| 参数 | 说明                 | 类型         | 默认值 |\n| ---- | -------------------- | ------------ | ------ |\n| fn   | 组件卸载时执行的函数 | `() => void` | -      |\n"
  },
  {
    "path": "packages/hooks/src/useUnmountedRef/__tests__/index.spec.ts",
    "content": "import { describe, expect, test } from 'vitest';\nimport { renderHook } from '../../utils/tests';\nimport useUnmountedRef from '../index';\n\ndescribe('useUnmountedRef', () => {\n  test('should work', async () => {\n    const hook = renderHook(() => useUnmountedRef());\n    expect(hook.result.current.current).toBe(false);\n    hook.rerender();\n    expect(hook.result.current.current).toBe(false);\n    hook.unmount();\n    expect(hook.result.current.current).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useUnmountedRef/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: unmountedRef.current means whether the component is unmounted\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: unmountedRef.current 代表组件是否已经卸载\n */\n\nimport { useBoolean, useUnmountedRef } from 'ahooks';\nimport { message } from 'antd';\nimport { useEffect } from 'react';\n\nconst MyComponent = () => {\n  const unmountedRef = useUnmountedRef();\n  useEffect(() => {\n    setTimeout(() => {\n      if (!unmountedRef.current) {\n        message.info('component is alive');\n      }\n    }, 3000);\n  }, []);\n\n  return <p>Hello World!</p>;\n};\n\nexport default () => {\n  const [state, { toggle }] = useBoolean(true);\n\n  return (\n    <>\n      <button type=\"button\" onClick={toggle}>\n        {state ? 'unmount' : 'mount'}\n      </button>\n      {state && <MyComponent />}\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useUnmountedRef/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useUnmountedRef\n\nA Hook can be used to get whether the component is unmounted.\n\n## Examples\n\n### Default Usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst unmountRef: { current: boolean } = useUnmountedRef();\n```\n\n### Result\n\n| Property   | Description                        | Type                   |\n| ---------- | ---------------------------------- | ---------------------- |\n| unmountRef | Whether the component is unmounted | `{ current: boolean }` |\n"
  },
  {
    "path": "packages/hooks/src/useUnmountedRef/index.tsx",
    "content": "import { useEffect, useRef } from 'react';\n\nconst useUnmountedRef = () => {\n  const unmountedRef = useRef(false);\n  useEffect(() => {\n    unmountedRef.current = false;\n    return () => {\n      unmountedRef.current = true;\n    };\n  }, []);\n  return unmountedRef;\n};\n\nexport default useUnmountedRef;\n"
  },
  {
    "path": "packages/hooks/src/useUnmountedRef/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useUnmountedRef\n\n获取当前组件是否已经卸载的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst unmountRef: { current: boolean } = useUnmountedRef();\n```\n\n### Result\n\n| 参数       | 说明             | 类型                   |\n| ---------- | ---------------- | ---------------------- |\n| unmountRef | 组件是否已经卸载 | `{ current: boolean }` |\n"
  },
  {
    "path": "packages/hooks/src/useUpdate/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useMemoizedFn from '../../useMemoizedFn';\nimport useUpdate from '..';\n\ndescribe('useUpdate', () => {\n  test('should update', () => {\n    let count = 0;\n    const hooks = renderHook(() => {\n      const update = useUpdate();\n      return {\n        update,\n        count,\n        onChange: useMemoizedFn(() => {\n          count++;\n          update();\n        }),\n      };\n    });\n    expect(hooks.result.current.count).toBe(0);\n    act(hooks.result.current.onChange);\n    expect(hooks.result.current.count).toBe(1);\n  });\n  test('should return same update function', () => {\n    const hooks = renderHook(() => useUpdate());\n    const preUpdate = hooks.result.current;\n    hooks.rerender();\n    expect(hooks.result.current).toEqual(preUpdate);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useUpdate/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: Forces component to re-render.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 强制组件重新渲染。\n */\n\nimport { useUpdate } from 'ahooks';\n\nexport default () => {\n  const update = useUpdate();\n\n  return (\n    <>\n      <div>Time: {Date.now()}</div>\n      <button type=\"button\" onClick={update} style={{ marginTop: 8 }}>\n        update\n      </button>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useUpdate/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useUpdate\n\nA hook that returns a function which can be used to force the component to re-render.\n\n## Examples\n\n### Default Usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst update = useUpdate();\n```\n"
  },
  {
    "path": "packages/hooks/src/useUpdate/index.ts",
    "content": "import { useState } from 'react';\nimport useMemoizedFn from '../useMemoizedFn';\n\nconst useUpdate = () => {\n  const [, setState] = useState({});\n\n  return useMemoizedFn(() => setState({}));\n};\n\nexport default useUpdate;\n"
  },
  {
    "path": "packages/hooks/src/useUpdate/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useUpdate\n\nuseUpdate 会返回一个函数，调用该函数会强制组件重新渲染。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nconst update = useUpdate();\n```\n"
  },
  {
    "path": "packages/hooks/src/useUpdateEffect/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useUpdateEffect from '../index';\n\ndescribe('useUpdateEffect', () => {\n  test('test on mounted', async () => {\n    let mountedState = 1;\n    const hook = renderHook(() =>\n      useUpdateEffect(() => {\n        mountedState = 2;\n      }),\n    );\n    expect(mountedState).toBe(1);\n    hook.rerender();\n    expect(mountedState).toBe(2);\n  });\n  test('test on optional', () => {\n    let mountedState = 1;\n    const hook = renderHook(() =>\n      useUpdateEffect(() => {\n        mountedState = 3;\n      }, [mountedState]),\n    );\n    expect(mountedState).toBe(1);\n    hook.rerender();\n    expect(mountedState).toBe(1);\n    mountedState = 2;\n    hook.rerender();\n    expect(mountedState).toBe(3);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useUpdateEffect/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: This hook is exactly the same as useEffect, except it skips running the effect for the first time.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 使用上与 useEffect 完全相同，只是它忽略了首次执行，只在依赖项更新时执行。\n */\n\nimport { useEffect, useState } from 'react';\nimport { useUpdateEffect } from 'ahooks';\n\nexport default () => {\n  const [count, setCount] = useState(0);\n  const [effectCount, setEffectCount] = useState(0);\n  const [updateEffectCount, setUpdateEffectCount] = useState(0);\n\n  useEffect(() => {\n    setEffectCount((c) => c + 1);\n  }, [count]);\n\n  useUpdateEffect(() => {\n    setUpdateEffectCount((c) => c + 1);\n    return () => {\n      // do something\n    };\n  }, [count]); // you can include deps array if necessary\n\n  return (\n    <div>\n      <p>effectCount: {effectCount}</p>\n      <p>updateEffectCount: {updateEffectCount}</p>\n      <p>\n        <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\n          reRender\n        </button>\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useUpdateEffect/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useUpdateEffect\n\nA hook alike `useEffect` but skips running the effect for the first time.\n\n## Examples\n\n### Basic usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\nThe API is exactly the same as `React.useEffect`.\n\n```typescript\nuseUpdateEffect(\n  effect: React.EffectCallback,\n  deps?: React.DependencyList,\n)\n```\n"
  },
  {
    "path": "packages/hooks/src/useUpdateEffect/index.ts",
    "content": "import { useEffect } from 'react';\nimport { createUpdateEffect } from '../createUpdateEffect';\n\nexport default createUpdateEffect(useEffect);\n"
  },
  {
    "path": "packages/hooks/src/useUpdateEffect/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useUpdateEffect\n\n`useUpdateEffect` 用法等同于 `useEffect`，但是会忽略首次执行，只在依赖更新时执行。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\nAPI 与 `React.useEffect` 完全一致。\n\n```typescript\nuseUpdateEffect(\n  effect: React.EffectCallback,\n  deps?: React.DependencyList,\n)\n```\n"
  },
  {
    "path": "packages/hooks/src/useUpdateLayoutEffect/__tests__/index.spec.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport useUpdateLayoutEffect from '../index';\n\ndescribe('useUpdateLayoutEffect', () => {\n  test('test on mounted', async () => {\n    let mountedState = 1;\n    const hook = renderHook(() =>\n      useUpdateLayoutEffect(() => {\n        mountedState = 2;\n      }),\n    );\n    expect(mountedState).toBe(1);\n    hook.rerender();\n    expect(mountedState).toBe(2);\n  });\n  test('test on optional', () => {\n    let mountedState = 1;\n    const hook = renderHook(() =>\n      useUpdateLayoutEffect(() => {\n        mountedState = 3;\n      }, [mountedState]),\n    );\n    expect(mountedState).toBe(1);\n    hook.rerender();\n    expect(mountedState).toBe(1);\n    mountedState = 2;\n    hook.rerender();\n    expect(mountedState).toBe(3);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useUpdateLayoutEffect/demo/demo1.tsx",
    "content": "/**\n * title: Basic usage\n * desc: This hook is exactly the same as useLayoutEffect, except it skips running the effect for the first time.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 使用上与 useLayoutEffect 完全相同，只是它忽略了首次执行，且只在依赖项更新时执行。\n */\n\nimport { useLayoutEffect, useState } from 'react';\nimport { useUpdateLayoutEffect } from 'ahooks';\n\nexport default () => {\n  const [count, setCount] = useState(0);\n  const [layoutEffectCount, setLayoutEffectCount] = useState(0);\n  const [updateLayoutEffectCount, setUpdateLayoutEffectCount] = useState(0);\n\n  useLayoutEffect(() => {\n    setLayoutEffectCount((c) => c + 1);\n  }, [count]);\n\n  useUpdateLayoutEffect(() => {\n    setUpdateLayoutEffectCount((c) => c + 1);\n    return () => {\n      // do something\n    };\n  }, [count]); // you can include deps array if necessary\n\n  return (\n    <div>\n      <p>layoutEffectCount: {layoutEffectCount}</p>\n      <p>updateLayoutEffectCount: {updateLayoutEffectCount}</p>\n      <p>\n        <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\n          reRender\n        </button>\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useUpdateLayoutEffect/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useUpdateLayoutEffect\n\nA hook alike `useLayoutEffect` but skips running the effect for the first time.\n\n## Examples\n\n### Basic usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\nThe API is exactly the same as `React.useLayoutEffect`.\n\n```typescript\nuseUpdateEffect(\n  effect: React.EffectCallback,\n  deps?: React.DependencyList,\n)\n```\n"
  },
  {
    "path": "packages/hooks/src/useUpdateLayoutEffect/index.ts",
    "content": "import { useLayoutEffect } from 'react';\nimport { createUpdateEffect } from '../createUpdateEffect';\n\nexport default createUpdateEffect(useLayoutEffect);\n"
  },
  {
    "path": "packages/hooks/src/useUpdateLayoutEffect/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useUpdateLayoutEffect\n\n`useUpdateLayoutEffect` 用法等同于 `useLayoutEffect`，但是会忽略首次执行，只在依赖更新时执行。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\nAPI 与 `React.useLayoutEffect` 完全一致。\n\n```typescript\nuseUpdateLayoutEffect(\n  effect: React.EffectCallback,\n  deps?: React.DependencyList,\n)\n```\n"
  },
  {
    "path": "packages/hooks/src/useVirtualList/__tests__/index.spec.ts",
    "content": "import { act, type RenderHookResult, renderHook } from '@testing-library/react';\nimport { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';\nimport type { Options } from '../index';\nimport useVirtualList from '../index';\n\ndescribe('useVirtualList', () => {\n  describe('virtual list render', () => {\n    let hook: RenderHookResult<any, any>;\n    let container: HTMLDivElement;\n    let wrapper: HTMLDivElement;\n\n    beforeEach(() => {\n      container = document.createElement('div');\n\n      // mock clientheight, clientWidth\n      // see: https://github.com/testing-library/react-testing-library/issues/353\n\n      vi.spyOn(container, 'clientHeight', 'get').mockImplementation(() => 300);\n      vi.spyOn(container, 'clientWidth', 'get').mockImplementation(() => 300);\n\n      wrapper = document.createElement('div');\n      container.appendChild(wrapper);\n\n      document.body.appendChild(container);\n    });\n\n    afterEach(() => {\n      document.body.removeChild(container);\n      hook.unmount();\n    });\n\n    const setup = (list: any[] = [], options: Options<any>) => {\n      hook = renderHook(() => useVirtualList(list, options));\n    };\n\n    test('test return list size', () => {\n      setup(Array.from(Array(99999).keys()), {\n        containerTarget: () => container,\n        wrapperTarget: () => wrapper,\n        itemHeight: 30,\n      });\n\n      act(() => {\n        hook.result.current[1](80);\n      });\n\n      // 10 items plus 5 overscan * 2\n      expect(hook.result.current[0].length).toBe(20);\n      expect(container.scrollTop).toBe(80 * 30);\n    });\n\n    test('test with fixed height', () => {\n      setup(Array.from(Array(99999).keys()), {\n        overscan: 0,\n        itemHeight: 30,\n        containerTarget: () => container,\n        wrapperTarget: () => wrapper,\n      });\n\n      act(() => {\n        hook.result.current[1](20);\n      });\n\n      expect(hook.result.current[0].length).toBe(10);\n      expect(container.scrollTop).toBe(20 * 30);\n      expect(hook.result.current[0][0].data).toBe(20);\n      expect(hook.result.current[0][0].index).toBe(20);\n    });\n\n    test('test with dynamic height', async () => {\n      const list = Array.from(Array(99999).keys());\n      setup(list, {\n        overscan: 0,\n        containerTarget: () => container,\n        wrapperTarget: () => wrapper,\n        itemHeight: (i: number, data) => {\n          expect(list[i] === data).toBe(true);\n          return i % 2 === 0 ? 30 : 60;\n        },\n      });\n\n      act(() => {\n        hook.result.current[1](20);\n      });\n\n      // average height for easy calculation\n      const averageHeight = (30 + 60) / 2;\n\n      expect(hook.result.current[0].length).toBe(Math.floor(300 / averageHeight));\n      expect(container.scrollTop).toBe(10 * 30 + 10 * 60);\n      expect((hook.result.current[0][0] as { data: number }).data).toBe(20);\n      expect((hook.result.current[0][0] as { index: number }).index).toBe(20);\n      expect((hook.result.current[0][5] as { data: number }).data).toBe(25);\n      expect((hook.result.current[0][5] as { index: number }).index).toBe(25);\n\n      expect(wrapper.style.marginTop).toBe(20 * averageHeight + 'px');\n      expect(wrapper.style.height).toBe((99998 - 20) * averageHeight + 30 + 'px');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useVirtualList/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: render 100,000 items in a list.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 渲染大量数据\n */\n\nimport { useMemo, useRef } from 'react';\nimport { useVirtualList } from 'ahooks';\n\nexport default () => {\n  const containerRef = useRef(null);\n  const wrapperRef = useRef(null);\n\n  const originalList = useMemo(() => Array.from(Array(99999).keys()), []);\n\n  const [list] = useVirtualList(originalList, {\n    containerTarget: containerRef,\n    wrapperTarget: wrapperRef,\n    itemHeight: 60,\n    overscan: 10,\n  });\n  return (\n    <>\n      <div ref={containerRef} style={{ height: '300px', overflow: 'auto', border: '1px solid' }}>\n        <div ref={wrapperRef}>\n          {list.map((ele) => (\n            <div\n              style={{\n                height: 52,\n                display: 'flex',\n                justifyContent: 'center',\n                alignItems: 'center',\n                border: '1px solid #e8e8e8',\n                marginBottom: 8,\n              }}\n              key={ele.index}\n            >\n              Row: {ele.data}\n            </div>\n          ))}\n        </div>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useVirtualList/demo/demo2.tsx",
    "content": "/**\n * title: Dynamic item height\n * desc: Specify item height dynamically.\n *\n * title.zh-CN: 动态元素高度\n * desc.zh-CN: 动态指定每个元素的高度\n */\n\nimport { useMemo, useRef, useState } from 'react';\nimport { useVirtualList } from 'ahooks';\n\nexport default () => {\n  const containerRef = useRef(null);\n  const wrapperRef = useRef(null);\n\n  const originalList = useMemo(() => Array.from(Array(99999).keys()), []);\n\n  const [value, onChange] = useState<number>(0);\n\n  const [list, scrollTo] = useVirtualList(originalList, {\n    containerTarget: containerRef,\n    wrapperTarget: wrapperRef,\n    itemHeight: (i) => (i % 2 === 0 ? 42 + 8 : 84 + 8),\n    overscan: 10,\n  });\n\n  return (\n    <div>\n      <div style={{ textAlign: 'right', marginBottom: 16 }}>\n        <input\n          style={{ width: 120 }}\n          placeholder=\"line number\"\n          type=\"number\"\n          value={value}\n          onChange={(e) => onChange(Number(e.target.value))}\n        />\n        <button\n          style={{ marginLeft: 8 }}\n          type=\"button\"\n          onClick={() => {\n            scrollTo(Number(value));\n          }}\n        >\n          scroll to\n        </button>\n      </div>\n      <div ref={containerRef} style={{ height: '300px', overflow: 'auto' }}>\n        <div ref={wrapperRef}>\n          {list.map((ele) => (\n            <div\n              style={{\n                height: ele.index % 2 === 0 ? 42 : 84,\n                display: 'flex',\n                justifyContent: 'center',\n                alignItems: 'center',\n                border: '1px solid #e8e8e8',\n                marginBottom: 8,\n              }}\n              key={ele.index}\n            >\n              Row: {ele.data} size: {ele.index % 2 === 0 ? 'small' : 'large'}\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useVirtualList/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useVirtualList\n\nA hook that allows you to use virtual list to render huge chunks of list data.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n### Dynamic item height\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst [list, scrollTo] = useVirtualList<T>(\n  originalList: T[],\n  options: {\n    containerTarget: (() => Element) | Element | MutableRefObject<Element>,\n    wrapperTarget: (() => Element) | Element | MutableRefObject<Element>,\n    itemHeight: number | ((index: number, data: T) => number),\n    overscan?: number,\n  }\n);\n```\n\n### Params\n\n| Property     | Description                                                                                                                                                | Type      | Default |\n| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------- |\n| originalList | The original list that contains a lot of data entries. **Attention: must undergo useMemo processing or never change, otherwise there will be a dead loop** | `T[]`     | `[]`    |\n| options      | config                                                                                                                                                     | `Options` | -       |\n\n### Options\n\n| Property        | Description                                                             | Type                                                        | Default |\n| --------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------- | ------- |\n| containerTarget | Outter Container，support DOM element or ref                            | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -       |\n| wrapperTarget   | Inner Container，DOM element or ref                                     | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -       |\n| itemHeight      | Item height, accept a pixel value or a function that returns the height | `number` \\| `((index: number, data: T) => number)`          | -       |\n| overscan        | The extra buffer items outside of the view area                         | `number`                                                    | `5`     |\n\n### Result\n\n| Property | Description                                            | Type                           |\n| -------- | ------------------------------------------------------ | ------------------------------ |\n| list     | The current portion of data need to be rendered to DOM | `{ data: T, index: number }[]` |\n| scrollTo | Scroll to specific index                               | `(index: number) => void`      |\n"
  },
  {
    "path": "packages/hooks/src/useVirtualList/index.ts",
    "content": "import { useEffect, useMemo, useState, useRef } from 'react';\nimport type { CSSProperties } from 'react';\nimport useEventListener from '../useEventListener';\nimport useLatest from '../useLatest';\nimport useMemoizedFn from '../useMemoizedFn';\nimport useSize from '../useSize';\nimport { getTargetElement } from '../utils/domTarget';\nimport type { BasicTarget } from '../utils/domTarget';\nimport { isNumber } from '../utils';\nimport useUpdateEffect from '../useUpdateEffect';\n\ntype ItemHeight<T> = (index: number, data: T) => number;\n\nexport interface Options<T> {\n  containerTarget: BasicTarget;\n  wrapperTarget: BasicTarget;\n  itemHeight: number | ItemHeight<T>;\n  overscan?: number;\n}\n\nconst useVirtualList = <T = any>(list: T[], options: Options<T>) => {\n  const { containerTarget, wrapperTarget, itemHeight, overscan = 5 } = options;\n\n  const itemHeightRef = useLatest(itemHeight);\n\n  const size = useSize(containerTarget);\n\n  const scrollTriggerByScrollToFunc = useRef(false);\n\n  const [targetList, setTargetList] = useState<{ index: number; data: T }[]>([]);\n\n  const [wrapperStyle, setWrapperStyle] = useState<CSSProperties>({});\n\n  const getVisibleCount = (containerHeight: number, fromIndex: number) => {\n    if (isNumber(itemHeightRef.current)) {\n      return Math.ceil(containerHeight / itemHeightRef.current);\n    }\n\n    let sum = 0;\n    let endIndex = 0;\n    for (let i = fromIndex; i < list.length; i++) {\n      const height = itemHeightRef.current(i, list[i]);\n      sum += height;\n      endIndex = i;\n      if (sum >= containerHeight) {\n        break;\n      }\n    }\n    return endIndex - fromIndex;\n  };\n\n  const getOffset = (scrollTop: number) => {\n    if (isNumber(itemHeightRef.current)) {\n      return Math.floor(scrollTop / itemHeightRef.current);\n    }\n    let sum = 0;\n    let offset = 0;\n    for (let i = 0; i < list.length; i++) {\n      const height = itemHeightRef.current(i, list[i]);\n      sum += height;\n      if (sum >= scrollTop) {\n        offset = i;\n        break;\n      }\n    }\n    return offset + 1;\n  };\n\n  // 获取上部高度\n  const getDistanceTop = (index: number) => {\n    if (isNumber(itemHeightRef.current)) {\n      const height = index * itemHeightRef.current;\n      return height;\n    }\n    const height = list\n      .slice(0, index)\n      .reduce<number>((sum, _, i) => sum + (itemHeightRef.current as ItemHeight<T>)(i, list[i]), 0);\n    return height;\n  };\n\n  const totalHeight = useMemo(() => {\n    if (isNumber(itemHeightRef.current)) {\n      return list.length * itemHeightRef.current;\n    }\n    return list.reduce<number>(\n      (sum, _, index) => sum + (itemHeightRef.current as ItemHeight<T>)(index, list[index]),\n      0,\n    );\n  }, [list]);\n\n  const calculateRange = () => {\n    const container = getTargetElement(containerTarget);\n\n    if (container) {\n      const { scrollTop, clientHeight } = container;\n\n      const offset = getOffset(scrollTop);\n      const visibleCount = getVisibleCount(clientHeight, offset);\n\n      const start = Math.max(0, offset - overscan);\n      const end = Math.min(list.length, offset + visibleCount + overscan);\n\n      const offsetTop = getDistanceTop(start);\n\n      setWrapperStyle({\n        height: totalHeight - offsetTop + 'px',\n        marginTop: offsetTop + 'px',\n      });\n\n      setTargetList(\n        list.slice(start, end).map((ele, index) => ({\n          data: ele,\n          index: index + start,\n        })),\n      );\n    }\n  };\n\n  useUpdateEffect(() => {\n    const wrapper = getTargetElement(wrapperTarget) as HTMLElement;\n    if (wrapper) {\n      Object.keys(wrapperStyle).forEach(\n        (key) => ((wrapper.style as any)[key] = (wrapperStyle as any)[key]),\n      );\n    }\n  }, [wrapperStyle]);\n\n  useEffect(() => {\n    if (!size?.width || !size?.height) {\n      return;\n    }\n    calculateRange();\n  }, [size?.width, size?.height, list]);\n\n  useEventListener(\n    'scroll',\n    (e) => {\n      if (scrollTriggerByScrollToFunc.current) {\n        scrollTriggerByScrollToFunc.current = false;\n        return;\n      }\n      e.preventDefault();\n      calculateRange();\n    },\n    {\n      target: containerTarget,\n    },\n  );\n\n  const scrollTo = (index: number) => {\n    const container = getTargetElement(containerTarget);\n    if (container) {\n      scrollTriggerByScrollToFunc.current = true;\n      container.scrollTop = getDistanceTop(index);\n      calculateRange();\n    }\n  };\n\n  return [targetList, useMemoizedFn(scrollTo)] as const;\n};\n\nexport default useVirtualList;\n"
  },
  {
    "path": "packages/hooks/src/useVirtualList/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useVirtualList\n\n提供虚拟化列表能力的 Hook，用于解决展示海量数据渲染时首屏渲染缓慢和滚动卡顿问题。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n### 动态元素高度\n\n<code src=\"./demo/demo2.tsx\" />\n\n## API\n\n```typescript\nconst [list, scrollTo] = useVirtualList<T>(\n  originalList: T[],\n  options: {\n    containerTarget: (() => Element) | Element | MutableRefObject<Element>,\n    wrapperTarget: (() => Element) | Element | MutableRefObject<Element>,\n    itemHeight: number | ((index: number, data: T) => number),\n    overscan?: number,\n  }\n);\n```\n\n### Params\n\n| 参数         | 说明                                                                           | 类型      | 默认值 |\n| ------------ | ------------------------------------------------------------------------------ | --------- | ------ |\n| originalList | 包含大量数据的列表。 **注意：必须经过 useMemo 处理或者永不变化，否则会死循环** | `T[]`     | `[]`   |\n| options      | 配置项                                                                         | `Options` | -      |\n\n### Options\n\n| 参数            | 说明                                                   | 类型                                                        | 默认值 |\n| --------------- | ------------------------------------------------------ | ----------------------------------------------------------- | ------ |\n| containerTarget | 外面容器，支持 DOM 节点或者 Ref 对象                   | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -      |\n| wrapperTarget   | 内部容器，支持 DOM 节点或者 Ref 对象                   | `() => Element` \\| `Element` \\| `MutableRefObject<Element>` | -      |\n| itemHeight      | 行高度，静态高度可以直接写入像素值，动态高度可传入函数 | `number` \\| `((index: number, data: T) => number)`          | -      |\n| overscan        | 视区上、下额外展示的 DOM 节点数量                      | `number`                                                    | `5`    |\n\n### Result\n\n| 参数     | 说明                   | 类型                           |\n| -------- | ---------------------- | ------------------------------ |\n| list     | 当前需要展示的列表内容 | `{ data: T, index: number }[]` |\n| scrollTo | 快速滚动到指定 index   | `(index: number) => void`      |\n"
  },
  {
    "path": "packages/hooks/src/useWebSocket/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { afterEach, describe, expect, test, vi } from 'vitest';\nimport WS from 'vitest-websocket-mock';\nimport { sleep } from '../../utils/testingHelpers';\nimport useWebSocket, { ReadyState } from '../index';\n\nconst promise: Promise<void> = new Promise((resolve) => resolve());\nconst wsUrl = 'ws://localhost:9999';\n\ndescribe('useWebSocket', () => {\n  afterEach(() => {\n    WS.clean();\n  });\n\n  test('should work', async () => {\n    const wsServer = new WS(wsUrl);\n    const hooks = renderHook(() => useWebSocket(wsUrl));\n\n    // connect\n    expect(hooks.result.current.readyState).toBe(ReadyState.Connecting);\n    expect(hooks.result.current.latestMessage).toBeUndefined();\n    await act(async () => {\n      await wsServer.connected;\n      return promise;\n    });\n    expect(hooks.result.current.readyState).toBe(ReadyState.Open);\n\n    // send message\n    const nowTime = `${Date.now()}`;\n    hooks.result.current.sendMessage?.(nowTime);\n    await expect(wsServer).toReceiveMessage(nowTime);\n\n    // receive message\n    act(() => {\n      wsServer.send(nowTime);\n    });\n    expect(hooks.result.current.latestMessage?.data).toBe(nowTime);\n\n    // disconnect\n    act(() => wsServer.close());\n    await act(async () => {\n      await wsServer.closed;\n      return promise;\n    });\n    expect(hooks.result.current.readyState).toBe(ReadyState.Closed);\n  });\n\n  test('disconnect should work', async () => {\n    const wsServer = new WS(wsUrl);\n    const hooks = renderHook(() => useWebSocket(wsUrl));\n\n    // connect\n    expect(hooks.result.current.readyState).toBe(ReadyState.Connecting);\n    await act(() => wsServer.connected);\n    expect(hooks.result.current.readyState).toBe(ReadyState.Open);\n\n    // disconnect\n    act(() => hooks.result.current.disconnect());\n    await act(() => wsServer.closed);\n    expect(hooks.result.current.readyState).toBe(ReadyState.Closed);\n  });\n\n  test('useWebSocket should be manually triggered', async () => {\n    const wsServer = new WS(wsUrl);\n\n    new WebSocket(wsUrl);\n\n    const hooks = renderHook(() => useWebSocket(wsUrl, { manual: true }));\n\n    expect(hooks.result.current.readyState).toBe(ReadyState.Closed);\n    await act(async () => {\n      await wsServer.connected;\n    });\n\n    // We set \"manual: true\", so the connection status should be still closed.\n    expect(hooks.result.current.readyState).toBe(ReadyState.Closed);\n\n    await act(async () => {\n      hooks.result.current.connect!();\n      await sleep(100); // To make sure connection is established\n    });\n    expect(hooks.result.current.readyState).toBe(ReadyState.Open);\n\n    act(() => wsServer.close());\n  });\n\n  test('should not call connect when initial socketUrl is empty', async () => {\n    const wsServer = new WS(wsUrl);\n    const onOpen = vi.fn();\n    const onClose = vi.fn();\n\n    let url = '';\n    const hooks = renderHook(() => useWebSocket(url, { onOpen, onClose }));\n\n    await act(async () => {\n      await sleep(1000);\n    });\n\n    expect(hooks.result.current.readyState).toBe(ReadyState.Closed);\n\n    url = wsUrl;\n    hooks.rerender();\n\n    await act(async () => {\n      await wsServer.connected;\n    });\n\n    expect(hooks.result.current.readyState).toBe(ReadyState.Open);\n    expect(onOpen).toBeCalledTimes(1);\n\n    act(() => wsServer.close());\n  });\n\n  test('change socketUrl should connect correctly', async () => {\n    const wsUrl1 = 'ws://localhost:8888';\n    const wsServer1 = new WS(wsUrl);\n    const wsServer2 = new WS(wsUrl1);\n\n    const onOpen = vi.fn();\n    const onClose = vi.fn();\n\n    let url = wsUrl;\n    const hooks = renderHook(() => useWebSocket(url, { onOpen, onClose, reconnectInterval: 300 }));\n\n    expect(hooks.result.current.readyState).toBe(ReadyState.Connecting);\n    await act(async () => {\n      await wsServer1.connected;\n    });\n    expect(hooks.result.current.readyState).toBe(ReadyState.Open);\n\n    url = wsUrl1;\n    hooks.rerender();\n    await act(async () => {\n      await wsServer2.connected;\n    });\n    expect(hooks.result.current.readyState).toBe(ReadyState.Open);\n\n    await act(async () => {\n      await sleep(3000);\n    });\n    expect(onOpen).toBeCalledTimes(2);\n    expect(onClose).toBeCalledTimes(1);\n\n    act(() => wsServer1.close());\n    act(() => wsServer2.close());\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useWebSocket/demo/demo1.tsx",
    "content": "import { useRef, useMemo } from 'react';\nimport { useWebSocket } from 'ahooks';\n\nenum ReadyState {\n  Connecting = 0,\n  Open = 1,\n  Closing = 2,\n  Closed = 3,\n}\n\nexport default () => {\n  const messageHistory = useRef<any[]>([]);\n\n  const { readyState, sendMessage, latestMessage, disconnect, connect } = useWebSocket(\n    'wss://ws.postman-echo.com/raw',\n  );\n\n  messageHistory.current = useMemo(\n    () => messageHistory.current.concat(latestMessage),\n    [latestMessage],\n  );\n\n  return (\n    <div>\n      {/* send message */}\n      <button\n        onClick={() => sendMessage && sendMessage(`${Date.now()}`)}\n        disabled={readyState !== ReadyState.Open}\n        style={{ marginRight: 8 }}\n      >\n        ✉️ send\n      </button>\n      {/* disconnect */}\n      <button\n        onClick={() => disconnect && disconnect()}\n        disabled={readyState !== ReadyState.Open}\n        style={{ marginRight: 8 }}\n      >\n        ❌ disconnect\n      </button>\n      {/* connect */}\n      <button onClick={() => connect && connect()} disabled={readyState === ReadyState.Open}>\n        {readyState === ReadyState.Connecting ? 'connecting' : '📞 connect'}\n      </button>\n      <div style={{ marginTop: 8 }}>readyState: {readyState}</div>\n      <div style={{ marginTop: 8 }}>\n        <p>received message: </p>\n        {messageHistory.current.map((message, index) => (\n          <p key={index} style={{ wordWrap: 'break-word' }}>\n            {message?.data}\n          </p>\n        ))}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useWebSocket/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useWebSocket\n\nA hook for WebSocket.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nenum ReadyState {\n  Connecting = 0,\n  Open = 1,\n  Closing = 2,\n  Closed = 3,\n}\n\ninterface Options {\n  reconnectLimit?: number;\n  reconnectInterval?: number;\n  onOpen?: (event: WebSocketEventMap['open'], instance: WebSocket) => void;\n  onClose?: (event: WebSocketEventMap['close'], instance: WebSocket) => void;\n  onMessage?: (message: WebSocketEventMap['message'], instance: WebSocket) => void;\n  onError?: (event: WebSocketEventMap['error'], instance: WebSocket) => void;\n  protocols?: string | string[];\n}\n\ninterface Result {\n  latestMessage?: WebSocketEventMap['message'];\n  sendMessage: WebSocket['send'];\n  disconnect: () => void;\n  connect: () => void;\n  readyState: ReadyState;\n  webSocketIns?: WebSocket;\n}\n\nuseWebSocket(socketUrl: string, options?: Options): Result;\n```\n\n### Params\n\n| Property  | Description                    | Type      | Default |\n| --------- | ------------------------------ | --------- | ------- |\n| socketUrl | Required, webSocket url        | `string`  | -       |\n| options   | connect the configuration item | `Options` | -       |\n\n#### Options\n\n| Options Property  | Description                        | Type                                                                   | Default |\n| ----------------- | ---------------------------------- | ---------------------------------------------------------------------- | ------- |\n| onOpen            | The webSocket connect callback     | `(event: WebSocketEventMap['open'], instance: WebSocket) => void`      | -       |\n| onClose           | WebSocket close callback           | `(event: WebSocketEventMap['close'], instance: WebSocket) => void`     | -       |\n| onMessage         | WebSocket receive message callback | `(message: WebSocketEventMap['message'], instance: WebSocket) => void` | -       |\n| onError           | WebSocket error callback           | `(event: WebSocketEventMap['error'], instance: WebSocket) => void`     | -       |\n| reconnectLimit    | Retry times                        | `number`                                                               | `3`     |\n| reconnectInterval | Retry interval(ms)                 | `number`                                                               | `3000`  |\n| manual            | Manually starts connection         | `boolean`                                                              | `false` |\n| protocols         | Sub protocols                      | `string` \\| `string[]`                                                 | -       |\n\n### Result\n\n| Options Property | Description                                                                            | Type                           |\n| ---------------- | -------------------------------------------------------------------------------------- | ------------------------------ |\n| latestMessage    | Latest message                                                                         | `WebSocketEventMap['message']` |\n| sendMessage      | Send message function                                                                  | `WebSocket['send']`            |\n| disconnect       | Disconnect webSocket manually                                                          | `() => void`                   |\n| connect          | Connect webSocket manually. If already connected, close the current one and reconnect. | `() => void`                   |\n| readyState       | Current webSocket connection status                                                    | `ReadyState`                   |\n| webSocketIns     | WebSocket instance                                                                     | `WebSocket`                    |\n"
  },
  {
    "path": "packages/hooks/src/useWebSocket/index.ts",
    "content": "import { useEffect, useRef, useState } from 'react';\nimport useLatest from '../useLatest';\nimport useMemoizedFn from '../useMemoizedFn';\nimport useUnmount from '../useUnmount';\n\nexport enum ReadyState {\n  Connecting = 0,\n  Open = 1,\n  Closing = 2,\n  Closed = 3,\n}\n\nexport interface Options {\n  reconnectLimit?: number;\n  reconnectInterval?: number;\n  manual?: boolean;\n  onOpen?: (event: WebSocketEventMap['open'], instance: WebSocket) => void;\n  onClose?: (event: WebSocketEventMap['close'], instance: WebSocket) => void;\n  onMessage?: (message: WebSocketEventMap['message'], instance: WebSocket) => void;\n  onError?: (event: WebSocketEventMap['error'], instance: WebSocket) => void;\n\n  protocols?: string | string[];\n}\n\nexport interface Result {\n  latestMessage?: WebSocketEventMap['message'];\n  sendMessage: WebSocket['send'];\n  disconnect: () => void;\n  connect: () => void;\n  readyState: ReadyState;\n  webSocketIns?: WebSocket;\n}\n\nfunction useWebSocket(socketUrl: string, options: Options = {}): Result {\n  const {\n    reconnectLimit = 3,\n    reconnectInterval = 3 * 1000,\n    manual = false,\n    onOpen,\n    onClose,\n    onMessage,\n    onError,\n    protocols,\n  } = options;\n\n  const [latestMessage, setLatestMessage] = useState<WebSocketEventMap['message']>();\n  const [readyState, setReadyState] = useState<ReadyState>(ReadyState.Closed);\n\n  const onOpenRef = useLatest(onOpen);\n  const onCloseRef = useLatest(onClose);\n  const onMessageRef = useLatest(onMessage);\n  const onErrorRef = useLatest(onError);\n  const readyStateRef = useLatest(readyState);\n\n  const reconnectTimesRef = useRef(0);\n  const reconnectTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);\n\n  const websocketRef = useRef<WebSocket>(undefined);\n\n  const reconnect = () => {\n    if (\n      reconnectTimesRef.current < reconnectLimit &&\n      websocketRef.current?.readyState !== ReadyState.Open\n    ) {\n      if (reconnectTimerRef.current) {\n        clearTimeout(reconnectTimerRef.current);\n      }\n\n      reconnectTimerRef.current = setTimeout(() => {\n        // eslint-disable-next-line @typescript-eslint/no-use-before-define\n        connectWs();\n        reconnectTimesRef.current++;\n      }, reconnectInterval);\n    }\n  };\n\n  const connectWs = () => {\n    if (reconnectTimerRef.current) {\n      clearTimeout(reconnectTimerRef.current);\n    }\n\n    if (websocketRef.current) {\n      websocketRef.current.close();\n    }\n\n    const ws = new WebSocket(socketUrl, protocols);\n    setReadyState(ReadyState.Connecting);\n\n    ws.onerror = (event) => {\n      if (websocketRef.current !== ws) {\n        return;\n      }\n      reconnect();\n      onErrorRef.current?.(event, ws);\n      setReadyState(ws.readyState || ReadyState.Closed);\n    };\n    ws.onopen = (event) => {\n      if (websocketRef.current !== ws) {\n        return;\n      }\n      onOpenRef.current?.(event, ws);\n      reconnectTimesRef.current = 0;\n      setReadyState(ws.readyState || ReadyState.Open);\n    };\n    ws.onmessage = (message: WebSocketEventMap['message']) => {\n      if (websocketRef.current !== ws) {\n        return;\n      }\n      onMessageRef.current?.(message, ws);\n      setLatestMessage(message);\n    };\n    ws.onclose = (event) => {\n      onCloseRef.current?.(event, ws);\n      // closed by server\n      if (websocketRef.current === ws) {\n        reconnect();\n      }\n      // closed by disconnect or closed by server\n      if (!websocketRef.current || websocketRef.current === ws) {\n        setReadyState(ws.readyState || ReadyState.Closed);\n      }\n    };\n\n    websocketRef.current = ws;\n  };\n\n  const sendMessage: WebSocket['send'] = (message) => {\n    if (readyStateRef.current === ReadyState.Open) {\n      websocketRef.current?.send(message);\n    } else {\n      throw new Error('WebSocket disconnected');\n    }\n  };\n\n  const connect = () => {\n    reconnectTimesRef.current = 0;\n    connectWs();\n  };\n\n  const disconnect = () => {\n    if (reconnectTimerRef.current) {\n      clearTimeout(reconnectTimerRef.current);\n    }\n\n    reconnectTimesRef.current = reconnectLimit;\n    websocketRef.current?.close();\n    websocketRef.current = undefined;\n  };\n\n  useEffect(() => {\n    if (!manual && socketUrl) {\n      connect();\n    }\n  }, [socketUrl, manual]);\n\n  useUnmount(() => {\n    disconnect();\n  });\n\n  return {\n    latestMessage,\n    sendMessage: useMemoizedFn(sendMessage),\n    connect: useMemoizedFn(connect),\n    disconnect: useMemoizedFn(disconnect),\n    readyState,\n    webSocketIns: websocketRef.current,\n  };\n}\n\nexport default useWebSocket;\n"
  },
  {
    "path": "packages/hooks/src/useWebSocket/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useWebSocket\n\n用于处理 WebSocket 的 Hook。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\nenum ReadyState {\n  Connecting = 0,\n  Open = 1,\n  Closing = 2,\n  Closed = 3,\n}\n\ninterface Options {\n  reconnectLimit?: number;\n  reconnectInterval?: number;\n  onOpen?: (event: WebSocketEventMap['open'], instance: WebSocket) => void;\n  onClose?: (event: WebSocketEventMap['close'], instance: WebSocket) => void;\n  onMessage?: (message: WebSocketEventMap['message'], instance: WebSocket) => void;\n  onError?: (event: WebSocketEventMap['error'], instance: WebSocket) => void;\n  protocols?: string | string[];\n}\n\ninterface Result {\n  latestMessage?: WebSocketEventMap['message'];\n  sendMessage: WebSocket['send'];\n  disconnect: () => void;\n  connect: () => void;\n  readyState: ReadyState;\n  webSocketIns?: WebSocket;\n}\n\nuseWebSocket(socketUrl: string, options?: Options): Result;\n```\n\n### Params\n\n| 参数      | 说明                 | 类型      | 默认值 |\n| --------- | -------------------- | --------- | ------ |\n| socketUrl | 必填，webSocket 地址 | `string`  | -      |\n| options   | 可选，连接配置项     | `Options` | -      |\n\n#### Options\n\n| 参数              | 说明                   | 类型                                                                   | 默认值  |\n| ----------------- | ---------------------- | ---------------------------------------------------------------------- | ------- |\n| onOpen            | webSocket 连接成功回调 | `(event: WebSocketEventMap['open'], instance: WebSocket) => void`      | -       |\n| onClose           | webSocket 关闭回调     | `(event: WebSocketEventMap['close'], instance: WebSocket) => void`     | -       |\n| onMessage         | webSocket 收到消息回调 | `(message: WebSocketEventMap['message'], instance: WebSocket) => void` | -       |\n| onError           | webSocket 错误回调     | `(event: WebSocketEventMap['error'], instance: WebSocket) => void`     | -       |\n| reconnectLimit    | 重试次数               | `number`                                                               | `3`     |\n| reconnectInterval | 重试时间间隔（ms）     | `number`                                                               | `3000`  |\n| manual            | 手动启动连接           | `boolean`                                                              | `false` |\n| protocols         | 子协议                 | `string` \\| `string[]`                                                 | -       |\n\n### Result\n\n| 参数          | 说明                                                   | 类型                           |\n| ------------- | ------------------------------------------------------ | ------------------------------ |\n| latestMessage | 最新消息                                               | `WebSocketEventMap['message']` |\n| sendMessage   | 发送消息函数                                           | `WebSocket['send']`            |\n| disconnect    | 手动断开 webSocket 连接                                | `() => void`                   |\n| connect       | 手动连接 webSocket，如果当前已有连接，则关闭后重新连接 | `() => void`                   |\n| readyState    | 当前 webSocket 连接状态                                | `ReadyState`                   |\n| webSocketIns  | webSocket 实例                                         | `WebSocket`                    |\n"
  },
  {
    "path": "packages/hooks/src/useWhyDidYouUpdate/__tests__/index.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { useState } from 'react';\nimport { describe, expect, test, vi } from 'vitest';\nimport useWhyDidYouUpdate from '../index';\n\ndescribe('useWhyDidYouUpdate', () => {\n  test('should work', () => {\n    console.log = vi.fn();\n    const setup = () =>\n      renderHook(() => {\n        const [count, setCount] = useState(100);\n        useWhyDidYouUpdate('UseWhyDidYouUpdateComponent', { count });\n        return {\n          setCount,\n        };\n      });\n\n    const hook = setup();\n\n    act(() => {\n      hook.result.current.setCount(1);\n    });\n    expect(console.log).toHaveBeenCalledWith(\n      '[why-did-you-update]',\n      'UseWhyDidYouUpdateComponent',\n      {\n        count: {\n          from: 100,\n          to: 1,\n        },\n      },\n    );\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/useWhyDidYouUpdate/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Update state or props, you can see the output in the console\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 更新 state 或 props，可以在控制台看到输出\n */\n\nimport { useWhyDidYouUpdate } from 'ahooks';\nimport { useState } from 'react';\n\nconst Demo: React.FC<{ count: number }> = (props) => {\n  const [randomNum, setRandomNum] = useState(Math.random());\n\n  useWhyDidYouUpdate('useWhyDidYouUpdateComponent', { ...props, randomNum });\n\n  return (\n    <div>\n      <div>\n        <span>number: {props.count}</span>\n      </div>\n      <div>\n        randomNum: {randomNum}\n        <button onClick={() => setRandomNum(Math.random)} style={{ marginLeft: 8 }}>\n          🎲\n        </button>\n      </div>\n    </div>\n  );\n};\n\nexport default () => {\n  const [count, setCount] = useState(0);\n\n  return (\n    <div>\n      <Demo count={count} />\n      <div>\n        <button onClick={() => setCount((prevCount) => prevCount - 1)}>count -</button>\n        <button onClick={() => setCount((prevCount) => prevCount + 1)} style={{ marginLeft: 8 }}>\n          count +\n        </button>\n      </div>\n      <p style={{ marginTop: 8 }}>Please open the browser console to view the output!</p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/hooks/src/useWhyDidYouUpdate/index.en-US.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useWhyDidYouUpdate\n\nHelp developers troubleshoot what changes have caused component rerender.\n\n## Examples\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\ntype IProps = Record<string, any>;\n\nuseWhyDidYouUpdate(componentName: string, props: IProps): void;\n```\n\n### Params\n\n| Property      | Description                                                                                 | Type     | Default |\n| ------------- | ------------------------------------------------------------------------------------------- | -------- | ------- |\n| componentName | Required, the name of the observation component                                             | `string` | -       |\n| props         | Required, data to be observed (`state` or `props` and other data that may lead to rerender) | `object` | -       |\n\n### Result\n\nPlease open the browser console, you can see the output of the changed observed `state` or `props`.\n"
  },
  {
    "path": "packages/hooks/src/useWhyDidYouUpdate/index.ts",
    "content": "import { useEffect, useRef } from 'react';\n\nexport type IProps = Record<string, any>;\n\nfunction useWhyDidYouUpdate(componentName: string, props: IProps) {\n  const prevProps = useRef<IProps>({});\n\n  useEffect(() => {\n    if (prevProps.current) {\n      const allKeys = Object.keys({ ...prevProps.current, ...props });\n      const changedProps: IProps = {};\n\n      allKeys.forEach((key) => {\n        if (!Object.is(prevProps.current[key], props[key])) {\n          changedProps[key] = {\n            from: prevProps.current[key],\n            to: props[key],\n          };\n        }\n      });\n\n      if (Object.keys(changedProps).length) {\n        console.log('[why-did-you-update]', componentName, changedProps);\n      }\n    }\n\n    prevProps.current = props;\n  });\n}\n\nexport default useWhyDidYouUpdate;\n"
  },
  {
    "path": "packages/hooks/src/useWhyDidYouUpdate/index.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\n---\n\n# useWhyDidYouUpdate\n\n帮助开发者排查是哪个属性改变导致了组件的 rerender。\n\n## 代码演示\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" />\n\n## API\n\n```typescript\ntype IProps = Record<string, any>;\n\nuseWhyDidYouUpdate(componentName: string, props: IProps): void;\n```\n\n### Params\n\n| 参数          | 说明                                                                                   | 类型     | 默认值 |\n| ------------- | -------------------------------------------------------------------------------------- | -------- | ------ |\n| componentName | 必填，观测组件的名称                                                                   | `string` | -      |\n| props         | 必填，需要观测的数据（当前组件 `state` 或者传入的 `props` 等可能导致 rerender 的数据） | `object` | -      |\n\n### Result\n\n打开控制台，可以看到改变的属性。\n"
  },
  {
    "path": "packages/hooks/src/utils/__tests__/index.spec.ts",
    "content": "import { describe, expect, test } from 'vitest';\nimport { isBoolean, isFunction, isNumber, isObject, isString, isUndef } from '../index';\n\ndescribe('shared utils methods', () => {\n  test('isBoolean', () => {\n    expect(isBoolean(true)).toBe(true);\n    expect(isBoolean(false)).toBe(true);\n\n    expect(isBoolean('')).toBe(false);\n    expect(isBoolean([])).toBe(false);\n  });\n\n  test('isFunction', () => {\n    expect(isFunction(function foo() {})).toBe(true);\n    expect(isFunction(() => {})).toBe(true);\n\n    expect(isFunction({})).toBe(false);\n    expect(isFunction(1)).toBe(false);\n  });\n\n  test('isNumber', () => {\n    expect(isNumber(1)).toBe(true);\n    expect(isNumber(Infinity)).toBe(true);\n    expect(isNumber(NaN)).toBe(true);\n\n    expect(isNumber('str')).toBe(false);\n    expect(isNumber({})).toBe(false);\n  });\n\n  test('isObject', () => {\n    expect(isObject({})).toBe(true);\n    expect(isObject([])).toBe(true);\n    expect(isObject(/(?:)/)).toBe(true);\n    expect(isObject(new Date())).toBe(true);\n\n    expect(isObject(null)).toBe(false);\n    expect(isObject(function foo() {})).toBe(false);\n    expect(isObject(123)).toBe(false);\n  });\n\n  test('isString', () => {\n    expect(isString('1')).toBe(true);\n    expect(isString(String('1'))).toBe(true);\n\n    expect(isString(1)).toBe(false);\n    expect(isString({})).toBe(false);\n  });\n\n  test('isUndef', () => {\n    expect(isUndef(undefined)).toBe(true);\n\n    expect(isUndef(0)).toBe(false);\n    expect(isUndef(null)).toBe(false);\n    expect(isUndef(NaN)).toBe(false);\n    expect(isUndef('')).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/hooks/src/utils/createEffectWithTarget.ts",
    "content": "import type { DependencyList, EffectCallback, useEffect, useLayoutEffect } from 'react';\nimport { useRef } from 'react';\nimport useUnmount from '../useUnmount';\nimport depsAreSame from './depsAreSame';\nimport type { BasicTarget } from './domTarget';\nimport { getTargetElement } from './domTarget';\n\nconst createEffectWithTarget = (useEffectType: typeof useEffect | typeof useLayoutEffect) => {\n  /**\n   *\n   * @param effect\n   * @param deps\n   * @param target target should compare ref.current vs ref.current, dom vs dom, ()=>dom vs ()=>dom\n   */\n  const useEffectWithTarget = (\n    effect: EffectCallback,\n    deps: DependencyList,\n    target: BasicTarget<any> | BasicTarget<any>[],\n  ) => {\n    const hasInitRef = useRef(false);\n\n    const lastElementRef = useRef<(Element | null)[]>([]);\n    const lastDepsRef = useRef<DependencyList>([]);\n\n    const unLoadRef = useRef<any>(undefined);\n\n    useEffectType(() => {\n      const targets = Array.isArray(target) ? target : [target];\n      const els = targets.map((item) => getTargetElement(item));\n\n      // init run\n      if (!hasInitRef.current) {\n        hasInitRef.current = true;\n        lastElementRef.current = els;\n        lastDepsRef.current = deps;\n\n        unLoadRef.current = effect();\n        return;\n      }\n\n      if (\n        els.length !== lastElementRef.current.length ||\n        !depsAreSame(lastElementRef.current, els) ||\n        !depsAreSame(lastDepsRef.current, deps)\n      ) {\n        unLoadRef.current?.();\n\n        lastElementRef.current = els;\n        lastDepsRef.current = deps;\n        unLoadRef.current = effect();\n      }\n    });\n\n    useUnmount(() => {\n      unLoadRef.current?.();\n      // for react-refresh\n      hasInitRef.current = false;\n    });\n  };\n\n  return useEffectWithTarget;\n};\n\nexport default createEffectWithTarget;\n"
  },
  {
    "path": "packages/hooks/src/utils/depsAreSame.ts",
    "content": "import type { DependencyList } from 'react';\n\nfunction depsAreSame(oldDeps: DependencyList, deps: DependencyList): boolean {\n  if (oldDeps === deps) {\n    return true;\n  }\n  for (let i = 0; i < oldDeps.length; i++) {\n    if (!Object.is(oldDeps[i], deps[i])) {\n      return false;\n    }\n  }\n  return true;\n}\n\nexport default depsAreSame;\n"
  },
  {
    "path": "packages/hooks/src/utils/depsEqual.ts",
    "content": "import type { DependencyList } from 'react';\nimport isEqual from 'react-fast-compare';\n\nexport const depsEqual = (aDeps: DependencyList = [], bDeps: DependencyList = []) =>\n  isEqual(aDeps, bDeps);\n"
  },
  {
    "path": "packages/hooks/src/utils/domTarget.ts",
    "content": "import { isFunction } from './index';\nimport isBrowser from './isBrowser';\nimport type { RefObject } from 'react';\n\ntype TargetValue<T> = T | undefined | null;\n\ntype TargetType = HTMLElement | Element | Window | Document;\n\nexport type BasicTarget<T extends TargetType = Element> =\n  | (() => TargetValue<T>)\n  | TargetValue<T>\n  | RefObject<TargetValue<T>>;\n\nexport function getTargetElement<T extends TargetType>(target: BasicTarget<T>, defaultElement?: T) {\n  if (!isBrowser) {\n    return undefined;\n  }\n\n  if (!target) {\n    return defaultElement;\n  }\n\n  let targetElement: TargetValue<T>;\n\n  if (isFunction(target)) {\n    targetElement = target();\n  } else if ('current' in target) {\n    targetElement = target.current;\n  } else {\n    targetElement = target;\n  }\n\n  return targetElement;\n}\n"
  },
  {
    "path": "packages/hooks/src/utils/getDocumentOrShadow.ts",
    "content": "import type { BasicTarget } from '../utils/domTarget';\nimport { getTargetElement } from '../utils/domTarget';\n\ndeclare type TargetValue<T> = T | undefined | null;\n\nconst checkIfAllInShadow = (targets: BasicTarget[]) => {\n  return targets.every((item) => {\n    const targetElement = getTargetElement(item);\n    if (!targetElement) {\n      return false;\n    }\n    if (targetElement.getRootNode() instanceof ShadowRoot) {\n      return true;\n    }\n    return false;\n  });\n};\n\nconst getShadow = (node: TargetValue<Element>) => {\n  if (!node) {\n    return document;\n  }\n  return node.getRootNode();\n};\n\nconst getDocumentOrShadow = (target: BasicTarget | BasicTarget[]): Document | Node => {\n  if (!target || !document.getRootNode) {\n    return document;\n  }\n\n  const targets = Array.isArray(target) ? target : [target];\n\n  if (checkIfAllInShadow(targets)) {\n    return getShadow(getTargetElement(targets[0]));\n  }\n\n  return document;\n};\n\nexport default getDocumentOrShadow;\n"
  },
  {
    "path": "packages/hooks/src/utils/index.ts",
    "content": "export const isObject = (value: unknown): value is Record<any, any> =>\n  value !== null && typeof value === 'object';\nexport const isFunction = (value: unknown): value is (...args: any) => any =>\n  typeof value === 'function';\nexport const isString = (value: unknown): value is string => typeof value === 'string';\nexport const isBoolean = (value: unknown): value is boolean => typeof value === 'boolean';\nexport const isNumber = (value: unknown): value is number => typeof value === 'number';\nexport const isUndef = (value: unknown): value is undefined => typeof value === 'undefined';\n"
  },
  {
    "path": "packages/hooks/src/utils/isAppleDevice.ts",
    "content": "const isAppleDevice = /(mac|iphone|ipod|ipad)/i.test(\n  typeof navigator !== 'undefined' ? navigator?.platform : '',\n);\n\nexport default isAppleDevice;\n"
  },
  {
    "path": "packages/hooks/src/utils/isBrowser.ts",
    "content": "const isBrowser = !!(\n  typeof window !== 'undefined' &&\n  window.document &&\n  window.document.createElement\n);\n\nexport default isBrowser;\n"
  },
  {
    "path": "packages/hooks/src/utils/isDev.ts",
    "content": "const isDev = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test';\n\nexport default isDev;\n"
  },
  {
    "path": "packages/hooks/src/utils/lodash-polyfill.ts",
    "content": "import debounce from 'lodash/debounce';\n\nfunction isNodeOrWeb() {\n  const freeGlobal =\n    (typeof global === 'undefined' ? 'undefined' : typeof global) == 'object' &&\n    global &&\n    global.Object === Object &&\n    global;\n  const freeSelf = typeof self == 'object' && self && self.Object === Object && self;\n  return freeGlobal || freeSelf;\n}\n\nif (!isNodeOrWeb()) {\n  global.Date = Date;\n}\n\nexport { debounce };\n"
  },
  {
    "path": "packages/hooks/src/utils/noop.ts",
    "content": "const noop = () => {};\n\nexport default noop;\n"
  },
  {
    "path": "packages/hooks/src/utils/rect.ts",
    "content": "const getScrollTop = (el: Document | Element) => {\n  if (el === document || el === document.documentElement || el === document.body) {\n    return Math.max(\n      window.pageYOffset,\n      document.documentElement.scrollTop,\n      document.body.scrollTop,\n    );\n  }\n  return (el as Element).scrollTop;\n};\n\nconst getScrollHeight = (el: Document | Element) => {\n  return (\n    (el as Element).scrollHeight ||\n    Math.max(document.documentElement.scrollHeight, document.body.scrollHeight)\n  );\n};\n\nconst getClientHeight = (el: Document | Element) => {\n  return (\n    (el as Element).clientHeight ||\n    Math.max(document.documentElement.clientHeight, document.body.clientHeight)\n  );\n};\n\nexport { getScrollTop, getScrollHeight, getClientHeight };\n"
  },
  {
    "path": "packages/hooks/src/utils/testingHelpers.ts",
    "content": "export function sleep(time: number) {\n  return new Promise<void>((resolve) => {\n    setTimeout(() => {\n      resolve();\n    }, time);\n  });\n}\n\nexport function request(req: any) {\n  return new Promise<string>((resolve, reject) =>\n    setTimeout(() => {\n      if (req === 0) {\n        reject(new Error('fail'));\n      } else {\n        resolve('success');\n      }\n    }, 1000),\n  );\n}\n"
  },
  {
    "path": "packages/hooks/src/utils/tests.tsx",
    "content": "import { StrictMode } from 'react';\nimport { renderHook } from '@testing-library/react';\n\nexport * from '@testing-library/react';\n\nconst Wrapper = process.env.REACT_MODE === 'strict' ? StrictMode : undefined;\n\nconst customRender: typeof renderHook = (ui, options) =>\n  renderHook(ui, { wrapper: Wrapper, ...options });\n\nexport { customRender as renderHook };\n"
  },
  {
    "path": "packages/hooks/src/utils/useDeepCompareWithTarget.ts",
    "content": "import type { DependencyList, EffectCallback } from 'react';\nimport { useRef } from 'react';\nimport type { BasicTarget } from './domTarget';\nimport useEffectWithTarget from './useEffectWithTarget';\nimport { depsEqual } from './depsEqual';\n\nconst useDeepCompareEffectWithTarget = (\n  effect: EffectCallback,\n  deps: DependencyList,\n  target: BasicTarget<any> | BasicTarget<any>[],\n) => {\n  const ref = useRef<DependencyList>(undefined);\n  const signalRef = useRef<number>(0);\n\n  if (!depsEqual(deps, ref.current)) {\n    signalRef.current += 1;\n  }\n  ref.current = deps;\n\n  useEffectWithTarget(effect, [signalRef.current], target);\n};\n\nexport default useDeepCompareEffectWithTarget;\n"
  },
  {
    "path": "packages/hooks/src/utils/useEffectWithTarget.ts",
    "content": "import { useEffect } from 'react';\nimport createEffectWithTarget from './createEffectWithTarget';\n\nconst useEffectWithTarget = createEffectWithTarget(useEffect);\n\nexport default useEffectWithTarget;\n"
  },
  {
    "path": "packages/hooks/src/utils/useIsomorphicLayoutEffectWithTarget.ts",
    "content": "import isBrowser from './isBrowser';\nimport useEffectWithTarget from './useEffectWithTarget';\nimport useLayoutEffectWithTarget from './useLayoutEffectWithTarget';\n\nconst useIsomorphicLayoutEffectWithTarget = isBrowser\n  ? useLayoutEffectWithTarget\n  : useEffectWithTarget;\n\nexport default useIsomorphicLayoutEffectWithTarget;\n"
  },
  {
    "path": "packages/hooks/src/utils/useLayoutEffectWithTarget.ts",
    "content": "import { useLayoutEffect } from 'react';\nimport createEffectWithTarget from './createEffectWithTarget';\n\nconst useEffectWithTarget = createEffectWithTarget(useLayoutEffect);\n\nexport default useEffectWithTarget;\n"
  },
  {
    "path": "packages/hooks/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\"\n}\n"
  },
  {
    "path": "packages/hooks/tsconfig.pro.json",
    "content": "{\n  \"extends\": \"../../tsconfig.pro.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"src\",\n    \"sourceMap\": false\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/hooks/vitest.config.ts",
    "content": "import { resolve } from 'node:path';\nimport { defineConfig } from 'vitest/config';\n\n// https://cn.vitest.dev/guide/\nexport default defineConfig({\n  resolve: {\n    alias: {\n      src: resolve(__dirname, 'src'),\n    },\n  },\n  test: {\n    reporters: ['default', 'verbose'],\n    environment: 'jsdom',\n    include: ['src/**/__tests__/*.spec.ts?(x)'],\n    coverage: {\n      provider: 'istanbul',\n      include: ['src/**/*.ts'],\n    },\n  },\n});\n"
  },
  {
    "path": "packages/hooks/webpack.config.js",
    "content": "const { merge } = require('webpack-merge');\nconst common = require('../../webpack.common.js');\nconst path = require('node:path');\n\nmodule.exports = merge(common, {\n  entry: './es/index.js',\n  output: {\n    filename: 'ahooks.js',\n    library: 'ahooks',\n    path: path.resolve(__dirname, './dist'),\n  },\n});\n"
  },
  {
    "path": "packages/use-url-state/README.md",
    "content": "# useUrlState\n\nA hook that stores the state into url query parameters.\n\n## Installing\n\nInside your React project directory, run the following:\n\n```bash\nyarn add @ahooksjs/use-url-state -S\n```\n\nOr with npm:\n\n```bash\nnpm install @ahooksjs/use-url-state -S\n```\n\nOr with pnpm\n\n```bash\npnpm add @ahooksjs/use-url-state\n```\n\nOr with Bun\n\n```bash\nbun add @ahooksjs/use-url-state\n```\n\n## Example\n\n```javascript\nimport useUrlState from '@ahooksjs/use-url-state';\n\nconst [state, setState] = useUrlState({ demoCount: '1' });\n```\n\n## Documentation\n\nhttps://ahooks.js.org/hooks/state/use-url-state\n"
  },
  {
    "path": "packages/use-url-state/__tests__/browser.spec.tsx",
    "content": "import { act } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport { setup } from './setup';\n\ndescribe('useUrlState', () => {\n  test('state should be url search params', () => {\n    const res = setup([\n      {\n        pathname: '/index',\n        search: '?count=1',\n      },\n    ]);\n    expect(res.state).toMatchObject({ count: '1' });\n  });\n\n  test('url shoule be changed when use setState', () => {\n    const res = setup(['/index']);\n    expect(res.state).toMatchObject({});\n    act(() => {\n      res.setState({ count: 1 });\n    });\n    expect(res.state).toMatchObject({ count: '1' });\n  });\n\n  test('multiple states should be work', () => {\n    const res = setup(['/index']);\n    act(() => {\n      res.setState({ page: 1 });\n    });\n    act(() => {\n      res.setState({ pageSize: 10 });\n    });\n    expect(res.state).toMatchObject({ page: '1', pageSize: '10' });\n  });\n\n  test('query-string options should work', async () => {\n    const res = setup(\n      [\n        {\n          pathname: '/index',\n          search: '?foo=1,2,3',\n        },\n      ],\n      {},\n      {\n        parseOptions: {\n          arrayFormat: 'comma',\n        },\n        stringifyOptions: {\n          arrayFormat: 'comma',\n        },\n      },\n    );\n    expect(res.state).toMatchObject({ foo: ['1', '2', '3'] });\n\n    act(() => {\n      res.setState({ foo: ['4', '5', '6'] });\n    });\n    expect(res.state).toMatchObject({ foo: ['4', '5', '6'] });\n  });\n\n  test('location.state should be remain', () => {\n    const res = setup([\n      {\n        pathname: '/index',\n        state: 'state',\n      },\n    ]);\n    expect(res.location.state).toBe('state');\n    act(() => {\n      res.setState({ count: 1 });\n    });\n    expect(res.state).toMatchObject({ count: '1' });\n    expect(res.location.state).toBe('state');\n  });\n});\n"
  },
  {
    "path": "packages/use-url-state/__tests__/router.spec.tsx",
    "content": "import { act } from '@testing-library/react';\nimport { describe, expect, test } from 'vitest';\nimport { setup } from './setup';\n\ndescribe('React Router V6', () => {\n  test('useUrlState should be work', () => {\n    const res = setup(['/index']);\n    act(() => {\n      res.setState({ count: 1 });\n    });\n\n    expect(res.state).toMatchObject({ count: '1' });\n  });\n});\n"
  },
  {
    "path": "packages/use-url-state/__tests__/setup.tsx",
    "content": "import { render } from '@testing-library/react';\nimport type { MemoryRouterProps } from 'react-router';\nimport { MemoryRouter, useLocation } from 'react-router';\nimport type { Options } from 'src';\nimport useUrlState from 'src';\nimport { expect, test } from 'vitest';\n\nexport const setup = (\n  initialEntries: MemoryRouterProps['initialEntries'],\n  baseState: any = {},\n  options?: Options,\n) => {\n  const res = {} as any;\n\n  const Component = () => {\n    const [state, setState] = useUrlState(baseState, options);\n    const location = useLocation();\n    Object.assign(res, { state, setState, location });\n    return null;\n  };\n\n  render(\n    <MemoryRouter initialEntries={initialEntries}>\n      <Component />\n    </MemoryRouter>,\n  );\n\n  return res;\n};\n\ntest('useUrlState should be defined', () => {\n  expect(useUrlState).toBeDefined();\n});\n"
  },
  {
    "path": "packages/use-url-state/demo/demo1.tsx",
    "content": "/**\n * title: Default usage\n * desc: Store the state into url query. By set the value to `undefined`, the attribute can be removed from the url query.\n *\n * title.zh-CN: 基础用法\n * desc.zh-CN: 将状态同步到 url query 中。通过设置值为 `undefined`, 可以从 url query 上彻底删除某个属性，从而使用默认值。\n */\n\nimport useUrlState from '@ahooksjs/use-url-state';\n\nexport default () => {\n  const [state, setState] = useUrlState({ count: '1' });\n\n  return (\n    <>\n      <button\n        style={{ marginRight: 8 }}\n        type=\"button\"\n        onClick={() => setState({ count: `${Number(state.count || 0) + 1}` })}\n      >\n        add\n      </button>\n      <button type=\"button\" onClick={() => setState({ count: undefined })}>\n        clear\n      </button>\n      <div>state: {state?.count}</div>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/use-url-state/demo/demo2.tsx",
    "content": "/**\n * title: Multi-state management\n * desc: useUrlState can manage multiple states at the same time\n *\n * title.zh-CN: 多状态管理\n * desc.zh-CN: useUrlState 可以同时管理多个状态\n */\n\nimport useUrlState from '@ahooksjs/use-url-state';\n\nexport default () => {\n  const [state, setState] = useUrlState({ page: '1', pageSize: '10' });\n\n  return (\n    <>\n      <div>\n        page: {state.page}\n        <span style={{ paddingLeft: 8 }}>\n          <button\n            onClick={() => {\n              setState((s) => ({ page: `${Number(s.page) + 1}` }));\n            }}\n          >\n            +\n          </button>\n          <button\n            onClick={() => {\n              setState((s) => ({ page: `${Number(s.page) - 1}` }));\n            }}\n            style={{ margin: '0 8px' }}\n          >\n            -\n          </button>\n          <button\n            onClick={() => {\n              setState({ page: undefined });\n            }}\n          >\n            reset\n          </button>\n        </span>\n      </div>\n      <br />\n      <div>\n        pageSize: {state.pageSize}\n        <span style={{ paddingLeft: 8 }}>\n          <button\n            onClick={() => {\n              setState((s) => ({ pageSize: `${Number(s.pageSize) + 1}` }));\n            }}\n          >\n            +\n          </button>\n          <button\n            onClick={() => {\n              setState((s) => ({ pageSize: `${Number(s.pageSize) - 1}` }));\n            }}\n            style={{ margin: '0 8px' }}\n          >\n            -\n          </button>\n          <button\n            onClick={() => {\n              setState({ pageSize: undefined });\n            }}\n          >\n            reset\n          </button>\n        </span>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/use-url-state/demo/demo3.tsx",
    "content": "/**\n * title: Custom query-string options\n * desc: The rules can be customized by passing in parseOptions and stringifyOptions.\n *\n * title.zh-CN: 自定义 query-string 配置\n * desc.zh-CN: 可以通过传入 parseOptions 和 stringifyOptions 自定义转换规则。\n */\n\nimport useUrlState from '@ahooksjs/use-url-state';\n\nexport default () => {\n  const [state, setState] = useUrlState(\n    { ids: ['1', '2', '3'] },\n    {\n      parseOptions: {\n        arrayFormat: 'comma',\n      },\n      stringifyOptions: {\n        arrayFormat: 'comma',\n      },\n    },\n  );\n\n  return (\n    <div>\n      <button\n        onClick={() => {\n          const arr = Array(3)\n            .fill(1)\n            .map(() => `${Math.floor(Math.random() * 10)}`);\n          setState({ ids: arr });\n        }}\n      >\n        变更数组state\n      </button>\n      <div>ids: {JSON.stringify(state.ids)}</div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/use-url-state/demo/demo4.tsx",
    "content": "/**\n * title: Multi-state management (split)\n * desc: useUrlState can handle multiple useUrlState updates simultaneously\n *\n * title.zh-CN: 多状态管理 (拆分)\n * desc.zh-CN: useUrlState 可以同时处理多个 useUrlState 更新\n */\n\nimport useUrlState from '@ahooksjs/use-url-state';\n\nexport default () => {\n  const [page, setPage] = useUrlState({ page: '1' });\n  const [pageSize, setPageSize] = useUrlState({ pageSize: '10' });\n\n  return (\n    <>\n      <div>\n        page: {page.page}\n        <span style={{ paddingLeft: 8 }}>\n          <button\n            onClick={() => {\n              setPage((s) => ({ page: `${Number(s.page) + 1}` }));\n            }}\n          >\n            +\n          </button>\n          <button\n            onClick={() => {\n              setPage((s) => ({ page: `${Number(s.page) - 1}` }));\n            }}\n            style={{ margin: '0 8px' }}\n          >\n            -\n          </button>\n          <button\n            onClick={() => {\n              setPage({ page: undefined });\n            }}\n          >\n            reset\n          </button>\n        </span>\n      </div>\n      <br />\n      <div>\n        pageSize: {pageSize.pageSize}\n        <span style={{ paddingLeft: 8 }}>\n          <button\n            onClick={() => {\n              setPageSize((s) => ({ pageSize: `${Number(s.pageSize) + 1}` }));\n            }}\n          >\n            +\n          </button>\n          <button\n            onClick={() => {\n              setPageSize((s) => ({ pageSize: `${Number(s.pageSize) - 1}` }));\n            }}\n            style={{ margin: '0 8px' }}\n          >\n            -\n          </button>\n          <button\n            onClick={() => {\n              setPageSize({ pageSize: undefined });\n            }}\n          >\n            reset\n          </button>\n        </span>\n      </div>\n      <div>\n        <button\n          onClick={async () => {\n            await setPageSize({ pageSize: undefined });\n            await setPage({ page: undefined });\n          }}\n        >\n          reset all\n        </button>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/use-url-state/gulpfile.js",
    "content": "const commonConfig = require('../../gulpfile');\n\nexports.default = commonConfig.default;\n"
  },
  {
    "path": "packages/use-url-state/package.json",
    "content": "{\n  \"name\": \"@ahooksjs/use-url-state\",\n  \"version\": \"3.6.0\",\n  \"description\": \"A hook that stores the state into url query parameters.\",\n  \"main\": \"./lib/index.js\",\n  \"module\": \"./es/index.js\",\n  \"types\": \"./lib/index.d.ts\",\n  \"unpkg\": \"dist/ahooks-use-url-state.js\",\n  \"files\": [\n    \"dist\",\n    \"lib\",\n    \"es\",\n    \"package.json\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/alibaba/hooks.git\"\n  },\n  \"scripts\": {\n    \"build\": \"gulp && webpack-cli\",\n    \"test\": \"vitest run --color\",\n    \"test:cov\": \"vitest run --color --coverage\",\n    \"tsc\": \"tsc --noEmit\"\n  },\n  \"publishConfig\": {\n    \"registry\": \"https://registry.npmjs.org/\"\n  },\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/alibaba/hooks/issues\"\n  },\n  \"homepage\": \"https://github.com/alibaba/hooks\",\n  \"gitHead\": \"11f6ad571bd365c95ecb9409ca3050cbbfc9b34a\",\n  \"dependencies\": {\n    \"@babel/runtime\": \"^7.21.0\",\n    \"ahooks\": \"^3.4.1\",\n    \"query-string\": \"^8.1.0\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\",\n    \"react-dom\": \"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0\",\n    \"react-router\": \"^5.0.0 || ^6.0.0 || ^7.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/use-url-state/src/index.ts",
    "content": "import { useMemoizedFn, useUpdate } from 'ahooks';\nimport qs from 'query-string';\nimport type { ParseOptions, StringifyOptions } from 'query-string';\nimport { useMemo, useRef } from 'react';\nimport type * as React from 'react';\nimport * as tmp from 'react-router';\n\n// ignore waring `\"export 'useNavigate' (imported as 'rc') was not found in 'react-router'`\nconst rc = tmp as any;\n\nexport interface Options {\n  navigateMode?: 'push' | 'replace';\n  parseOptions?: ParseOptions;\n  stringifyOptions?: StringifyOptions;\n}\n\nconst baseParseConfig: ParseOptions = {\n  parseNumbers: false,\n  parseBooleans: false,\n};\n\nconst baseStringifyConfig: StringifyOptions = {\n  skipNull: false,\n  skipEmptyString: false,\n};\n\ntype UrlState = Record<string, any>;\n\nconst useUrlState = <S extends UrlState = UrlState>(\n  baseState?: S | (() => S),\n  options?: Options,\n) => {\n  type State = Partial<{\n    [key in keyof S]: Required<S>[key] extends any[] ? string[] : string;\n  }>;\n\n  const { navigateMode = 'push', parseOptions, stringifyOptions } = options || {};\n\n  const mergedParseOptions = { ...baseParseConfig, ...parseOptions };\n  const mergedStringifyOptions = {\n    ...baseStringifyConfig,\n    ...stringifyOptions,\n  };\n\n  const location = rc.useLocation();\n\n  // react-router v5\n  const history = rc.useHistory?.();\n\n  // react-router v6 & v7\n  const navigate = rc.useNavigate?.();\n\n  const update = useUpdate();\n\n  const baseStateRef = useRef(typeof baseState === 'function' ? baseState() : baseState || {});\n\n  const queryFromUrl = useMemo(() => {\n    return qs.parse(location.search, mergedParseOptions);\n  }, [location.search]);\n\n  const targetQuery = useMemo<State>(\n    () => ({\n      ...baseStateRef.current,\n      ...queryFromUrl,\n    }),\n    [queryFromUrl],\n  );\n\n  const setState = (s: React.SetStateAction<State>) => {\n    const newQuery = typeof s === 'function' ? s(targetQuery) : s;\n\n    // 1. 如果 setState 后，search 没变化，就需要 update 来触发一次更新。比如 demo1 直接点击 clear，就需要 update 来触发更新。\n    // 2. update 和 history 的更新会合并，不会造成多次更新\n    update();\n    if (history) {\n      history[navigateMode](\n        {\n          hash: location.hash,\n          search: qs.stringify({ ...queryFromUrl, ...newQuery }, mergedStringifyOptions) || '?',\n        },\n        location.state,\n      );\n    }\n    if (navigate) {\n      navigate(\n        {\n          hash: location.hash,\n          search: qs.stringify({ ...queryFromUrl, ...newQuery }, mergedStringifyOptions) || '?',\n        },\n        {\n          replace: navigateMode === 'replace',\n          state: location.state,\n        },\n      );\n    }\n  };\n\n  return [targetQuery, useMemoizedFn(setState)] as const;\n};\n\nexport default useUrlState;\n"
  },
  {
    "path": "packages/use-url-state/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@ahooksjs/use-url-state\": [\"./src/index.ts\"]\n    }\n  }\n}\n"
  },
  {
    "path": "packages/use-url-state/tsconfig.pro.json",
    "content": "{\n  \"extends\": \"../../tsconfig.pro.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/use-url-state/use-url-state.en-US.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /\n---\n\n# useUrlState\n\nA hook that store the state into url query.\n\n## Install\n\n```bash\nnpm i @ahooksjs/use-url-state -S\n```\n\n> This hook relies on useLocation & useHistory & useNavigate from `react-router`, to use this hook, you need to ensure\n>\n> 1\\. Your project is using `react-router` version 5.0 or 6.0 to manage routing\n>\n> 2\\. Installed @ahooksjs/use-url-state\n\n## Usage\n\n```js\nimport useUrlState from '@ahooksjs/use-url-state';\n```\n\n## Examples\n\n### CodeSandbox Demo\n\nReact Router V5: https://codesandbox.io/s/suspicious-feather-cz4e0?file=/App.tsx\n\nReact Router V6: https://codesandbox.io/s/autumn-shape-odrt9?file=/App.tsx\n\n### Default usage\n\n<code src=\"./demo/demo1.tsx\" hideActions='[\"CSB\"]' />\n\n### Multi-state management\n\n<code src=\"./demo/demo2.tsx\" hideActions='[\"CSB\"]' />\n\n### Multi-state management (split)\n\n<code src=\"./demo/demo4.tsx\" hideActions='[\"CSB\"]' />\n\n### Custom query-string options\n\n<code src=\"./demo/demo3.tsx\" hideActions='[\"CSB\"]' />\n\n## API\n\n```typescript\nconst [state, setState] = useUrlState(baseState, options);\n```\n\n### Params\n\n| Property  | Description                                                                                    | Type           | Default |\n| --------- | ---------------------------------------------------------------------------------------------- | -------------- | ------- |\n| baseState | URL search params will be merged into BaseState | `S \\| () => S` | -       |\n| options   | Url config                                                                                     | `Options`      | -       |\n\n### Options\n\n| Property         | Description                                                                                                 | Type                  | Default  |\n| ---------------- | ----------------------------------------------------------------------------------------------------------- | --------------------- | -------- |\n| navigateMode     | Type of history navigate mode                                                                               | `'push' \\| 'replace'` | `'push'` |\n| parseOptions     | [parse](https://github.com/sindresorhus/query-string#parsestring-options) options of `query-string`         | `ParseOptions`        | -        |\n| stringifyOptions | [stringify](https://github.com/sindresorhus/query-string#stringifyobject-options) options of `query-string` | `StringifyOptions`    | -        |\n\n### Result\n\n| Property | Description                                  | Type                                              |\n| -------- | -------------------------------------------- | ------------------------------------------------- |\n| state    | Url query object                             | `object`                                          |\n| setState | Same as useState, but state should be object | `(state: S) => void \\| (() => ((state: S) => S))` |\n"
  },
  {
    "path": "packages/use-url-state/use-url-state.zh-CN.md",
    "content": "---\nnav:\n  path: /hooks\ngroup:\n  path: /\n---\n\n# useUrlState\n\n通过 url query 来管理 state 的 Hook。\n\n## 安装\n\n```bash\nnpm i @ahooksjs/use-url-state -S\n```\n\n> 该 Hooks 基于 `react-router` 的 useLocation & useHistory & useNavigate 进行 query 管理，所以使用该 Hooks 之前，你需要保证\n>\n> 1\\. 你项目正在使用 `react-router` 5.x 或 6.x 版本来管理路由\n>\n> 2\\. 独立安装了 @ahooksjs/use-url-state\n\n## 使用\n\n```js\nimport useUrlState from '@ahooksjs/use-url-state';\n```\n\n## 代码演示\n\n### 在线演示\n\nReact Router V5: https://codesandbox.io/s/suspicious-feather-cz4e0?file=/App.tsx\n\nReact Router V6: https://codesandbox.io/s/autumn-shape-odrt9?file=/App.tsx\n\n### 基础用法\n\n<code src=\"./demo/demo1.tsx\" hideActions='[\"CSB\"]' />\n\n### 多状态管理\n\n<code src=\"./demo/demo2.tsx\" hideActions='[\"CSB\"]' />\n\n### 多状态管理（拆分）\n\n<code src=\"./demo/demo4.tsx\" hideActions='[\"CSB\"]' />\n\n### 自定义 query-string 配置\n\n<code src=\"./demo/demo3.tsx\" hideActions='[\"CSB\"]' />\n\n## API\n\n```typescript\nconst [state, setState] = useUrlState(baseState, options);\n```\n\n### Params\n\n| 参数      | 说明                                                           | 类型           | 默认值 |\n| --------- | -------------------------------------------------------------- | -------------- | ------ |\n| baseState | 基准状态，URL 查询参数会在此基础上进行合并 | `S \\| () => S` | -      |\n| options   | url 配置                                                       | `Options`      | -      |\n\n### Options\n\n| 参数             | 说明                                                                                                    | 类型                  | 默认值   |\n| ---------------- | ------------------------------------------------------------------------------------------------------- | --------------------- | -------- |\n| navigateMode     | 状态变更时切换 history 的方式                                                                           | `'push' \\| 'replace'` | `'push'` |\n| parseOptions     | `query-string` [parse](https://github.com/sindresorhus/query-string#parsestring-options) 的配置         | `ParseOptions`        | -        |\n| stringifyOptions | `query-string` [stringify](https://github.com/sindresorhus/query-string#stringifyobject-options) 的配置 | `StringifyOptions`    | -        |\n\n### Result\n\n| 参数     | 说明                                    | 类型                                              |\n| -------- | --------------------------------------- | ------------------------------------------------- |\n| state    | url query 对象                          | `object`                                          |\n| setState | 用法同 useState，但 state 需要是 object | `(state: S) => void \\| (() => ((state: S) => S))` |\n"
  },
  {
    "path": "packages/use-url-state/vitest.config.ts",
    "content": "import { resolve } from 'node:path';\nimport { defineConfig } from 'vitest/config';\n\n// https://cn.vitest.dev/guide/\nexport default defineConfig({\n  resolve: {\n    alias: {\n      src: resolve(__dirname, 'src'),\n    },\n  },\n  test: {\n    reporters: ['default', 'verbose'],\n    environment: 'jsdom',\n    include: ['__tests__/**/*.spec.ts?(x)'],\n    coverage: {\n      provider: 'istanbul',\n      include: ['src/**/*.ts'],\n    },\n  },\n});\n"
  },
  {
    "path": "packages/use-url-state/webpack.config.js",
    "content": "const { merge } = require('webpack-merge');\nconst common = require('../../webpack.common.js');\nconst path = require('node:path');\n\nmodule.exports = merge(common, {\n  entry: './es/index.js',\n  output: {\n    filename: 'ahooks-use-url-state.js',\n    library: 'ahooksUseUrlState',\n    path: path.resolve(__dirname, './dist'),\n  },\n});\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - \"packages/*\"\n"
  },
  {
    "path": "public/style.css",
    "content": ".__dumi-default-navbar-logo {\n  color: transparent !important;\n}\n\n.__dumi-default-layout-hero img {\n  width: 250px;\n}\n\na[title=\"站长统计\"] {\n  display: none;\n}\n\ninput,\nbutton {\n  padding: 4px;\n}\n\ninput[type=\"number\"] {\n  border: 1px solid rgb(118, 118, 118);\n}\n\n[data-prefers-color=\"dark\"] button {\n  color: #141414;\n}\n\n[data-prefers-color=\"dark\"] input {\n  color: #141414;\n}\n\n#logo-version {\n  flex: 1;\n  margin-top: 10px;\n  margin-left: -30px;\n  color: #a8a8ad;\n  font-size: 16px;\n}\n"
  },
  {
    "path": "public/useExternal/bootstrap-badge.css",
    "content": ".badge {\n  display: inline-block;\n  padding: 0.25em 0.4em;\n  font-size: 75%;\n  font-weight: 700;\n  line-height: 1;\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: baseline;\n  border-radius: 0.25rem;\n  transition:\n    color 0.15s ease-in-out,\n    background-color 0.15s ease-in-out,\n    border-color 0.15s ease-in-out,\n    box-shadow 0.15s ease-in-out;\n  margin-right: 0.24rem;\n}\n.badge-pill {\n  padding-right: 0.6em;\n  padding-left: 0.6em;\n  border-radius: 10rem;\n}\n.badge-primary {\n  color: #fff;\n  background-color: #007bff;\n}\n\n.badge-secondary {\n  color: #fff;\n  background-color: #6c757d;\n}\n\n.badge-success {\n  color: #fff;\n  background-color: #28a745;\n}\n\n.badge-danger {\n  color: #fff;\n  background-color: #dc3545;\n}\n\n.badge-warning {\n  color: #212529;\n  background-color: #ffc107;\n}\n\n.badge-info {\n  color: #fff;\n  background-color: #17a2b8;\n}\n\n.badge-light {\n  color: #212529;\n  background-color: #f8f9fa;\n}\n\n.badge-dark {\n  color: #fff;\n  background-color: #343a40;\n}\n"
  },
  {
    "path": "public/useExternal/test-external-script.js",
    "content": "window.TEST_SCRIPT = {\n  start: function () {\n    return 'Hello World';\n  },\n};\n"
  },
  {
    "path": "scripts/build-with-relative-paths.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\n\nconst configPath = path.join(__dirname, '../config/config.ts');\nconst backupPath = path.join(__dirname, '../config/config.ts.backup');\n\n// 备份原配置\nfs.copyFileSync(configPath, backupPath);\n\ntry {\n  // 读取配置文件\n  let config = fs.readFileSync(configPath, 'utf8');\n\n  // 修改配置\n  config = config.replace(/publicPath: ['\"].*['\"],/, \"publicPath: '/hooks/',\");\n  config = config.replace(\n    /{ rel: 'stylesheet', href: '\\/style\\.css' }/,\n    \"{ rel: 'stylesheet', href: '/hooks/style.css' }\",\n  );\n  config = config.replace(/logo: '\\/logo\\.svg',/, \"logo: '/hooks/logo.svg',\");\n\n  // 写入修改后的配置\n  fs.writeFileSync(configPath, config);\n\n  // 运行构建命令\n  execSync('pnpm run build:doc', { stdio: 'inherit' });\n\n  // 进入 dist 目录\n  process.chdir(path.join(__dirname, '../dist'));\n\n  // 初始化 git 仓库（如果不存在）\n  try {\n    execSync('git init', { stdio: 'inherit' });\n  } catch (e) {\n    // 如果已经初始化过，忽略错误\n  }\n\n  // 添加所有文件\n  execSync('git add .', { stdio: 'inherit' });\n\n  // 提交更改\n  execSync('git commit -m \"chore: update gh-pages\"', { stdio: 'inherit' });\n\n  // 添加远程仓库（如果不存在）\n  try {\n    execSync('git remote add origin git@github.com:alibaba/hooks.git', { stdio: 'inherit' });\n  } catch (e) {\n    // 如果远程仓库已存在，忽略错误\n  }\n\n  // 强制推送到 gh-pages 分支\n  execSync('git push -f origin HEAD:gh-pages', { stdio: 'inherit' });\n\n  // 返回到项目根目录\n  process.chdir(path.join(__dirname, '..'));\n} finally {\n  // 恢复原配置\n  fs.copyFileSync(backupPath, configPath);\n  fs.unlinkSync(backupPath);\n}\n"
  },
  {
    "path": "tsconfig.base.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"compilerOptions\": {\n    // 模块解析\n    \"lib\": [\"ESNext\", \"DOM\"], // 使用的库的声明\n    \"jsx\": \"react-jsx\", // JSX 语法支持\n    \"target\": \"ESNext\", // 生成的目标代码规范\n    \"module\": \"ESNext\", // 生成的模块规范\n    \"moduleResolution\": \"bundler\", // 可以在使用用 exports 声明类型的同时，使用相对路径模块可以不写扩展名\n    \"verbatimModuleSyntax\": true, // 禁止混用 ESM 或 CommonJS 的导入导出语法\n    \"resolveJsonModule\": true, // 允许导入 JSON 文件\n    \"noEmit\": true,\n    // 路径解析\n    \"baseUrl\": \"${configDir}\",\n    \"paths\": {\n      \"src/*\": [\"${configDir}/src/*\"]\n    },\n    \"types\": [],\n    \"rootDir\": \"${configDir}\",\n    \"outDir\": \"${configDir}/dist\",\n    \"sourceMap\": true, // 生成源映射文件\n    \"sourceRoot\": \"${configDir}\", // 源文件的根目录\n    \"declaration\": true, // 生成 .d.ts 文件\n    // 检查\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"forceConsistentCasingInFileNames\": true\n  },\n  \"include\": [\n    \"${configDir}/src\",\n    \"${configDir}/types\",\n    \"${configDir}/scripts\",\n    \"${configDir}/docs\",\n    \"${configDir}/tests\",\n    \"${configDir}/e2e\",\n    \"${configDir}/demo\",\n    \"${configDir}/example\",\n    \"${configDir}/site\"\n  ]\n}\n"
  },
  {
    "path": "tsconfig.pro.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES5\",\n    \"moduleResolution\": \"node\",\n    \"jsx\": \"react\",\n    \"esModuleInterop\": true,\n    \"downlevelIteration\": true,\n    \"sourceMap\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"],\n      \"@ahooksjs/use-url-state\": [\"./packages/use-url-state/src/index.ts\"]\n    },\n    \"types\": [],\n    \"allowJs\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"skipLibCheck\": true,\n    \"declaration\": false,\n    \"strictNullChecks\": true,\n    \"importHelpers\": true\n  },\n  \"exclude\": [\n    \"node_modules\",\n    \"lib\",\n    \"es\",\n    \"dist\",\n    \"**/__tests__\",\n    \"**/__test__\",\n    \"**/demo\",\n    \"example\",\n    \"gulpfile.js\",\n    \"vitest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "umd.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Hello World</title>\n    <script src=\"https://unpkg.com/react@17/umd/react.development.js\"></script>\n    <script src=\"https://unpkg.com/react-dom@17/umd/react-dom.development.js\"></script>\n\n    <!-- Don't use this in production: -->\n    <script src=\"https://unpkg.com/@babel/standalone/babel.min.js\"></script>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script src=\"./packages/hooks/dist/ahooks.js\"></script>\n\n    <script type=\"text/babel\">\n      const { useToggle } = window.ahooks;\n      const Demo = () => {\n        const [show, { toggle }] = useToggle(false);\n        return (\n          <div>\n            <button onClick={toggle}>Toggle</button>\n            {show && <div>Hello World</div>}\n          </div>\n        );\n      };\n      ReactDOM.render(<Demo />, document.getElementById('root'));\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  test: {\n    projects: ['packages/*/vitest.config.ts'],\n    coverage: {\n      provider: 'istanbul',\n      include: ['packages/*/src/**/*.ts'],\n      reporter: ['text', 'lcov'],\n    },\n  },\n});\n"
  },
  {
    "path": "webpack.common.js",
    "content": "module.exports = {\n  output: {\n    libraryTarget: 'umd',\n    globalObject: 'this',\n  },\n  mode: 'production',\n  resolve: {\n    extensions: ['.json', '.js'],\n  },\n  // module: {\n  //   rules: [\n  //     {\n  //       test: /\\.jsx?$/,\n  //       use: {\n  //         loader: 'babel-loader',\n  //       },\n  //     }\n  //   ],\n  // },\n  externals: [\n    {\n      react: 'React',\n    },\n  ],\n};\n"
  }
]