Skip to content

fix: Rule deps on the useWatch#776

Merged
zombieJ merged 7 commits intomasterfrom
fix-deps
Mar 4, 2026
Merged

fix: Rule deps on the useWatch#776
zombieJ merged 7 commits intomasterfrom
fix-deps

Conversation

@zombieJ
Copy link
Member

@zombieJ zombieJ commented Mar 3, 2026

鸡生蛋蛋生鸡的问题,useWatch 之前有问题会导致额外的 render,而正好这又触发了 validator 使得可以拿到最新的 watch 值。在 validateRule 里由于本来就是异步的,所以添加一个 raf 延迟在 getRules 之前以保证得到的是最新的规则。

ref ant-design/ant-design#56962

Summary by CodeRabbit

发行说明

  • Bug 修复

    • 优化了表单验证时序:在执行验证规则前等待一帧渲染,解决在规则中使用 useWatch 导致的时序问题。
  • Tests

    • 更新并增强了测试:引入对延迟行为的模拟与定时器控制,添加依赖字段变化场景的测试,移除调试输出,调整若干测试以适配新时序。

zombieJ and others added 4 commits March 3, 2026 14:46
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace @rc-component/util/lib/raf with custom delayFrame utility
- Export macroTask for test usage
- Add delayUtil.ts with mockable delayFrame implementation
- Update tests to use fake timers and waitFakeTime for better control
- Add jest.mock for delayUtil in test files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Mar 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
field-form Ready Ready Preview, Comment Mar 3, 2026 9:57am

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求旨在解决表单验证规则在依赖于 useWatch 观察的值时,可能因 useWatch 的异步特性和随后的重新渲染而使用过时数据进行评估的问题。通过在验证过程中,于规则检索之前引入一个帧延迟,系统现在能够确保验证始终使用最新的表单状态,从而避免了不一致的验证结果。

Highlights

  • 修复 useWatch 依赖规则问题: 解决了 useWatch 导致的额外渲染可能触发验证器获取到旧的 watch 值的问题,确保在 validateRule 中获取到最新的规则。
  • 引入帧延迟机制: 在 validateRule 中,通过添加 raf (requestAnimationFrame) 延迟,确保在 getRules 之前等待一帧渲染,从而获取到最新的规则状态。
  • 测试用例增强: 更新了多个测试文件,以模拟新的 delayUtil 并调整了假计时器的时序,同时新增了一个测试用例来验证依赖字段改变时错误清除的场景。

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/Field.tsx
    • 引入了 delayFrame 工具函数。
    • validateRule 方法中,getRules 调用前增加了 await delayFrame(),以确保获取到最新的规则。
    • 调整了 FieldProps 接口的格式。
  • src/hooks/useNotifyWatch.ts
    • macroTask 函数导出,以便于测试使用。
  • src/utils/delayUtil.ts
    • 新增了 delayUtil.ts 文件,导出一个异步函数 delayFrame,该函数利用 requestAnimationFrame 实现帧延迟。
  • tests/mocks/src/utils/delayUtil.ts
    • delayFrame 提供了测试用的 mock 实现。
  • tests/common/timeout.ts
    • 引入了 macroTask
    • waitFakeTime 函数中增加了 macroTask 的等待逻辑,以确保测试中的时序正确性。
  • tests/context.test.tsx
    • 添加了 jest.mock('../src/utils/delayUtil') 来模拟 delayUtil
  • tests/dependencies.test.tsx
    • afterEach 钩子中添加了 jest.useRealTimers()
    • 在特定的测试用例中添加了 jest.useFakeTimers()
    • changeValue 后增加了 await waitFakeTime()
    • 新增了一个测试用例,验证当依赖字段改变且规则变为 false 时,错误能够被清除。
  • tests/index.test.tsx
    • 添加了 jest.mock('../src/utils/delayUtil') 来模拟 delayUtil
  • tests/list.test.tsx
    • 添加了 jest.mock('../src/utils/delayUtil') 来模拟 delayUtil
  • tests/validate-warning.test.tsx
    • 添加了 jest.mock('../src/utils/delayUtil') 来模拟 delayUtil
  • tests/validate.test.tsx
    • 添加了 jest.mock('../src/utils/delayUtil') 来模拟 delayUtil
Activity
  • 此拉取请求旨在解决 GitHub issue #56962 中描述的问题。
  • 作者 zombieJ 引入了一个新的 delayFrame 工具函数,用于在表单验证逻辑中引入帧延迟。
  • 核心改动在于 src/Field.tsx 中,在获取验证规则之前等待一个动画帧,以确保 useWatch 依赖的规则是最新的。
  • 为了支持这一改动,新增了 src/utils/delayUtil.ts 文件及其对应的 mock 文件 tests/__mocks__/src/utils/delayUtil.ts
  • 多个测试文件(tests/context.test.tsx, tests/dependencies.test.tsx, tests/index.test.tsx, tests/list.test.tsx, tests/validate-warning.test.tsx, tests/validate.test.tsx)都更新了,以模拟新的 delayUtil 并调整了测试计时器行为。
  • tests/dependencies.test.tsx 中添加了一个新的测试用例,以验证依赖字段变化时错误清除的正确性。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a34250 and 8c0135d.

📒 Files selected for processing (1)
  • tests/__mocks__/src/utils/delayUtil.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/mocks/src/utils/delayUtil.ts

Walkthrough

在验证规则执行前引入帧延迟:新增 delayFrame() 工具并在 Field.validateRules 中 await 该延迟,以确保渲染完成(支持在规则内使用 useWatch);同时为测试添加模块 mock 和时序调整。

Changes

Cohort / File(s) Summary
核心功能 - 帧延迟验证
src/Field.tsx, src/utils/delayUtil.ts
新增 delayFrame()(基于 raf)并在 validateRules 中 await 该函数,确保在验证前等待下一帧渲染完成。
测试模拟 - 延迟工具
tests/__mocks__/src/utils/delayUtil.ts
为测试环境添加 delayFrame() 的 mock 实现(立即 resolve 的 Promise)。
测试文件 - 引入 mock
tests/context.test.tsx, tests/index.test.tsx, tests/list.test.tsx, tests/validate-warning.test.tsx, tests/validate.test.tsx
在多个测试文件顶部加入 jest.mock('../src/utils/delayUtil'),将真实延迟替换为 mock。
测试时序与依赖用例
tests/common/timeout.ts, tests/dependencies.test.tsx
waitFakeTime 中增加额外的时间推进步骤(各 11ms),并在依赖相关测试中使用 waitFakeTime + jest.useFakeTimers(),新增依赖字段导致错误清除的用例。
测试微调
tests/initialValue.test.tsx, tests/useWatch.test.tsx
移除多余的 console.log 并做小幅格式调整(空行)。

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • crazyair

Poem

🐰 轻轻一跃入帧间,
等待渲染把梦还,
规则静听 useWatch 言,
测试模仿奏节拍,
验证安然不慌乱。

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确总结了主要变化:通过添加帧延迟来修复useWatch导致的依赖关系问题,与所有文件变更的核心目的相符。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-deps

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

本次 PR 旨在修复一个因 useWatch 导致的竞态条件问题,该问题可能导致校验规则无法获取到最新的值。通过在校验前增加一个 requestAnimationFrame 延迟来解决此问题,这个改动是合理且清晰的。实现上通过创建一个 delayFrame 工具函数来完成,非常整洁。相关的测试也已更新,对新的工具函数进行了 mock,并增加了一个新的测试用例来覆盖修复的场景,这很好。我只有一个关于测试 mock 文件中遗留的调试语句的小建议。

@codecov
Copy link

codecov bot commented Mar 3, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.54%. Comparing base (fa2483d) to head (8c0135d).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #776   +/-   ##
=======================================
  Coverage   99.54%   99.54%           
=======================================
  Files          19       20    +1     
  Lines        1311     1324   +13     
  Branches      329      308   -21     
=======================================
+ Hits         1305     1318   +13     
  Misses          6        6           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/common/timeout.ts (1)

14-14: 建议消除 11 这个魔法数字。

建议抽成具名常量,避免后续维护时误解其语义。

♻️ 可选改动示例
 export async function waitFakeTime(timeout: number = 10) {
+  const MACRO_TASK_FLUSH_MS = 11;
   await act(async () => {
     await new Promise<void>(resolve => {
       macroTask(resolve);
-      jest.advanceTimersByTime(11);
+      jest.advanceTimersByTime(MACRO_TASK_FLUSH_MS);
     });
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/common/timeout.ts` at line 14, Replace the magic number 11 used in
jest.advanceTimersByTime(11) with a named constant to clarify meaning and ease
maintenance: define a descriptive constant (e.g., TIMEOUT_MS or
DEFAULT_TIMEOUT_MS) near the top of tests/common/timeout.ts (or export it if
shared) and use that constant in place of the literal in the
jest.advanceTimersByTime call to make the intent explicit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/__mocks__/src/utils/delayUtil.ts`:
- Line 2: 在 tests/__mocks__/src/utils/delayUtil.ts 的 mock 实现中移除调试日志
console.log('@@@'):打开该文件找到 console.log('@@@') 并删除该行(或改为无副作用的注释),确保 mock
只包含必要的导出/实现且不在 CI 输出中产生噪音,随后运行相关测试以确认未影响断言或行为。

In `@tests/dependencies.test.tsx`:
- Around line 279-303: The test intermittently asserts before async validation
settles; update the test that defines Demo (using InfoField,
Form.useForm/useWatch) to use fake timers and explicitly advance them or wait
after changeValue calls: call jest.useFakeTimers() at test start, then after
each changeValue(getInput(...)) call advance timers (jest.runAllTimers() or
jest.advanceTimersByTime(...)) inside an act/await helper so the validation
finishes before calling matchError; ensure to restore timers after the test.

---

Nitpick comments:
In `@tests/common/timeout.ts`:
- Line 14: Replace the magic number 11 used in jest.advanceTimersByTime(11) with
a named constant to clarify meaning and ease maintenance: define a descriptive
constant (e.g., TIMEOUT_MS or DEFAULT_TIMEOUT_MS) near the top of
tests/common/timeout.ts (or export it if shared) and use that constant in place
of the literal in the jest.advanceTimersByTime call to make the intent explicit.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa2483d and ffed1b2.

📒 Files selected for processing (11)
  • src/Field.tsx
  • src/hooks/useNotifyWatch.ts
  • src/utils/delayUtil.ts
  • tests/__mocks__/src/utils/delayUtil.ts
  • tests/common/timeout.ts
  • tests/context.test.tsx
  • tests/dependencies.test.tsx
  • tests/index.test.tsx
  • tests/list.test.tsx
  • tests/validate-warning.test.tsx
  • tests/validate.test.tsx

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
tests/dependencies.test.tsx (1)

279-303: ⚠️ Potential issue | 🟡 Minor

新增依赖回归用例仍缺少显式等待,存在时序抖动风险。

Line 297 和 Line 301 之后直接断言,可能早于异步校验完成。建议该用例也使用 fake timers,并在每次输入后等待一次。

🧪 建议修复
   it('error should be cleared when dependency field changes and rule becomes false', async () => {
+    jest.useFakeTimers();
+
     const Demo = () => {
       const [form] = Form.useForm();
       const type = Form.useWatch('type', form);
@@
     const { container } = render(<Demo />);
     await changeValue(getInput(container, 1), ['bamboo', '']);
+    await waitFakeTime();
     matchError(container, true);
@@
     // Change type to make rule true
     await changeValue(getInput(container), '1');
+    await waitFakeTime();
     matchError(container, false);
   });
#!/bin/bash
# 只读验证:检查该测试块是否包含 useFakeTimers 和等待次数
python - <<'PY'
from pathlib import Path
p = Path("tests/dependencies.test.tsx")
txt = p.read_text(encoding="utf-8")
title = "it('error should be cleared when dependency field changes and rule becomes false'"
start = txt.find(title)
if start == -1:
    print("test block not found")
    raise SystemExit(1)
end = txt.find("\n  });", start)
block = txt[start:end]
print("has useFakeTimers:", "jest.useFakeTimers()" in block)
print("waitFakeTime count:", block.count("waitFakeTime("))
print("changeValue count:", block.count("changeValue("))
PY
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/dependencies.test.tsx` around lines 279 - 303, The test "error should
be cleared when dependency field changes and rule becomes false" risks flakiness
because it asserts immediately after asynchronous validation; update the test to
use fake timers (jest.useFakeTimers()) and await a waitFakeTime() (or advance
timers) after each changeValue call so validation completes before assertions;
specifically, add jest.useFakeTimers() at the start of the test and insert await
waitFakeTime() (or equivalent timer advance) after
changeValue(getInput(container, 1)... ) and after
changeValue(getInput(container), '1') so that the asserts via matchError run
only after async validation finishes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/__mocks__/src/utils/delayUtil.ts`:
- Around line 2-3: There are two identical "return Promise.resolve();"
statements in tests/__mocks__/src/utils/delayUtil.ts causing an unreachable
statement lint error; remove the duplicate so only a single "return
Promise.resolve();" remains (i.e., keep the first return branch and delete the
second unreachable one) within the mock function in delayUtil.ts.

---

Duplicate comments:
In `@tests/dependencies.test.tsx`:
- Around line 279-303: The test "error should be cleared when dependency field
changes and rule becomes false" risks flakiness because it asserts immediately
after asynchronous validation; update the test to use fake timers
(jest.useFakeTimers()) and await a waitFakeTime() (or advance timers) after each
changeValue call so validation completes before assertions; specifically, add
jest.useFakeTimers() at the start of the test and insert await waitFakeTime()
(or equivalent timer advance) after changeValue(getInput(container, 1)... ) and
after changeValue(getInput(container), '1') so that the asserts via matchError
run only after async validation finishes.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffed1b2 and 6f5d2c4.

📒 Files selected for processing (4)
  • tests/__mocks__/src/utils/delayUtil.ts
  • tests/dependencies.test.tsx
  • tests/initialValue.test.tsx
  • tests/useWatch.test.tsx
✅ Files skipped from review due to trivial changes (2)
  • tests/useWatch.test.tsx
  • tests/initialValue.test.tsx

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/common/timeout.ts`:
- Around line 10-22: The shared helper waitFakeTime in tests/common/timeout.ts
unconditionally advances fake timers twice by 11ms (total 22ms), which can
affect unrelated test timing; change waitFakeTime to accept an optional
parameter (e.g., extraAdvanceMs or advanceExtraFrames boolean) defaulting to
0/false and only call the additional act+jest.advanceTimersByTime when that
parameter is truthy, and update any callers that need the extra frame to pass
the flag so other tests keep their original timing; reference the waitFakeTime
helper and the two occurrences of act + jest.advanceTimersByTime blocks in this
file when making the change.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6f5d2c4 and 1a34250.

📒 Files selected for processing (1)
  • tests/common/timeout.ts

Comment on lines +10 to +22
await act(async () => {
await new Promise<void>(resolve => {
setTimeout(resolve, 11);
jest.advanceTimersByTime(11);
});
});

await act(async () => {
await new Promise<void>(resolve => {
setTimeout(resolve, 11);
jest.advanceTimersByTime(11);
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

waitFakeTime 在共享 helper 中无条件多推进 22ms,容易影响无关测试时序。

Line 10-22 的两次固定 11ms 推进会作用于所有调用方,不仅仅是 useWatch 场景;这可能提前触发其它定时逻辑,导致测试“误通过”或时序不稳定。建议把额外帧推进改为可配置参数,仅在需要的用例里显式开启。

可参考的最小改法
 export async function waitFakeTime(timeout: number = 10) {
-  await act(async () => {
-    await new Promise<void>(resolve => {
-      setTimeout(resolve, 11);
-      jest.advanceTimersByTime(11);
-    });
-  });
-
-  await act(async () => {
-    await new Promise<void>(resolve => {
-      setTimeout(resolve, 11);
-      jest.advanceTimersByTime(11);
-    });
-  });
+export async function waitFakeTime(timeout: number = 10, preFrames: number = 0) {
+  for (let i = 0; i < preFrames; i += 1) {
+    await act(async () => {
+      await new Promise<void>(resolve => {
+        setTimeout(resolve, 11);
+        jest.advanceTimersByTime(11);
+      });
+    });
+  }

   await act(async () => {
     await Promise.resolve();
     jest.advanceTimersByTime(timeout);
     await Promise.resolve();
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/common/timeout.ts` around lines 10 - 22, The shared helper waitFakeTime
in tests/common/timeout.ts unconditionally advances fake timers twice by 11ms
(total 22ms), which can affect unrelated test timing; change waitFakeTime to
accept an optional parameter (e.g., extraAdvanceMs or advanceExtraFrames
boolean) defaulting to 0/false and only call the additional
act+jest.advanceTimersByTime when that parameter is truthy, and update any
callers that need the extra frame to pass the flag so other tests keep their
original timing; reference the waitFakeTime helper and the two occurrences of
act + jest.advanceTimersByTime blocks in this file when making the change.

@crazyair
Copy link
Contributor

crazyair commented Mar 4, 2026

不用 useWatch 也一样,这个跟 useWatch 没有关系

import { changeValue, getInput } from './common';
import timeout from './common/timeout';

jest.mock('../src/utils/delayUtil');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

为啥要 mock,delayFrame 里面的 raf 不能在 test 中用吗,raf 有什么功能

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raf 见评论描述,测试里 mock 是因为 raf 需要 adv timer。而测试里有很多测试都是从 rc-form 里过来的,所以用的是真实的 timeout 导致测试会卡死。所以对部分老测试使用 mock 来保持时序,其他比较新的测试写法 fakeTimer 的不动也自己过了。

@zombieJ zombieJ merged commit ceb07ef into master Mar 4, 2026
13 checks passed
@zombieJ zombieJ deleted the fix-deps branch March 4, 2026 02:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants