Skip to content

Conversation

@aojunhao123
Copy link
Contributor

@aojunhao123 aojunhao123 commented Dec 23, 2025

Summary by CodeRabbit

发行说明

  • Bug 修复

    • 修复了 Escape 键处理在组件卸载时的堆栈清理问题,确保过期数据不会被保留。
  • 测试

    • 增加了覆盖组件卸载后堆栈清理行为的单元测试,提升了对该场景的回归保护。

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 23, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

导出并暴露了 useEscKeyDown 模块内的 stack 变量,并在其 useEffect 清理路径中显式移除当前 id;同时在测试中引用并新增用例验证组件卸载时 stack 被清空。

Changes

内聚 / 文件(s) 变更摘要
Escape 钩子导出与清理
src/useEscKeyDown.ts
将内部 stack 从私有改为导出(export let stack: string[] = []),在 effect 的清理路径中添加 stack = stack.filter(item => item !== id),确保卸载/清理时移除该 id。
测试:卸载时清理验证
tests/index.test.tsx
导入 stack 并新增测试用例 "should clear stack when unmount":渲染 Portal 后断言 stack.length === 1,卸载后断言 stack.length === 0

Sequence Diagram(s)

(本次变更为导出变量与清理/测试验证,未引入跨多个组件的新控制流,故省略序列图。)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • zombieJ

Poem

🐰 我是小兔来报到,
栈里旧影一并扫,
卸载一行来清理,
测试验证不绕道。
啾啾跳跃庆新改! 🥕✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main change: fixing the stack reset behavior when a component unmounts, which aligns with the core objective of preventing memory leaks.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a 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

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

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @aojunhao123, 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!

This pull request resolves an issue where components utilizing the useEscKeyDown hook were not properly deregistering themselves from an internal stack upon unmounting. The change introduces a cleanup mechanism that filters out the component's unique identifier from the stack, thereby maintaining an accurate list of active listeners for the Escape keydown event and improving resource management.

Highlights

  • Stack Management: Ensures that the component's ID is removed from the internal stack when the useEscKeyDown hook's cleanup function runs, preventing stale references and potential memory leaks.

🧠 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.

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.

@codecov
Copy link

codecov bot commented Dec 23, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.24%. Comparing base (623edd7) to head (405ef22).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master      #31      +/-   ##
==========================================
+ Coverage   94.20%   94.24%   +0.04%     
==========================================
  Files           7        7              
  Lines         138      139       +1     
  Branches       48       48              
==========================================
+ Hits          130      131       +1     
  Misses          8        8              

☔ 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
Contributor

@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

This pull request fixes a bug where a component's ID was not removed from the global stack on unmount. The change correctly addresses this by adding cleanup logic to the useEffect hook. However, this introduces code duplication, as similar logic exists in a useMemo hook. My review includes a comment pointing out this duplication and suggesting a refactor to improve maintainability by consolidating the related logic into a single useEffect hook, which is the standard practice for handling side effects in React.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/useEscKeyDown.ts (1)

18-24: 应使用 useEffect 而非 useMemo 来执行副作用。

useMemo 中修改全局 stack 违反了 React 的规则。useMemo 设计用于记忆计算结果,不应包含副作用。在渲染期间修改外部状态会导致以下问题:

  • 并发渲染问题:React 18+ 的并发特性可能会中断和重新开始渲染,导致 stack 状态不一致
  • StrictMode 兼容性:开发模式下渲染可能执行两次,可能产生意外的副作用
  • 可维护性:违反 React 最佳实践使代码更难理解和维护
🔎 建议的重构方案

将 stack 管理逻辑移至 useEffect 中:

-  useMemo(() => {
+  useEffect(() => {
     if (open && !stack.includes(id)) {
       stack.push(id);
-    } else if (!open) {
-      stack = stack.filter(item => item !== id);
     }
+    return () => {
+      stack = stack.filter(item => item !== id);
+    };
   }, [open, id]);

这样可以:

  1. 遵循 React 的副作用处理规范
  2. 确保清理逻辑在正确的时机执行
  3. 提高代码在并发模式下的可靠性

注意:这将与现有的 useEffect(第 26-37 行)中的清理逻辑协同工作。

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 623edd7 and 04188f5.

📒 Files selected for processing (1)
  • src/useEscKeyDown.ts
🔇 Additional comments (1)
src/useEscKeyDown.ts (1)

34-34: 修复有效,成功解决了内存泄漏问题。

useEffect 清理函数中添加 stack 过滤逻辑正确修复了以下场景:

  • 当组件在 open=true 状态下卸载时,确保 id 从全局 stack 中移除
  • 防止 stack 无限增长导致的内存泄漏

虽然这与 useMemo 中的清理存在部分冗余(当 open 变为 false 时),但这种防御性编程是合理的,确保了所有卸载场景都能正确清理。

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a memory leak in the Escape key handling functionality where the internal stack of portal IDs was not being cleaned up when a Portal component unmounted while in an open state.

Key Changes:

  • Added stack cleanup logic in the useEffect cleanup function to remove the portal's ID when the component unmounts

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

window.addEventListener('keydown', handleEscKeyDown);

return () => {
stack = stack.filter(item => item !== id);
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

The stack cleanup on component unmount lacks test coverage. Consider adding a test that verifies the stack is properly cleaned up when a Portal component unmounts while still open (not just when open changes to false). This would ensure the fix works correctly and prevent regressions.

Copilot uses AI. Check for mistakes.
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)
src/useEscKeyDown.ts (1)

18-37: 逻辑重复且存在 React 反模式。

当前实现存在以下问题:

  1. 逻辑重复:栈过滤操作出现在两处(第 22 行的 useMemo 和第 34 行的 useEffect 清理函数),导致维护困难。

  2. React 反模式useMemo 被用于执行副作用(修改模块级 stack),这违反了 React 的设计原则。useMemo 应该用于缓存计算值,而副作用应该放在 useEffect 中。

这些问题在先前的评审中已被提及但未得到解决。

🔎 建议的修复方案

将所有栈管理逻辑合并到单个 useEffect 中:

-  useMemo(() => {
-    if (open && !stack.includes(id)) {
-      stack.push(id);
-    } else if (!open) {
-      stack = stack.filter(item => item !== id);
-    }
-  }, [open, id]);
-
   useEffect(() => {
-    if (!open) {
-      return;
-    }
-
-    window.addEventListener('keydown', handleEscKeyDown);
+    if (open) {
+      // 添加到栈
+      if (!stack.includes(id)) {
+        stack.push(id);
+      }
+      
+      // 注册事件监听器
+      window.addEventListener('keydown', handleEscKeyDown);
+    }
 
     return () => {
+      // 清理:从栈中移除并移除事件监听器
       stack = stack.filter(item => item !== id);
       window.removeEventListener('keydown', handleEscKeyDown);
     };
   }, [open, id]);

这样可以:

  • 消除逻辑重复
  • 遵循 React 最佳实践
  • 使组件生命周期效果更易理解
  • 确保栈的添加和移除逻辑集中在一处

基于先前评审中的反馈。

🧹 Nitpick comments (1)
src/useEscKeyDown.ts (1)

6-6: 导出可变模块状态存在风险。

使用 export let 导出可变的模块级状态会带来以下问题:

  • 创建了与测试代码的紧耦合
  • API 语义不清晰(是公共 API 还是仅用于测试?)
  • 可能被外部代码意外修改

建议考虑以下替代方案:

🔎 推荐的替代方案

方案 1:提供测试专用的辅助函数

-export let stack: string[] = []; // export for testing
+let stack: string[] = [];
+
+// 仅用于测试
+export const __TEST__ = {
+  getStack: () => stack,
+  resetStack: () => { stack = []; }
+};

方案 2:通过自定义事件通知栈变化

在测试中监听事件而不是直接访问内部状态。

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04188f5 and 405ef22.

📒 Files selected for processing (2)
  • src/useEscKeyDown.ts
  • tests/index.test.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
tests/index.test.tsx (1)
src/useEscKeyDown.ts (1)
  • stack (6-6)
🔇 Additional comments (1)
tests/index.test.tsx (1)

360-370: 测试逻辑正确。

新增的测试用例有效验证了组件卸载时 stack 的清理行为,解决了先前评审中提到的测试覆盖不足的问题。

import { render, fireEvent } from '@testing-library/react';
import React from 'react';
import Portal from '../src';
import { stack } from '../src/useEscKeyDown';
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

考虑测试隔离问题。

直接导入内部实现的 stack 用于测试会造成测试与实现的紧耦合。更重要的是,由于 stack 是模块级别的可变状态,如果没有在测试之间正确重置,可能会导致测试相互影响。

建议在 beforeEach 中添加清理逻辑:

beforeEach(() => {
  global.isOverflow = true;
  stack.length = 0; // 清空 stack 以确保测试隔离
});
🤖 Prompt for AI Agents
In tests/index.test.tsx around line 4, the test currently imports the internal
mutable "stack" which tightly couples tests to implementation and can leak state
between tests; add a beforeEach block that resets global.isOverflow to true and
clears the stack (set its length to 0) to ensure test isolation, and preferably
update tests to use the public API rather than importing the internal stack
directly.

@zombieJ zombieJ merged commit 50da72b into react-component:master Dec 23, 2025
8 checks passed
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