Skip to content

fix: Avoid exponential loops when a large number of components are un… #755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

SuperBigSniper
Copy link

@SuperBigSniper SuperBigSniper commented Jun 17, 2025

I have a demo ready for you https://codesandbox.io/p/sandbox/rt8qcf

当有useWatch存在时,有大量的Field组件被卸载时,会导致react在commit阶段调用componentWillUnmount, 导致调用registerField的返回函数(在useWatch中是叫cancelRegister),然后调用notifyWatch,notifyWatch又会去调用this.getFieldsValue(), this.getFieldsValue()会去循环所有挂载的组件,就会导致循环指数增长,如果有50个组件就会有 (49+1)49/2 = 1225 次循环,如果有1000个组件就会有 (999+1) 999 / 2 = 499500 次循环,如果给notifyWatch增加promise.then或者setTimeout就可以解决这个问题,延迟执行后,已经卸载的组件就不会再出现在fieldEntities中,减少大量循环,且该commit对功能无影响

When a large number of Field components are unmounted, it will cause react to call componentWillUnmount during the commit phase, resulting in the call of the return function of registerField (called cancelRegister in useWatch). Then call 'notifyWatch', and 'notifyWatch' will call 'this.getFieldsValue()'. 'this.getFieldsValue()' will loop through all mounted components, which will cause the loop to grow exponentially. If there are 50 components, there will be (49+1)49/2 = 1225 loops. If there are 1000 components, there will be (999+1) 999/2 = 499,500 loops. If promise.then or setTimeout is added to notifyWatch, this problem can be solved. After the delayed execution, the uninstalled components will no longer appear in fieldEntities, reducing a large number of loops, and this commit has no impact on the functionality

Summary by CodeRabbit

Summary by CodeRabbit

  • Bug Fixes
    • 优化了表单字段注销时的通知机制,避免在大量组件同时卸载时出现性能问题。
    • 调整了测试用例,确保组件卸载后的异步状态更新正确完成,提升测试稳定性。

Copy link

vercel bot commented Jun 17, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
field-form ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 17, 2025 10:28am

Copy link

coderabbitai bot commented Jun 17, 2025

Walkthrough

本次更改调整了 FormStore 类中 registerField 方法的注销回调逻辑,将通知观察者的操作从同步调用改为通过微任务(Promise.resolve().then())异步调用,以避免在大量组件同时卸载时产生指数级的循环。测试中增加了卸载后等待异步状态更新完成的步骤。其他逻辑未发生变化。

Changes

文件/分组 变更摘要
src/useForm.ts 修改 registerField 注销回调,notifyWatch 从同步调用改为微任务异步调用。
tests/useWatch.test.tsx 卸载测试中增加异步等待,确保卸载触发的状态更新完成后再断言。

Suggested reviewers

  • zombieJ

Poem

兔子跳跃键盘旁,
代码微调步轻狂。
通知微任务守望,
卸载循环不成行。
异步等待添测试,
表单世界更安详。
🐇✨

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/useForm.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the config "prettier" to extend from. Please check that the name of the config is correct.

The config "prettier" was referenced from the config file in "/.eslintrc.js".

If you still have problems, please stop by https://eslint.org/chat/help to chat with the team.

tests/useWatch.test.tsx

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the config "prettier" to extend from. Please check that the name of the config is correct.

The config "prettier" was referenced from the config file in "/.eslintrc.js".

If you still have problems, please stop by https://eslint.org/chat/help to chat with the team.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate Unit Tests
  • Create PR with Unit Tests
  • Commit Unit Tests in branch master
  • Post Copyable Unit Tests in Comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai auto-generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6611c31 and 00b31b5.

📒 Files selected for processing (1)
  • src/useForm.ts (1 hunks)

src/useForm.ts Outdated
Comment on lines 685 to 689
// Avoid exponential loops when a large number of components are unloaded
setTimeout(() => {
this.notifyWatch([namePath]);
})
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

延迟方式仍存 O(N²) 隐患,建议合并为单次去抖触发

当前实现在每个 Field 卸载时都 setTimeout 一次,notifyWatch 仍会被调用 n 次;notifyWatch → getFieldsValue 会遍历全部剩余字段,整体复杂度依旧可能退化为 O(n²)。可以考虑为整个卸载阶段只触发一次异步通知,例如增加一个“待刷”标记并在下一个宏/微任务中统一执行:

+// 顶部成员
+private pendingNotify: boolean = false;

@@
-      // Avoid exponential loops when a large number of components are unloaded
-      setTimeout(() => {
-        this.notifyWatch([namePath]);
-      })
+      // 避免多次重复调用,统一去抖
+      if (!this.pendingNotify) {
+        this.pendingNotify = true;
+        Promise.resolve().then(() => {
+          this.pendingNotify = false;
+          this.notifyWatch();      // 合并调用,不再逐个 namePath
+        });
+      }

这样无论一次批量卸载多少 Field,只会有一次 notifyWatch 调用,可彻底消除指数级循环风险。
[建议级别: 必需优化]

🤖 Prompt for AI Agents
In src/useForm.ts around lines 685 to 689, the current implementation calls
setTimeout for each Field unload, causing notifyWatch to be invoked multiple
times and leading to O(n²) complexity. To fix this, implement a debounce
mechanism by adding a flag to mark pending notifications and schedule a single
notifyWatch call in the next macro or microtask, ensuring that no matter how
many Fields unload in a batch, notifyWatch is only called once, eliminating the
exponential loop risk.

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: 0

🧹 Nitpick comments (2)
tests/useWatch.test.tsx (2)

123-128: 建议改用微任务冲刷或 waitFor 以提升测试稳定性

此处通过 timeout() 触发一个宏任务,再包裹在 act 中等待状态更新。内部实现只是 Promise.resolve().then()(微任务)推迟了 notifyWatch,宏任务等待不仅多余且会放慢整体测试速度,也可能在某些环境下导致间歇性超时。

-      await act(async () => {
-        await timeout();
-      });
+      await act(async () => {
+        // 仅需冲刷微任务即可等待 notifyWatch 调度完成
+        await Promise.resolve();
+      });

或使用 RTL 的 waitFor

await waitFor(() =>
  expect(container.querySelector<HTMLDivElement>('.values')?.textContent).toEqual('')
);

这样既缩短测试时间,又避免依赖固定延时造成的脆弱性。


165-170: 重复代码可抽取为公共辅助函数

与上一段相同的延时等待逻辑再次出现,建议提取为如 await flushNotify(); 的帮助方法,以减少重复并保持两处行为一致。

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 00b31b5 and ee1196c.

📒 Files selected for processing (2)
  • src/useForm.ts (1 hunks)
  • tests/useWatch.test.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/useForm.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
tests/useWatch.test.tsx (1)
tests/common/timeout.ts (1)
  • timeout (3-7)

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.

1 participant