Skip to content

Conversation

muzea
Copy link

@muzea muzea commented Nov 13, 2022

fix ant-design/ant-design#34182

Summary by CodeRabbit

  • 重构

    • 多处组件与钩子将内部回调与渲染逻辑 memo 化/稳定化,减少不必要重渲染与子元素重建,用户可感知性能更稳定,行为不变。
  • 测试

    • 多项异步、滚动与触摸测试改为在 React 的 act() 中执行,新增复杂触摸场景并统一时序,测试更稳定。
  • 文档

    • 添加注释提示列表引用不变但内部数据变化时的潜在差异。
  • 变更

    • 精简配置方式:移除旧的配置接口,改为直接传入 key 提取器,可能影响依赖内部类型的 TypeScript 使用。

@vercel
Copy link

vercel bot commented Nov 13, 2022

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

Project Deployment Preview Comments Updated (UTC)
virtual-list Ready Ready Preview Comment Sep 30, 2025 8:00am

@afc163
Copy link
Member

afc163 commented Dec 27, 2022

冲突了

@codecov
Copy link

codecov bot commented Dec 31, 2022

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.86%. Comparing base (92f399f) to head (3fa5f31).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #190   +/-   ##
=======================================
  Coverage   97.86%   97.86%           
=======================================
  Files          19       19           
  Lines         796      797    +1     
  Branches      193      193           
=======================================
+ Hits          779      780    +1     
  Misses         17       17           

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

@muzea
Copy link
Author

muzea commented Jan 15, 2023

/ping @afc163 @zombieJ 这个 PR 上还有啥欠缺的东西吗?

@XianZhengquan
Copy link

@afc163 大佬,为啥这个还没有合并呢?都几年了,这个问题很严重啊

@afc163
Copy link
Member

afc163 commented Aug 18, 2025

@muzea @XianZhengquan 冲突了

@XianZhengquan
Copy link

@muzea @XianZhengquan 冲突了

@muzea 大佬,瞅一瞅呀

@Copilot Copilot AI review requested due to automatic review settings August 19, 2025 14:36
@muzea muzea force-pushed the hotfix/avoid-unnecessary-rerender branch from b505549 to 6b1ebb8 Compare August 19, 2025 14:36
Copy link

coderabbitai bot commented Aug 19, 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

在若干组件与钩子中使用 useMemo/useCallback 稳定函数与对象引用,移除 SharedConfig 并直接传入 getKey;大量测试将 DOM 交互与定时器推进包入 React 的 act();对外 API 行为总体不变(≤50 字)。

Changes

Cohort / File(s) Summary of changes
Resize handling stabilization
src/Filler.tsx
将内联 onResize 替换为 useCallback 记忆的 handleResize;仅在 entry.offsetHeight 为真时调用 onInnerResize,仅改变回调身份稳定性。
Remove SharedConfig / List usage
src/List.tsx
删除局部 SharedConfig 包装对象,直接传递 getKeyuseChildren;调整 imports,公共 API 未变化。
Hooks: children rendering memoization
src/hooks/useChildren.tsx
React.useMemo 缓存生成的子元素数组,依赖包括 list, startIndex, endIndex, setNodeRef, renderFunc, getKey, offsetX, scrollWidth;签名改为直接接收 getKey: GetKey<T> 并更新导入。
Hooks: heights management callbacks memoized
src/hooks/useHeights.tsx
cancelRafcollectHeightsetInstanceRefReact.useCallback 包装并列出依赖;逻辑与返回值保持不变,新增注释说明关联性。
Type/interface removal
src/interface.ts
移除导出接口 SharedConfig<T>,保留 GetKey<T> 和其他类型别名;代码改为直接使用 GetKey
Tests: act() wrapping & syntax normalization
tests/*
tests/scroll-Firefox.test.js, tests/scroll.test.js, tests/scrollWidth.test.tsx, tests/touch.test.js, tests/props.test.js, ...
scrollTo、触摸事件、jest.runAllTimers() 等操作包入 React 的 act();调整 act 的导入来源/顺序(部分改用 @testing-library/react);统一 render-prop/ itemKey 箭头语法;新增与调整若干测试用例。
Misc tests import/order adjustments
tests/scroll-Firefox.test.js, other tests
调整 act 的导入位置与调用范围,替换部分测试引入(如从 react-dom/test-utils@testing-library/react),并将 DOM 操作移入 act()。

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as 用户
  participant Filler as Filler
  participant RO as ResizeObserver
  participant Parent as onInnerResize

  User->>Filler: 渲染组件
  Filler->>RO: observe(inner)
  RO-->>Filler: resize(entry)
  alt entry.offsetHeight > 0
    Filler->>Filler: handleResize (useCallback)
    Filler->>Parent: onInnerResize(offsetHeight)
  else offsetHeight == 0
    note right of Filler: 不调用 onInnerResize
  end
Loading
sequenceDiagram
  autonumber
  participant List as List
  participant useChildren as useChildren (useMemo)
  participant renderFn as renderFunc
  participant Item as Item

  List->>useChildren: 提供 list,startIndex,endIndex,getKey,offsetX,scrollWidth
  useChildren->>useChildren: useMemo 缓存 children[](依赖未变则复用)
  alt 依赖未变
    useChildren-->>List: 返回缓存 children[]
  else 依赖变化
    loop i in [startIndex, endIndex)
      useChildren->>renderFn: 调用 renderFunc(i, width, offsetX)
      renderFn-->>useChildren: React 元素
      useChildren->>Item: 包装并设 key = getKey(...)
    end
    useChildren-->>List: 返回新 children[]
  end
Loading
sequenceDiagram
  autonumber
  participant Hook as useHeights
  participant RAF as requestAnimationFrame
  participant Instances as instancesRef

  Hook->>Hook: cancelRaf (useCallback) -> 增加 promiseIdRef,使旧任务失效
  Hook->>Hook: collectHeight (useCallback) -> 计算累积 heights,视情况更新 updatedMark
  Hook->>Hook: setInstanceRef (useCallback) -> 更新 instancesRef,触发 collectHeight 与 onItemAdd/remove
  alt 高度变更
    Hook-->>Hook: bump updatedMark
  else 无变更
    Hook-->>Hook: 无改动
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

兔儿穿过钩与挂,
回调稳固不再抖,
子元素静候旧引用,
测试入 act 心安稳,
跳跃更新,快乐撒 🐇✨

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 该标题使用 perf 前缀并简洁表述了跳过不必要渲染以提升性能的核心意图。标题与本次提交在各组件中通过稳定回调和 memo 化避免重复渲染的主要改动保持一致,因此通过校验。
Linked Issues Check ✅ Passed 本次提交在 Filler、List、useChildren、useHeights 等模块中引入了稳定回调和 useMemo/useCallback 以减少不必要的渲染操作,有效降低了 titleRender 在展开/折叠时的重复刷新次数。改动直接对应 #34182 中提出的“减少不是必要的 titleRender 刷新”诉求,因此符合关联问题的目标。
Out of Scope Changes Check ✅ Passed 所有文件的修改都聚焦于性能优化和测试用例同步,没有引入与减少渲染次数目标无关的新功能或业务逻辑,因此不存在范围外改动。
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fa5f31 and 6a3ee26.

📒 Files selected for processing (1)
  • tests/touch.test.js (3 hunks)
🔇 Additional comments (5)
tests/touch.test.js (5)

67-95: 测试正确地使用了 act 包裹触摸事件序列

该测试用例将整个触摸事件序列(touchstart、touchmove、touchend)和定时器推进都包裹在 act() 中,确保与 React 的渲染生命周期同步,避免了 act 警告。实现正确。


97-129: 测试正确地使用了 act 包裹多步触摸手势

该测试用例正确地将包含多个 touchmove 事件的触摸序列和定时器推进都包裹在 act() 中,确保状态更新的同步。实现正确。


174-216: 正确使用 act 包裹边界场景的触摸事件

该测试用例正确地将两个独立的触摸事件序列分别包裹在 act() 中,测试了滚动到底部时 preventDefault 的调用行为。实现正确且完整。


219-230: LGTM - 正确包裹滚动条的触摸事件

该测试用例正确地将滚动条上的 touchstart 事件包裹在 act() 中,确保 preventDefault 行为的正确测试。


232-276: 嵌套触摸测试正确使用 Testing Library 和 async act

该测试用例使用 @testing-library/reactfireEvent API,它会自动将更新包裹在 act 中。异步 act 块(lines 259-262)正确地推进定时器并等待 Promise,确保嵌套 List 组件的触摸行为得到正确测试。实现符合 Testing Library 最佳实践。


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
🧪 Early access (Sonnet 4.5): enabled

We are currently testing the Sonnet 4.5 model, which is expected to improve code review quality. However, this model may lead to increased noise levels in the review comments. Please disable the early access features if the noise level causes any inconvenience.

Note:

  • Public repositories are always opted into early access features.
  • You can enable or disable early access features from the CodeRabbit UI or by updating the CodeRabbit configuration file.

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

@muzea
Copy link
Author

muzea commented Aug 19, 2025

最近事情比较多,我看看这几天 rebase 一下代码

Copy link

@Copilot 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 optimizes performance by preventing unnecessary re-renders through the addition of React memoization techniques. The changes wrap functions and objects in useCallback and useMemo hooks to maintain reference stability across renders.

  • Wraps functions in useCallback to prevent recreation on every render
  • Adds useMemo to objects and computed values to maintain reference equality
  • Includes a Chinese comment explaining potential behavioral differences

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/hooks/useHeights.tsx Converts functions to useCallback and adds explanatory comment
src/hooks/useChildren.tsx Wraps return value in useMemo with dependency array
src/List.tsx Memoizes sharedConfig object creation
src/Filler.tsx Reorders imports and wraps resize handler in useCallback

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

return cancelRaf;
}, []);

// 这里稍显迷惑性,当 heightsRef.current.set 被调用时,updatedMark 会变化,进而导致 heightsRef.current 也出现变化
Copy link
Preview

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

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

The comment is in Chinese. Consider translating to English for consistency with the rest of the codebase: // This is somewhat confusing: when heightsRef.current.set is called, updatedMark changes, which causes heightsRef.current to also change

Suggested change
// 这里稍显迷惑性,当 heightsRef.current.set 被调用时,updatedMark 会变化,进而导致 heightsRef.current 也出现变化
// This is somewhat confusing: when heightsRef.current.set is called, updatedMark changes, which causes heightsRef.current to also change

Copilot uses AI. Check for mistakes.

},
offsetX,
}) as React.ReactElement;
// 可能存在 list 不变但是里面的数据存在变化的情况,会与之前写法存在不同的行为
Copy link
Preview

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

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

The comment is in Chinese. Consider translating to English for consistency: // There may be cases where the list doesn't change but the data inside it does, which will behave differently from the previous implementation

Suggested change
// 可能存在 list 不变但是里面的数据存在变化的情况,会与之前写法存在不同的行为
// There may be cases where the list reference does not change, but the data inside it does, which will behave differently from the previous implementation

Copilot uses AI. Check for mistakes.


useEffect(() => {
return cancelRaf;
}, []);
Copy link
Preview

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

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

The useEffect cleanup function should include cancelRaf in the dependency array since it's now a memoized callback. Change the dependency array from [] to [cancelRaf].

Suggested change
}, []);
}, [cancelRaf]);

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

🧹 Nitpick comments (6)
src/Filler.tsx (1)

72-79: 为 handleResize 标注类型,避免隐式 any;保持回调稳定性

当前解构参数未标注类型,在开启 noImplicitAny 时会报错。建议使用 rc-resize-observer 的类型来声明,既避免隐式 any,又与 onResize 的签名一致。

应用如下修改:

-import ResizeObserver from 'rc-resize-observer';
+import ResizeObserver from 'rc-resize-observer';
+import type { ResizeObserverProps } from 'rc-resize-observer';
-    const handleResize = React.useCallback(
-      ({ offsetHeight }) => {
+    const handleResize: ResizeObserverProps['onResize'] = React.useCallback(
+      ({ offsetHeight }) => {
         if (offsetHeight && onInnerResize) {
           onInnerResize();
         }
       },
       [onInnerResize],
     );

Also applies to: 84-85

src/hooks/useChildren.tsx (2)

15-16: 确认“list 引用不变但内部数据变化”的行为差异是否可接受

这里明确引入了记忆化,若外部在原地 mutate 数组(保持引用不变),则不会重新计算子元素,可能导致 UI 不更新。请确认库的契约是否要求数据不可变更新(推荐做法)。若需兼容原地变更,建议增加一个外部可控的“数据版本号”或“无效化”信号参与依赖。

如需,我可以起草一个 dataVersion(或 invalidate)的 prop 方案,并补充到 List 与 useChildren 的依赖中。


16-33: 记忆化计算本身 LGTM,但可补充 list.length 捕获原地增删场景

当前依赖包含 list 本身,但无法捕获“同引用但长度变化”的极端情况。增加 list.length 作为低成本的折中方案,可覆盖 push/pop 等原地操作(仍无法捕获仅内容变更但长度不变的情况)。

-  }, [list, startIndex, endIndex, setNodeRef, renderFunc, getKey, offsetX, scrollWidth]);
+  }, [list, list.length, startIndex, endIndex, setNodeRef, renderFunc, getKey, offsetX, scrollWidth]);
src/hooks/useHeights.tsx (3)

31-71: 微调:避免重复自增 promiseIdRef

collectHeight 一开始已调用 cancelRaf() 自增了 promiseIdRef;异步分支中再次自增会造成语义上的冗余(功能上无害)。建议删除第二次自增,提升可读性。

-      promiseIdRef.current += 1;
       const id = promiseIdRef.current;
       Promise.resolve().then(() => {
         if (id === promiseIdRef.current) {
           doCollect();
         }
       });

72-92: setInstanceRef 记忆化良好;布尔差异判断可读性可提升

当前用法 !origin !== !instance 属于“真假异或”,可读性略差。建议改为显式布尔转换对比。

-    // Instance changed
-    if (!origin !== !instance) {
+    // Instance changed
+    if (Boolean(origin) !== Boolean(instance)) {
       if (instance) {
         onItemAdd?.(item);
       } else {
         onItemRemove?.(item);
       }
     }

93-96: useEffect 依赖建议包含 cancelRaf 以满足 exhaustive-deps

虽然 cancelRaf 已稳定,但加入依赖可避免 ESLint 提示。

-  useEffect(() => {
-    return cancelRaf;
-  }, []);
+  useEffect(() => {
+    return cancelRaf;
+  }, [cancelRaf]);
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5769845 and 6b1ebb8.

📒 Files selected for processing (4)
  • src/Filler.tsx (2 hunks)
  • src/List.tsx (1 hunks)
  • src/hooks/useChildren.tsx (1 hunks)
  • src/hooks/useHeights.tsx (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/List.tsx (1)
src/interface.ts (1)
  • SharedConfig (7-9)
src/hooks/useChildren.tsx (1)
src/Item.tsx (1)
  • Item (8-16)
🔇 Additional comments (4)
src/Filler.tsx (1)

2-3: 引入 rc-resize-observer 没问题

依赖与用法与全局保持一致。

src/hooks/useHeights.tsx (2)

27-30: 将 cancelRaf 记忆化以稳定引用:LGTM

命名 + 空依赖的 useCallback 简洁明了,有助于下游依赖稳定。


97-99: 返回 heightsRef.current 的模式保持不变:LGTM

通过 updatedMark 驱动外部重新计算,高度缓存实例保持稳定引用,符合现有用法。

src/List.tsx (1)

168-173: sharedConfig 使用 useMemo 稳定引用:LGTM

以 getKey 为依赖可避免每次 render 产生新对象,降低子树无谓重渲染,符合本 PR 的目标。

src/List.tsx Outdated
return {
getKey,
};
}, [getKey]);
Copy link
Member

Choose a reason for hiding this comment

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

这个 sharedConfig 里只有一个 getKey,不如把参数直接拍平,不要这层 sharedConfig 了。

Copy link
Member

Choose a reason for hiding this comment

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

这个没改?

@afc163
Copy link
Member

afc163 commented Aug 19, 2025

@zombieJ 也看看

@XianZhengquan
Copy link

最近事情比较多,我看看这几天 rebase 一下代码

感谢大佬 🙏

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/scrollWidth.test.tsx (1)

233-235: 如后续 scrollTo 存在异步链路,可考虑切换为 await act

当前实现是同步断言,没问题;若未来 scrollTo 内部通过计时器/raf 异步更新,建议切换为 await act(async () => …) 并在内部推进计时器,减少潜在偶现。

Also applies to: 238-240

tests/scroll.test.js (1)

98-103: 建议调整 act 中的语句顺序:先触发 scrollTo 再推进计时器

当前先 runAllTimers 再 scrollTo,若 scrollTo 内部存在异步(如 raf/setTimeout),会遗漏本次 scrollTo 产生的任务,存在偶发不稳定风险。建议交换顺序:

@@
-    act(() => {
-      jest.runAllTimers();
-
-      listRef.current.scrollTo(null);
-    });
+    act(() => {
+      listRef.current.scrollTo(null);
+      jest.runAllTimers();
+    });
@@
-    act(() => {
-      jest.runAllTimers();
-
-      listRef.current.scrollTo(null);
-    });
+    act(() => {
+      listRef.current.scrollTo(null);
+      jest.runAllTimers();
+    });

Also applies to: 400-405

📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6b1ebb8 and c973f19.

📒 Files selected for processing (4)
  • tests/scroll-Firefox.test.js (2 hunks)
  • tests/scroll.test.js (5 hunks)
  • tests/scrollWidth.test.tsx (1 hunks)
  • tests/touch.test.js (3 hunks)
🔇 Additional comments (10)
tests/scrollWidth.test.tsx (1)

233-235: 将 ref.scrollTo 调用包裹到 act 中,行为更稳健

把同步的 scrollTo 调用放入 act,符合 React 对受控更新的测试约定,避免 “not wrapped in act(...)” 的告警。断言读取 getScrollInfo 也是同步的,改动合理。

Also applies to: 238-240

tests/scroll-Firefox.test.js (2)

1-1: 统一使用 @testing-library/react 的 act,方向正确

改为从 RTL 引入 act 与本仓库其余测试保持一致,避免跨包 act 实例不一致的问题。


127-130: 滚动到底部与计时器推进放进 act,避免状态未刷新断言

将 scrollTo 与 jest.runAllTimers 放到同一个 act 中,有助于确保副作用在断言前已生效。改动到位。

tests/scroll.test.js (1)

114-117: 将 scrollTo 与计时器统一包裹到 act 中,符合测试最佳实践

这些位置的顺序与时机处理合理:先触发,再推进计时器,保证状态在断言前被刷新。改动 +1。

Also applies to: 135-137, 161-164

tests/touch.test.js (6)

74-91: 触摸序列整体包裹进单个 act,降低异步时序带来的不确定性

start/move/end 与计时器推进放在同一 act 中,能确保副作用在断言前完成。实现合理。


106-117: “不可滚动时调用 preventDefault” 场景的事件派发包裹在 act 中,合理

在 act 中构造与派发 touch 事件并注入 preventDefault mock,能稳定覆盖逻辑分支。


121-137: 重新起一轮触摸交互并在 act 内重置 mock,保证隔离性

在同一 act 中推进计时器、reset mock 并再次派发事件,保证前后两段交互不串扰。用法到位。


146-150: 容器 touchstart 包裹 act,确保同步副作用按期落地

轻量但必要的包裹,避免 React 对未包裹更新的告警。


155-170: 嵌套用例迁移到 RTL 并显式指定 itemKey,稳定性更好

使用 RTL 的 render + container 模式,并为外层 List 指定 itemKey,可避免 key 生成差异导致的不必要重渲染。合理的微调。


182-185: 在异步 act 中推进计时器,确保嵌套滚动副作用完成

advanceTimersByTime 放在 await act(async () => …) 中,能保证最终断言观察到稳定状态。改动得当。

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

🧹 Nitpick comments (1)
tests/props.test.js (1)

26-28: prefixCls 用例风格统一 OK;建议补充清理

当前 mount 后未显式卸载,长期会增加测试间耦合风险。可在断言后补一行 wrapper.unmount()

可以直接在该用例末尾追加:

   expect(wrapper.find('.prefix-holder-inner').length).toBeTruthy();
+  wrapper.unmount();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c973f19 and 0b0e344.

📒 Files selected for processing (1)
  • tests/props.test.js (3 hunks)
🔇 Additional comments (3)
tests/props.test.js (3)

2-2: 导入顺序微调 OK

将 React 提前导入无副作用,风格一致性更好。


14-16: itemKey 与断言写法统一化 OK

  • 单参箭头函数加括号、children 解构写法清晰。
  • key 断言覆盖两项数据,语义准确。

Also applies to: 19-21


37-42: offsetX 渲染函数用例 OK

  • 解构第 3 个参数拿到 offsetX 并断言为 0 的路径清晰。
  • 单参箭头函数风格已统一。

Comment on lines 48 to 72
it('no unnecessary re-render', () => {
const renderItem = sinon.fake(({ id, key }) => <div key={key}>{id}</div>);
const data = [{ id: 1, key: 1 }];
function Wrapper() {
const [state, setState] = React.useState(0);

React.useEffect(() => {
setState(1);
}, []);

return (
<div>
<h1>{state}</h1>
<List data={data} itemKey="key" prefixCls="prefix">
{renderItem}
</List>
</div>
);
}
const wrapper = mount(<Wrapper />);
expect(wrapper.find('h1').text()).toBe('1');
expect(renderItem.callCount).toBe(1);
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

修复:测试依赖未导入且存在异步更新竞争,导致用例不稳定

  • 未导入 sinon:本文件直接使用 sinon.fake,在大多数 CI(Jest + Enzyme)环境下会 ReferenceError。本仓库更推荐使用 jest.fn,避免额外依赖。
  • 未用 act()/wrapper.update() 刷新 useEffect 触发的异步 setState。直接在 mount 后断言 h11 具有竞态,测试可能偶现失败。

建议改为 Jest 原生 mock,并用 act 明确冲刷副作用;同时在用例结束时 unmount

@@
-it('no unnecessary re-render', () => {
-  const renderItem = sinon.fake(({ id, key }) => <div key={key}>{id}</div>);
+it('no unnecessary re-render', async () => {
+  const renderItem = jest.fn(({ id, key }) => <div key={key}>{id}</div>);
   const data = [{ id: 1, key: 1 }];
   function Wrapper() {
     const [state, setState] = React.useState(0);
     React.useEffect(() => {
       setState(1);
     }, []);
@@
-  const wrapper = mount(<Wrapper />);
-  expect(wrapper.find('h1').text()).toBe('1');
-  expect(renderItem.callCount).toBe(1);
+  const wrapper = mount(<Wrapper />);
+  // 冲刷 useEffect 导致的异步 setState
+  await act(async () => {
+    await new Promise(resolve => setTimeout(resolve, 0));
+  });
+  wrapper.update();
+  expect(wrapper.find('h1').text()).toBe('1');
+  expect(renderItem).toHaveBeenCalledTimes(1);
+  wrapper.unmount();
 });

并在文件顶部补充 act 导入(不与现有导入顺序冲突即可):

 import { mount } from 'enzyme';
 import React from 'react';
+import { act } from 'react-dom/test-utils';
 import List from '../src';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('no unnecessary re-render', () => {
const renderItem = sinon.fake(({ id, key }) => <div key={key}>{id}</div>);
const data = [{ id: 1, key: 1 }];
function Wrapper() {
const [state, setState] = React.useState(0);
React.useEffect(() => {
setState(1);
}, []);
return (
<div>
<h1>{state}</h1>
<List data={data} itemKey="key" prefixCls="prefix">
{renderItem}
</List>
</div>
);
}
const wrapper = mount(<Wrapper />);
expect(wrapper.find('h1').text()).toBe('1');
expect(renderItem.callCount).toBe(1);
});
// at the top of tests/props.test.js
import { mount } from 'enzyme';
import React from 'react';
import { act } from 'react-dom/test-utils';
import List from '../src';
it('no unnecessary re-render', async () => {
const renderItem = jest.fn(({ id, key }) => <div key={key}>{id}</div>);
const data = [{ id: 1, key: 1 }];
function Wrapper() {
const [state, setState] = React.useState(0);
React.useEffect(() => {
setState(1);
}, []);
return (
<div>
<h1>{state}</h1>
<List data={data} itemKey="key" prefixCls="prefix">
{renderItem}
</List>
</div>
);
}
const wrapper = mount(<Wrapper />);
// Flush the async setState from useEffect
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
});
wrapper.update();
expect(wrapper.find('h1').text()).toBe('1');
expect(renderItem).toHaveBeenCalledTimes(1);
wrapper.unmount();
});
🤖 Prompt for AI Agents
In tests/props.test.js around lines 48 to 70, the test uses sinon.fake (which is
not imported) and asserts state updated by useEffect without flushing effects,
causing ReferenceError and flaky timing; replace sinon.fake with jest.fn (remove
sinon dependency), add an import for act from react-dom/test-utils at the top,
wrap the mount and the async state flush in act (or call act(() => { wrapper =
mount(<Wrapper />); }) then call wrapper.update()), assert after update, and
finally call wrapper.unmount() to clean up; this ensures no missing imports and
eliminates the async race.

@muzea
Copy link
Author

muzea commented Aug 30, 2025

ping @zombieJ @afc163
家人们看看这个 PR 还缺什么吗?

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 (1)
tests/touch.test.js (1)

94-95: 建议在 afterEach 中统一清理

虽然手动调用 unmount() 不是错误,但建议将清理逻辑统一放在 afterEach 钩子中,以保持测试的一致性和避免遗漏。

+  let wrapper;
+
+  afterEach(() => {
+    if (wrapper) {
+      wrapper.unmount();
+      wrapper = null;
+    }
     jest.useRealTimers();
   });

   // 在每个测试中
-  const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100) });
+  wrapper = genList({ itemHeight: 20, height: 100, data: genData(100) });
   // ... 测试代码
-  wrapper.unmount();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9915070 and 778d42f.

📒 Files selected for processing (1)
  • tests/touch.test.js (3 hunks)
🔇 Additional comments (5)
tests/touch.test.js (5)

74-91: 改动看起来不错!

将触摸事件模拟和定时器推进包装在 act() 中是正确的做法,确保了 React 的状态更新和副作用正确同步。


97-129: 新增的 origin scroll 测试合理

测试验证了向下滑动(pageY 增加)时滚动位置保持为 0 的行为,这与向上滑动的测试形成了良好的对比。act() 包装确保了状态更新正确同步。


140-170: 正确使用 act() 包装异步操作

将触摸事件处理和 jest.runAllTimers() 包装在 act() 中是必要的,确保了 React 组件状态更新和定时器副作用的正确同步。


189-233: 迁移到 React Testing Library 的改动良好

从 enzyme 迁移到 React Testing Library 是正确的选择,RTL 提供了更符合用户行为的测试方式。使用 fireEventact 的组合确保了异步更新的正确处理。


191-191: 确认:tests 中 itemKey 用法与 getKey 实现一致
仓库中未找到 SharedConfig;src/List.tsx 接受 itemKey(string | function)、实现 getKey 并将其传入 useHeights、useDiffItem、useGetSize 等 hooks;tests 中使用 itemKey="id" 与实现匹配,可保持不变。

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

♻️ Duplicate comments (2)
src/hooks/useHeights.tsx (1)

93-96: useEffect 清理函数依赖应包含取消函数

既然取消函数已被 useCallback 包装,建议将其加入依赖数组以满足 exhaustive-deps 规则(若按上文重命名则替换为 cancelSchedule)。

-  useEffect(() => {
-    return cancelRaf;
-  }, []);
+  useEffect(() => {
+    return cancelSchedule;
+  }, [cancelSchedule]);
src/hooks/useChildren.tsx (1)

26-31: ref 回调可能捕获过期的 setRef(跨文件影响)

Item.tsx 中 useCallback([]) 会固定首次的 setRef,当这里传入的新回调变化时(例如切片或依赖变更后),旧的 ref 闭包可能被继续使用,导致卸载/替换阶段调用过期的回调。

请在 src/Item.tsx 将依赖改为 [setRef](保持函数签名不变):

 export function Item({ children, setRef }: ItemProps) {
-  const refFunc = React.useCallback(node => {
-    setRef(node);
-  }, []);
+  const refFunc = React.useCallback((node) => {
+    setRef(node);
+  }, [setRef]);

   return React.cloneElement(children, {
     ref: refFunc,
   });
 }
🧹 Nitpick comments (6)
src/hooks/useChildren.tsx (2)

15-16: useMemo 仅依赖 list 引用:原地修改 list 时可能跳过重算,导致子节点不更新

若上游存在“就地变更 list(不创建新引用)”的用法,titleRender 等基于 item 内容的渲染可能不刷新。两种出路:

  • 明确要求“不可变更新”并在文档注明;
  • 或提供一个可选的“版本号”/“外部依赖”参数以显式触发重算(兼容但更稳妥)。

参考补丁(新增可选 dataVersion 依赖,向后兼容;调用方在就地变更时递增它):

 export default function useChildren<T>(
   list: T[],
   startIndex: number,
   endIndex: number,
   scrollWidth: number,
   offsetX: number,
   setNodeRef: (item: T, element: HTMLElement | null) => void,
   renderFunc: RenderFunc<T>,
   getKey: GetKey<T>,
+  dataVersion?: number,
-) {
+): React.ReactElement[] {
   // ...
-  }, [list, startIndex, endIndex, setNodeRef, renderFunc, getKey, offsetX, scrollWidth]);
+  }, [list, startIndex, endIndex, setNodeRef, renderFunc, getKey, offsetX, scrollWidth, dataVersion]);
 }

Also applies to: 33-33


11-12: 类型不精确:setNodeRef 第二参应允许 null

ref 回调在卸载时会传入 null。当前签名为 HTMLElement 容易造成类型与实际不符。

建议改为:

-  setNodeRef: (item: T, element: HTMLElement) => void,
+  setNodeRef: (item: T, element: HTMLElement | null) => void,
src/hooks/useHeights.tsx (4)

27-30: 命名与实现不符:cancelRaf 实际取消的是 microtask 排程

为避免误导维护者,建议重命名为 cancelSchedule(或更贴切命名),并统一替换引用。

-  const cancelRaf = React.useCallback(function cancelRaf() {
+  const cancelSchedule = React.useCallback(function cancelSchedule() {
     promiseIdRef.current += 1;
   }, []);

并将后续 cancelRaf()/依赖项/cleanup 一并替换为 cancelSchedule


31-70: 从 rAF 改为 microtask:测量时机可能提前,需验证布局读取的准确性

在复杂样式或异步布局下,microtask 可能更早触发布局读取(offsetHeight + getComputedStyle),存在强制同步布局与抖动风险。请验证在 Expand/Collapse、滚动、窗口缩放等场景下高度是否稳定;如有问题,建议优先使用 rAF,无法使用时再回退到 microtask。

可选改造(轻量回退到 rAF):

-      promiseIdRef.current += 1;
-      const id = promiseIdRef.current;
-      Promise.resolve().then(() => {
-        if (id === promiseIdRef.current) {
-          doCollect();
-        }
-      });
+      promiseIdRef.current += 1;
+      const id = promiseIdRef.current;
+      const schedule =
+        typeof window !== 'undefined' && 'requestAnimationFrame' in window
+          ? (cb: () => void) => window.requestAnimationFrame(() => cb())
+          : (cb: () => void) => Promise.resolve().then(cb);
+      schedule(() => {
+        if (id === promiseIdRef.current) {
+          doCollect();
+        }
+      });

16-17: 类型不准确:instance 实际可能为 null

ref 回调在卸载时传入 null。建议将类型改为 HTMLElement | null,避免 TS 与运行时不一致。

-  setInstanceRef: (item: T, instance: HTMLElement) => void,
+  setInstanceRef: (item: T, instance: HTMLElement | null) => void,
-  const setInstanceRef = React.useCallback(function setInstanceRef(item: T, instance: HTMLElement) {
+  const setInstanceRef = React.useCallback(function setInstanceRef(item: T, instance: HTMLElement | null) {

Also applies to: 72-72


53-56: 注释与实现不一致

代码仅在 changed 为 true 时递增 updatedMark,注释里的 “Always trigger” 与之不符,建议修正文案以免误导。

-      // Always trigger update mark to tell parent that should re-calculate heights when resized
+      // Trigger update mark only when heights actually changed
-// This is somewhat confusing: when heightsRef.current.set is called, updatedMark changes, which in turn causes heightsRef.current to also change.
+// Note: when heightsRef.current.set writes a new value, updatedMark increases to notify parent; the CacheMap instance stays the same.

Also applies to: 97-97

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 778d42f and 1d636f8.

📒 Files selected for processing (2)
  • src/hooks/useChildren.tsx (2 hunks)
  • src/hooks/useHeights.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/hooks/useChildren.tsx (2)
src/interface.ts (1)
  • GetKey (7-7)
src/Item.tsx (1)
  • Item (8-16)
🔇 Additional comments (1)
src/hooks/useChildren.tsx (1)

2-2: 公共 API 变更 — getKey 现为 useChildren 的直接参数:确认调用点与发版策略

  • 仓库搜索结果:useChildren 定义于 src/hooks/useChildren.tsx(签名包含 getKey);调用点为 src/List.tsx;未发现 SharedConfig 引用。
  • 动作:确认 src/List.tsx 已按新签名传入 getKey;若对外行为变化,按语义化版本发布(至少 minor;破坏性改动为 major)。

@XianZhengquan
Copy link

@afc163 这是不是还是没过?覆盖率?
@muzea 大佬,看这个是落后master版本了

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 (1)
src/hooks/useChildren.tsx (1)

16-33: 确认数据不可变契约,避免 useMemo 因原地 mutation 而漏更新

useMemo 依赖包含 list 引用但无法感知“原地修改 list 内部项”的变化。若使用方不更换数组引用,仅对项字段做 mutation,将导致不重算子元素、渲染停滞。请确认库契约要求传入的 data 为不可变(更换引用)或在文档中明确;否则需引入额外依赖信号(如范围内 key 快照或版本号)来触发更新。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d636f8 and e82a20d.

📒 Files selected for processing (10)
  • src/Filler.tsx (2 hunks)
  • src/List.tsx (2 hunks)
  • src/hooks/useChildren.tsx (2 hunks)
  • src/hooks/useHeights.tsx (3 hunks)
  • src/interface.ts (0 hunks)
  • tests/props.test.js (3 hunks)
  • tests/scroll-Firefox.test.js (2 hunks)
  • tests/scroll.test.js (5 hunks)
  • tests/scrollWidth.test.tsx (1 hunks)
  • tests/touch.test.js (3 hunks)
💤 Files with no reviewable changes (1)
  • src/interface.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/scroll.test.js
  • tests/scroll-Firefox.test.js
  • tests/props.test.js
  • src/Filler.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/hooks/useChildren.tsx (2)
src/interface.ts (1)
  • GetKey (7-7)
src/Item.tsx (1)
  • Item (8-16)
🔇 Additional comments (11)
src/hooks/useHeights.tsx (1)

93-95: useEffect 清理依赖建议包含 cancelRaf

cancelRaf 已用 useCallback 包装,建议将依赖从 [] 调整为 [cancelRaf] 以符合惯例并利于未来重构;当前行为无差异。

-  useEffect(() => {
-    return cancelRaf;
-  }, []);
+  useEffect(() => {
+    return cancelRaf;
+  }, [cancelRaf]);
tests/scrollWidth.test.tsx (2)

233-236: 用 act 包裹 ref.scrollTo 调用,LGTM

正确地将同步更新包裹在 act 内,避免测试时机问题与告警。


238-241: 二次 scrollTo 包装一致,LGTM

同样的 act 包裹,保证断言前更新已冲刷完成。

src/List.tsx (2)

563-572: useChildren 按新签名直接传入 getKey,LGTM

参数顺序与新定义一致(list, start, end, scrollWidth, offsetX, setRef, render, getKey)。


21-21: 全库核验通过:无 SharedConfig 残留,useChildren 调用已更新 全库未发现 SharedConfig 类型或变量引用,所有 useChildren 调用(仅在 List.tsx)均与新签名一致。

tests/touch.test.js (5)

74-91: 将触摸交互与定时器推进放入 act,LGTM

能确保更新批处理完成后再断言,提升用例稳定性。


97-127: 新增“origin scroll”用例并使用 act 包裹,LGTM

覆盖向上滑动回弹为 0 的路径,行为更可控。


140-171: 在非可滚动场景下用 act 包裹并校验 preventDefault,LGTM

时序更严谨,断言更可靠。


180-185: 容器 preventDefault 用 act 包裹,LGTM

符合事件处理在测试中的最佳实践。


191-204: 嵌套用例切换至 RTL 风格并显式传入 itemKey,LGTM

更贴近实际使用路径,也与本 PR 的 API 调整保持一致。

src/hooks/useChildren.tsx (1)

26-31: 修复:Item.tsx 中 ref 回调捕获过期 setRef,需更新 useCallback 依赖

此处传入的是每次计算都会新建的内联 setRef。如果 src/Item.tsx 里的 refFunc 仍然是 useCallback([], ...),会捕获首次传入的 setRef,后续替换/卸载场景可能失效。请将 Item.tsx 的依赖改为 [setRef](此前已有同类评论)。

可在 src/Item.tsx 中修改如下(示例代码片段):

export function Item({ children, setRef }: ItemProps) {
  const refFunc = React.useCallback((node) => {
    setRef(node);
  }, [setRef]);

  return React.cloneElement(children, { ref: refFunc });
}

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

🧹 Nitpick comments (2)
tests/touch.test.js (2)

200-206: 可选:将 mockReset() 放到 act 外,拆分为两个独立的 act 以更清晰的时序

避免把纯测试操作与 React 更新批在一起,读写边界更清楚(行为不变)。

-  act(() => {
-    // ======= Not call since scroll to the bottom =======
-    jest.runAllTimers();
-    preventDefault.mockReset();
-    // start
-    const touchEvent2 = new Event('touchstart');
-    touchEvent2.touches = [{ pageY: 500 }];
-    getElement().dispatchEvent(touchEvent2);
-    // move
-    const moveEvent2 = new Event('touchmove');
-    moveEvent2.touches = [{ pageY: 0 }];
-    moveEvent2.preventDefault = preventDefault;
-    getElement().dispatchEvent(moveEvent2);
-  });
+  act(() => {
+    // ======= Not call since scroll to the bottom =======
+    jest.runAllTimers();
+  });
+  preventDefault.mockReset();
+  act(() => {
+    // start
+    const touchEvent2 = new Event('touchstart');
+    touchEvent2.touches = [{ pageY: 500 }];
+    getElement().dispatchEvent(touchEvent2);
+    // move
+    const moveEvent2 = new Event('touchmove');
+    moveEvent2.touches = [{ pageY: 0 }];
+    moveEvent2.preventDefault = preventDefault;
+    getElement().dispatchEvent(moveEvent2);
+  });

234-234: 确认 fireEvent 是否已自动包裹 act(依赖版本相关)

多数版本下 @testing-library/react 的 fireEvent 会自动在 act 中执行,但不同版本行为略有差异。若需要完全一致的风格,可显式用 act 包裹这两次 fireEvent 调用(纯样式调整,不影响断言)。

是否需要我基于当前仓库的依赖版本帮你确认 fireEvent 的 act 包裹行为,并给出是否显式包裹的建议?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e82a20d and 3fa5f31.

📒 Files selected for processing (1)
  • tests/touch.test.js (3 hunks)
🔇 Additional comments (5)
tests/touch.test.js (5)

74-92: 用 act 包裹整段触摸序列与定时器推进,方向正确

这能避免 “not wrapped in act(...)” 警告并提升稳定性。LGTM。


97-129: 第二个 “origin scroll” 场景同步到 act,保持一致性

事件与定时器均放入 act,逻辑清晰。LGTM。


183-194: 首段“不可滚动时调用 preventDefault”的交互已放入 act,OK

时序与上文一致。LGTM。


198-214: 第二段交互与定时器推进放入 act,OK

整体一致性良好。LGTM。


223-227: 容器 preventDefault 也用 act 包裹,保持一致性

能避免潜在的异步更新告警。LGTM。

Comment on lines 131 to 171
it('origin scroll', () => {
const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100) });

function getElement() {
return wrapper.find('.rc-virtual-list-holder').instance();
}

// start
const touchEvent = new Event('touchstart');
touchEvent.touches = [{ pageY: 100 }];
getElement().dispatchEvent(touchEvent);

// move
const moveEvent = new Event('touchmove');
moveEvent.touches = [{ pageY: 90 }];
getElement().dispatchEvent(moveEvent);
const moveEvent1 = new Event('touchmove');
moveEvent1.touches = [{ pageY: 110 }];
getElement().dispatchEvent(moveEvent1);

// move
const moveEvent2 = new Event('touchmove');
moveEvent2.touches = [{ pageY: 150 }];
getElement().dispatchEvent(moveEvent2);

// move
const moveEvent3 = new Event('touchmove');
moveEvent3.touches = [{ pageY: 20 }];
getElement().dispatchEvent(moveEvent3);

// move
const moveEvent4 = new Event('touchmove');
moveEvent4.touches = [{ pageY: 100 }];
getElement().dispatchEvent(moveEvent4);

// end
const endEvent = new Event('touchend');
getElement().dispatchEvent(endEvent);

// smooth
jest.runAllTimers();
expect(wrapper.find('ul').instance().scrollTop > 10).toBeTruthy();

expect(wrapper.find('ul').instance().scrollTop).toBe(0);
wrapper.unmount();
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

这里的触摸事件未包裹在 act 中,易产生告警与竞态

这段与前两段用例不一致,可能导致 “An update was not wrapped in act(...)” 警告或偶发现象。建议与上方一致,用 act 包裹整段事件与定时器推进;另外该测试名称与上一个相同,建议改为唯一名称,便于定位。

可按如下方式调整:

-it('origin scroll', () => {
+it('origin scroll - multi moves and bounce back', () => {
   const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100) });

   function getElement() {
     return wrapper.find('.rc-virtual-list-holder').instance();
   }

-  // start
-  const touchEvent = new Event('touchstart');
-  touchEvent.touches = [{ pageY: 100 }];
-  getElement().dispatchEvent(touchEvent);
-
-  // move
-  const moveEvent1 = new Event('touchmove');
-  moveEvent1.touches = [{ pageY: 110 }];
-  getElement().dispatchEvent(moveEvent1);
-
-  // move
-  const moveEvent2 = new Event('touchmove');
-  moveEvent2.touches = [{ pageY: 150 }];
-  getElement().dispatchEvent(moveEvent2);
-
-  // move
-  const moveEvent3 = new Event('touchmove');
-  moveEvent3.touches = [{ pageY: 20 }];
-  getElement().dispatchEvent(moveEvent3);
-
-  // move
-  const moveEvent4 = new Event('touchmove');
-  moveEvent4.touches = [{ pageY: 100 }];
-  getElement().dispatchEvent(moveEvent4);
-
-  // end
-  const endEvent = new Event('touchend');
-  getElement().dispatchEvent(endEvent);
-
-  // smooth
-  jest.runAllTimers();
+  act(() => {
+    // start
+    const touchEvent = new Event('touchstart');
+    touchEvent.touches = [{ pageY: 100 }];
+    getElement().dispatchEvent(touchEvent);
+
+    // move
+    const moveEvent1 = new Event('touchmove');
+    moveEvent1.touches = [{ pageY: 110 }];
+    getElement().dispatchEvent(moveEvent1);
+
+    // move
+    const moveEvent2 = new Event('touchmove');
+    moveEvent2.touches = [{ pageY: 150 }];
+    getElement().dispatchEvent(moveEvent2);
+
+    // move
+    const moveEvent3 = new Event('touchmove');
+    moveEvent3.touches = [{ pageY: 20 }];
+    getElement().dispatchEvent(moveEvent3);
+
+    // move
+    const moveEvent4 = new Event('touchmove');
+    moveEvent4.touches = [{ pageY: 100 }];
+    getElement().dispatchEvent(moveEvent4);
+
+    // end
+    const endEvent = new Event('touchend');
+    getElement().dispatchEvent(endEvent);
+
+    // smooth
+    jest.runAllTimers();
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('origin scroll', () => {
const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100) });
function getElement() {
return wrapper.find('.rc-virtual-list-holder').instance();
}
// start
const touchEvent = new Event('touchstart');
touchEvent.touches = [{ pageY: 100 }];
getElement().dispatchEvent(touchEvent);
// move
const moveEvent = new Event('touchmove');
moveEvent.touches = [{ pageY: 90 }];
getElement().dispatchEvent(moveEvent);
const moveEvent1 = new Event('touchmove');
moveEvent1.touches = [{ pageY: 110 }];
getElement().dispatchEvent(moveEvent1);
// move
const moveEvent2 = new Event('touchmove');
moveEvent2.touches = [{ pageY: 150 }];
getElement().dispatchEvent(moveEvent2);
// move
const moveEvent3 = new Event('touchmove');
moveEvent3.touches = [{ pageY: 20 }];
getElement().dispatchEvent(moveEvent3);
// move
const moveEvent4 = new Event('touchmove');
moveEvent4.touches = [{ pageY: 100 }];
getElement().dispatchEvent(moveEvent4);
// end
const endEvent = new Event('touchend');
getElement().dispatchEvent(endEvent);
// smooth
jest.runAllTimers();
expect(wrapper.find('ul').instance().scrollTop > 10).toBeTruthy();
expect(wrapper.find('ul').instance().scrollTop).toBe(0);
wrapper.unmount();
it('origin scroll - multi moves and bounce back', () => {
const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100) });
function getElement() {
return wrapper.find('.rc-virtual-list-holder').instance();
}
act(() => {
// start
const touchEvent = new Event('touchstart');
touchEvent.touches = [{ pageY: 100 }];
getElement().dispatchEvent(touchEvent);
// move
const moveEvent1 = new Event('touchmove');
moveEvent1.touches = [{ pageY: 110 }];
getElement().dispatchEvent(moveEvent1);
// move
const moveEvent2 = new Event('touchmove');
moveEvent2.touches = [{ pageY: 150 }];
getElement().dispatchEvent(moveEvent2);
// move
const moveEvent3 = new Event('touchmove');
moveEvent3.touches = [{ pageY: 20 }];
getElement().dispatchEvent(moveEvent3);
// move
const moveEvent4 = new Event('touchmove');
moveEvent4.touches = [{ pageY: 100 }];
getElement().dispatchEvent(moveEvent4);
// end
const endEvent = new Event('touchend');
getElement().dispatchEvent(endEvent);
// smooth
jest.runAllTimers();
});
expect(wrapper.find('ul').instance().scrollTop).toBe(0);
wrapper.unmount();
});
🤖 Prompt for AI Agents
tests/touch.test.js around lines 131 to 171: the touch event sequence and timer
advances are not wrapped in React's act and the test name duplicates the
previous one; wrap the entire sequence of dispatching
touchstart/touchmove/touchend events and jest.runAllTimers() inside act(...) to
avoid "An update was not wrapped in act(...)" warnings and change the it(...)
description to a unique name (e.g., "origin scroll - touch events wrapped in
act") so the test is distinguishable.

@muzea
Copy link
Author

muzea commented Sep 26, 2025

@afc163 @zombieJ
覆蓋率下降的问题好了

@XianZhengquan
Copy link

@afc163 @zombieJ 大佬,可以合并了?

@XianZhengquan
Copy link

@afc163 @zombieJ 大佬,合并了噻

@zombieJ
Copy link
Member

zombieJ commented Sep 30, 2025

@gemini-code-assist pls help to check on this.

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

你好,感谢你对性能优化的贡献。这次的改动非常棒,通过 useCallbackuseMemo 对组件和钩子进行 memoization,有效地减少了不必要的重渲染,这是 React 性能优化的典范。移除 SharedConfig 的 API 简化也让代码更加清晰易用。同时,对测试用例的改进(如在 act 中包装异步操作)和补充,也大大增强了代码的稳定性和可靠性。我只在测试代码中发现了一个小问题,即存在重复的测试用例名称,除此之外,整体代码质量非常高。

};
}

const handleResize = React.useCallback(
Copy link
Member

Choose a reason for hiding this comment

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

const promiseIdRef = useRef<number>(0);

function cancelRaf() {
const cancelRaf = React.useCallback(function cancelRaf() {
Copy link
Member

Choose a reason for hiding this comment

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

不需要包 useCallback,原本代码里也是不加到 deps 里的。这里加了下面放到 effect 的 deps 是没有必要的。

}, []);

function collectHeight(sync = false) {
const collectHeight = React.useCallback(function (sync = false) {
Copy link
Member

Choose a reason for hiding this comment

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

collectHeight 应该是没有作为过条件,也是不需要 useCallback 的

</Item>
);
});
}, [list, startIndex, endIndex, setNodeRef, renderFunc, getKey, offsetX, scrollWidth]);
Copy link
Member

Choose a reason for hiding this comment

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

稍微有点复杂了,未来如果加 props 也容易 break。如果是只是为了 Item 不需要重新渲染,可以考虑 Item 直接用 React.memo 包一下,条件里直接忽略 setRef 即可。

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
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.

Tree 的 titleRender 刷新多次
4 participants