Skip to content

Conversation

@aojunhao123
Copy link
Contributor

@aojunhao123 aojunhao123 commented Dec 27, 2025

Summary by CodeRabbit

发布说明

  • Bug 修复

    • 改进了 Escape 键事件处理的栈管理:在 open 为 true 时确保挂载时将 id 入栈,提升严格模式与嵌套门户场景下顶层识别的稳定性与触发准确性。
  • 测试

    • 新增覆盖:严格模式下顶层识别测试与多层嵌套门户触发顺序测试,验证回调接收的 top 标识与调用顺序。
  • 兼容性

    • 对外接口未作变更。

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

@coderabbitai
Copy link

coderabbitai bot commented Dec 27, 2025

Walkthrough

useEscKeyDown 的 effect 中,新增在每次渲染且 opentrue 且 id 未在共享栈中时将 id push 到栈的逻辑;清理与移除逻辑未变。新增两条测试,覆盖 StrictMode 与嵌套 portal 的 onEsc 行为。

Changes

Cohort / File(s) 变更摘要
Escape 键栈插入逻辑
src/useEscKeyDown.ts
在 effect 生命周期中补充:当 open === true 且 id 尚未存在于共享栈时,将 id push 到栈。未改动清理(过滤移除 id)或 keydown 监听的移除逻辑。
测试覆盖(StrictMode 与嵌套)
tests/index.test.tsx
新增两条用例:验证在 React.StrictMode 下首个挂载的 portal 被识别为栈顶并触发 onEsc(top: true);验证三层嵌套 portal 在按 Escape 时按预期顺序触发 onEsctop 标志。

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 分钟

Possibly related PRs

Suggested reviewers

  • zombieJ

Poem

🐰 我在草堆间数列队,
小小 id 轻轻入栈来,
StrictMode 下先登场,嵌套里排队,
一按 Escape 风轻吹,
叽喳声里程序安好。

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 标题清晰地反映了主要变更,即处理 StrictMode 中的 esc 栈清理问题。
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 addresses and resolves an issue where the useEscKeyDown hook, which manages escape key handlers for components like Portals, exhibited incorrect behavior when used within React's StrictMode. The implemented changes ensure that components maintain their proper order within the internal escape key stack, even when StrictMode causes components to unmount and re-mount, thereby guaranteeing that the correct component consistently responds to the Escape key press.

Highlights

  • StrictMode Compatibility: The useEscKeyDown hook now correctly handles React's StrictMode by preserving the order of components in the internal escape key handler stack. This prevents issues where components might lose their 'topmost' status due to StrictMode's double-rendering behavior.
  • Index Tracking with useRef: A useRef hook has been introduced to store the last known index of a component's ID within the global escape key stack. This allows the component to be re-inserted at its original position when re-mounted by StrictMode.
  • Improved Stack Management Logic: The logic for adding and removing component IDs from the escape key stack has been refined to utilize the stored index, ensuring that components are correctly positioned in the stack even after StrictMode's re-renders, and preventing duplicate entries.
  • New Test for StrictMode Behavior: A dedicated test case has been added to verify that the onEsc callback correctly identifies the topmost portal when rendered within React.StrictMode, confirming the effectiveness of the fix.

🧠 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 27, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.36%. Comparing base (cb1d29b) to head (4c8899a).

Additional details and impacted files
@@            Coverage Diff             @@
##           master      #33      +/-   ##
==========================================
+ Coverage   94.28%   94.36%   +0.08%     
==========================================
  Files           7        7              
  Lines         140      142       +2     
  Branches       48       49       +1     
==========================================
+ Hits          132      134       +2     
  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 addresses an issue where the escape key handling stack was not being correctly managed in React's StrictMode, which could lead to incorrect onEsc behavior. The fix involves using a useRef to cache the portal's index in the stack and restoring it during the useEffect hook if it was removed by StrictMode's mount-unmount-remount cycle. A new test case is added to verify this behavior in StrictMode.

The fix appears correct and effectively solves the problem. My only concern is the continued use of useMemo for side effects, which is an anti-pattern in React. While changing this would require a larger refactoring, it's a point of fragility in the current implementation. Overall, the change is a good, targeted fix.

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)

19-26: 严重问题:useMemo 中存在副作用

useMemo 中直接修改模块级 stack 变量违反了 React 的规则。useMemo 仅应用于计算和记忆值,不应包含副作用。React 可能会在任意时刻调用 useMemo,或者跳过调用,这会导致不可预测的行为。

根据 React 文档,副作用应该放在 useEffectuseLayoutEffect 中。当前实现可能在某些边缘情况下失败,特别是在并发渲染模式下。

🔎 建议的修复方案

将栈管理逻辑移到 useEffect 中,并使用 useLayoutEffect 来确保同步执行:

-  useMemo(() => {
-    if (open && !stack.includes(id)) {
-      stack.push(id);
-    } else if (!open) {
-      stack = stack.filter(item => item !== id);
-    }
-    indexRef.current = stack.indexOf(id);
-  }, [open, id]);
-
   useEffect(() => {
     if (!open) {
+      stack = stack.filter(item => item !== id);
+      indexRef.current = -1;
       return;
     }
+    
     if (!stack.includes(id)) {
       const index = indexRef.current;
       const safeIndex =
         index < 0 ? stack.length : Math.min(index, stack.length);
       stack.splice(safeIndex, 0, id);
+    } else {
+      indexRef.current = stack.indexOf(id);
     }
 
     window.addEventListener('keydown', handleEscKeyDown);
 
     return () => {
       stack = stack.filter(item => item !== id);
       window.removeEventListener('keydown', handleEscKeyDown);
     };
   }, [open, id]);

注意:这个修复方案将所有栈操作统一到 useEffect 中,避免了 useMemo 的副作用问题。

🧹 Nitpick comments (1)
tests/index.test.tsx (1)

385-402: 建议增强测试覆盖率

当前测试仅验证了在 StrictMode 下首次挂载 Portal 时的场景。为了确保 StrictMode 下的栈清理逻辑完全正确,建议添加以下测试场景:

  1. 测试在 StrictMode 下切换 visible 属性(从 true 到 false 再到 true)
  2. 测试在 StrictMode 下多个 Portal 的挂载和卸载顺序
  3. 验证组件卸载后栈是否正确清空
🔎 建议的额外测试用例
+    it('onEsc should handle portal toggle in StrictMode', () => {
+      const onEsc = jest.fn();
+    
+      const Demo = ({ visible }: { visible: boolean }) =>
+        visible ? (
+          <Portal open onEsc={onEsc}>
+            <div />
+          </Portal>
+        ) : null;
+    
+      const { rerender } = render(<Demo visible />, { wrapper: React.StrictMode });
+      expect(stack).toHaveLength(1);
+    
+      rerender(<Demo visible={false} />);
+      expect(stack).toHaveLength(0);
+    
+      rerender(<Demo visible />);
+      expect(stack).toHaveLength(1);
+    
+      fireEvent.keyDown(window, { key: 'Escape' });
+      expect(onEsc).toHaveBeenCalledWith(expect.objectContaining({ top: true }));
+    });
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb1d29b and a6667f4.

📒 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)
src/useEscKeyDown.ts (1)

32-37: 在 StrictMode 下的栈管理逻辑已正确实现,无需修复

经验证,代码在 StrictMode 下的行为已通过测试用例 onEsc should treat first mounted portal as top in StrictMode(第 385-402 行)验证,该测试正确验证了以下场景:

  • Portal 在 StrictMode 中正确挂载并保持栈长度为 1
  • onEsc 回调正确接收 top: true

Git 历史记录显示存在针对 "handle StrictMode esc stack cleanup" 的提交,说明这一逻辑是有意设计的。虽然代码逻辑复杂,但 safeIndex 的计算和 indexRef 的使用方式能够正确处理所有场景,包括 StrictMode 的双重挂载流程。

Comment on lines 32 to 37
if (!stack.includes(id)) {
const index = indexRef.current;
const safeIndex =
index < 0 ? stack.length : Math.min(index, stack.length);
stack.splice(safeIndex, 0, id);
}
Copy link
Contributor Author

@aojunhao123 aojunhao123 Dec 28, 2025

Choose a reason for hiding this comment

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

这里用回 orderContext 的话代码似乎更简洁,可读性也会好一些

Comment on lines +404 to +426
it('nested portals should trigger in correct order', () => {
const onEsc = jest.fn();
const onEsc2 = jest.fn();
const onEsc3 = jest.fn();

render(
<Portal open onEsc={onEsc}>
<div />
<Portal open onEsc={onEsc2}>
<div />
<Portal open onEsc={onEsc3}>
<div />
</Portal>
</Portal>
</Portal>
);

fireEvent.keyDown(window, { key: 'Escape' });

expect(onEsc).toHaveBeenCalledWith(expect.objectContaining({ top: false }));
expect(onEsc2).toHaveBeenCalledWith(expect.objectContaining({ top: false }));
expect(onEsc3).toHaveBeenCalledWith(expect.objectContaining({ top: true }));
});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

顺带补了一下嵌套Portal下的case

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

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: 不应在 useMemo 中执行副作用操作

useMemo 中直接修改共享的 stack 状态是一个反模式:

  • useMemo 应该是纯函数,用于计算开销较大的值,而不是用于副作用
  • React 可能会在任何时候丢弃或重新计算 memoized 值,执行时机不可预测
  • 现在 useEffect 中也有相同的栈管理逻辑(27-28 行),造成逻辑重复且难以维护
  • 在 StrictMode 下,useMemouseEffect 的执行次数不同,可能导致不一致
🔎 建议的重构方案

将所有栈管理逻辑整合到 useEffect 中,移除 useMemo 中的副作用:

-  useMemo(() => {
-    if (open && !stack.includes(id)) {
-      stack.push(id);
-    } else if (!open) {
-      stack = stack.filter(item => item !== id);
-    }
-  }, [open, id]);
-
   useEffect(() => {
     if (open && !stack.includes(id)) {
       stack.push(id);
     } else if (!open) {
       return;
     }
 
     window.addEventListener('keydown', handleEscKeyDown);
 
     return () => {
       stack = stack.filter(item => item !== id);
       window.removeEventListener('keydown', handleEscKeyDown);
     };
   }, [open, id]);

这样可以确保栈管理逻辑只在一个地方维护,并且符合 React 的最佳实践。

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 831863b and 4c8899a.

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

Comment on lines 27 to 29
if (open && !stack.includes(id)) {
stack.push(id);
} else if (!open) {
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

栈管理逻辑重复

这段代码与 useMemo 中的逻辑(19-20 行)完全重复。虽然在 useEffect 中处理副作用是正确的做法,但现在同样的逻辑存在于两个地方,违反了 DRY 原则。

建议:将所有栈管理逻辑整合到 useEffect 中,移除 useMemo 中的副作用操作(见上一条评论的建议)。

🤖 Prompt for AI Agents
In src/useEscKeyDown.ts around lines 19-20 and 27-29, the stack push/remove
logic is duplicated between the useMemo and the useEffect; remove side effects
from the useMemo so it remains pure and consolidate all stack management (push
when open becomes true and id not present, and removal on close/unmount) inside
the useEffect cleanup and effect body; ensure the effect depends on open/id and
correctly adds the id when open and removes it when open becomes false or on
unmount, and delete the duplicated code in useMemo.

@zombieJ zombieJ merged commit b39bb9a into react-component:master Dec 29, 2025
12 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