close
Skip to content

refactor: migration to FC#1021

Open
li-jia-nan wants to merge 5 commits into
masterfrom
FC-29
Open

refactor: migration to FC#1021
li-jia-nan wants to merge 5 commits into
masterfrom
FC-29

Conversation

@li-jia-nan
Copy link
Copy Markdown
Member

@li-jia-nan li-jia-nan commented Mar 29, 2026

Summary by CodeRabbit

发布说明

  • 新功能

    • 引入新的TreeRef接口,通过引用(ref)支持访问Tree组件的内部方法和状态。
  • 改进

    • prefixCls属性现已改为可选配置项。
  • 重构

    • Tree组件内部架构已现代化,提升代码可维护性和性能。

Copilot AI review requested due to automatic review settings March 29, 2026 02:55
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 29, 2026

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

Project Deployment Actions Updated (UTC)
tree Ready Ready Preview, Comment Mar 29, 2026 3:55am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 29, 2026

Walkthrough

该 PR 将 Tree 组件从类组件重构为函数式组件,使用 React Hooks(useRef、useImperativeHandle)管理状态和生命周期,并暴露新的 TreeRef 接口。同时优化了默认 props 的处理方式,并更新了相关测试以适配新的类型系统。

Changes

组群 / 文件 变更摘要
Tree 组件重构
src/Tree.tsx
将类组件转换为函数式组件,引入 useRef 替代实例字段、useImperativeHandle 暴露 ref,使用 hook 替代 getDerivedStateFromProps,合并默认 props 行为(allowDropdropIndicatorRender),使 prefixCls 可选。
类型导出
src/index.ts
新增 TreeRef 类型导出,引入 TreeExport 常量作为默认导出,保持运行时值不变。
TreeNode 上下文处理
src/TreeNode.tsx
移除 TreeContext 的 nullish-safe 解构,假设 context 值始终已定义。
Tree 测试
tests/Tree.spec.tsx
更新引入使用 TreeRef 类型,添加两个新的 ref 兼容性测试用例,验证命令式字段可读写及后向兼容性。
工具函数测试
tests/util.spec.js
新增大量单元测试覆盖数组助手、状态转换、树遍历等工具函数的边界情况和空输入处理。
测试工具更新
tests/util.ts
更新 spyError 函数的 Jest console.error spy 实现为显式 jest.fn()

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • MadCcc
  • afc163
  • zombieJ

Poem

🐰 从类到函数,变身新模样
Hooks 握在爪间,状态舞翩翩
Ref 如明灯指,命令式永驻
树木又更新,兔兔笑颜开 ✨

🚥 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 The title accurately describes the main refactoring work: converting the Tree component from a class component to a functional component (FC) with hooks, which is the primary change across multiple files.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch FC-29

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.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 29, 2026

Codecov Report

❌ Patch coverage is 99.84277% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 99.92%. Comparing base (1f049b2) to head (19193cc).

Files with missing lines Patch % Lines
src/Tree.tsx 99.84% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1021      +/-   ##
==========================================
+ Coverage   99.84%   99.92%   +0.08%     
==========================================
  Files          14       15       +1     
  Lines        1302     1417     +115     
  Branches      396      424      +28     
==========================================
+ Hits         1300     1416     +116     
+ Misses          2        1       -1     

☔ 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
Copy Markdown

@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 refactors the Tree component from a class-based implementation to a functional component using hooks and React.forwardRef. It also adds onContextMenu and onScroll support to NodeList and includes a ResizeObserver polyfill for the test environment. Feedback focuses on simplifying state updates within the onNodeLoad callback by utilizing the setMergedState helper instead of direct setInnerState calls to ensure consistency and prevent potential state staleness.

Comment thread src/Tree.tsx
Comment thread src/Tree.tsx
Comment thread src/Tree.tsx
Copy link
Copy Markdown

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 refactors the core Tree component from a class component to a function component (FC) using hooks/forwardRef, and updates surrounding types/test setup to support the new implementation.

Changes:

  • Migrate src/Tree.tsx from a class-based component to a hook-based forwardRef component while preserving existing behaviors (drag/drop, loadData, keyboard, etc.).
  • Extend NodeList props typing to include onContextMenu and onScroll.
  • Add a Jest ResizeObserver polyfill in test setup to prevent missing-browser-API failures.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
tests/setupFilesAfterEnv.js Adds a ResizeObserver stub for the Jest environment.
src/index.ts Updates exported types from the package entrypoint (per diff).
src/Tree.tsx Large refactor: class component rewritten as a function component with hooks and forwardRef.
src/NodeList.tsx Adds typings for onContextMenu and onScroll passthrough.

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

Comment thread src/Tree.tsx
Copy link
Copy Markdown

@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: 3

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

Inline comments:
In `@src/Tree.tsx`:
- Around line 339-347: When fieldNames changes (detected by
needSync('fieldNames')), you must rebuild the full derived chain instead of only
updating fieldNames: after setting fieldNames =
fillFieldNames(mergedProps.fieldNames) and newState.fieldNames, also recompute
keyEntities and flattenNodes the same way you do in the treeData/children sync
branch so that derived structures used by expand/select/locate reflect the new
mapping; update the logic around needSync('fieldNames') to call the same code
paths that compute keyEntities and flattenNodes from
mergedProps.treeData/children (and assign them into newState) so shared treeData
references work correctly when only fieldNames changes.
- Around line 796-817: The current call to currentLoadData(treeNode) can
synchronously throw, leaving loadingKeysSetRef and loadingKeys state stuck; wrap
the call so synchronous throws become a rejected Promise (e.g. assign promise =
Promise.resolve().then(() => currentLoadData(treeNode)) or try/catch and set
promise = Promise.reject(err)) and ensure the existing .then/.catch/.finally
handlers still run to remove the key from loadingKeysSetRef and update
loadingKeys via setInnerState (the same pattern should be applied to the other
loadData call site that uses currentLoadData and loadingKeys handling).
- Around line 719-735: The onActiveChange callback currently calls
setMergedState({ activeKey: newActiveKey }) which overrides a controlled
activeKey prop; instead mirror the pattern used for expandedKeys/selectedKeys by
only updating internal state when activeKey is uncontrolled. Update
onActiveChange to check whether props.activeKey (via
mergedPropsRef.current.activeKey or stateRef.current.controlledKeys flag) is
undefined before calling setMergedState, keep scrollTo and
mergedPropsRef.current.onActiveChange?.(newActiveKey) as-is, and ensure the
initial sync logic that reads props.activeKey still sets internal state when
uncontrolled so controlled props are never clobbered by onActiveChange or
initial mount.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8b10e225-a409-412f-88c6-57ea7991813a

📥 Commits

Reviewing files that changed from the base of the PR and between 9b983c9 and adba5f0.

📒 Files selected for processing (4)
  • src/NodeList.tsx
  • src/Tree.tsx
  • src/index.ts
  • tests/setupFilesAfterEnv.js

Comment thread src/Tree.tsx
Comment thread src/Tree.tsx
Comment thread src/Tree.tsx
Copy link
Copy Markdown

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

🧹 Nitpick comments (2)
tests/Tree.spec.tsx (2)

1488-1490: 类型不完整可能导致 TypeScript 错误。

dragNodeProps 被赋值为 { eventKey: '0' },但根据 TreeRef 接口定义,该字段类型为 TreeNodeProps<TreeDataType> | null。这个部分对象可能无法满足完整的 TreeNodeProps 类型要求。

建议:使用类型断言或更完整的模拟对象
-    const dragNodeProps = { eventKey: '0' };
+    const dragNodeProps = { eventKey: '0' } as any;

或者提供符合 TreeNodeProps 的完整对象。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Tree.spec.tsx` around lines 1488 - 1490, The test assigns a partial
object to treeRef.current.dragNodeProps which is typed as
TreeNodeProps<TreeDataType> | null; fix by making the mock satisfy the full
TreeNodeProps<TreeDataType> shape or use a type assertion. Update the test to
set dragNodeProps to either a complete mock object including required fields
from TreeNodeProps (e.g., key, title, children, pos as needed) or cast the
partial to the correct type like ({} as unknown as TreeNodeProps<TreeDataType>)
before assigning, and keep the expect assertion on
treeRef.current.dragNodeProps.

1503-1503: 考虑使用 TreeRef 替代 any 以保持一致性。

此测试中使用 React.createRef<any>(),而文件中其他类似测试已改用 TreeRef。根据上下文代码片段,onNodeLoadtriggerExpandActionExpandonNodeDrop 等方法均已在 useImperativeHandle 中暴露。

如果这是为了测试非公开 API 而有意使用 any,建议添加注释说明原因。

建议:统一类型或添加注释
-    const treeRef = React.createRef<any>();
+    // Use `any` to access internal imperative methods not part of public TreeRef
+    const treeRef = React.createRef<any>();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Tree.spec.tsx` at line 1503, Replace the loose any ref with the
specific TreeRef type to match other tests: change the creation from
React.createRef<any>() to React.createRef<TreeRef>() (or import/define TreeRef
if missing), since methods like onNodeLoad, triggerExpandActionExpand and
onNodeDrop are exposed via useImperativeHandle; if you intentionally need any to
access non-public internals, add a brief comment above the treeRef declaration
explaining why any is required.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/Tree.spec.tsx`:
- Around line 1488-1490: The test assigns a partial object to
treeRef.current.dragNodeProps which is typed as TreeNodeProps<TreeDataType> |
null; fix by making the mock satisfy the full TreeNodeProps<TreeDataType> shape
or use a type assertion. Update the test to set dragNodeProps to either a
complete mock object including required fields from TreeNodeProps (e.g., key,
title, children, pos as needed) or cast the partial to the correct type like ({}
as unknown as TreeNodeProps<TreeDataType>) before assigning, and keep the expect
assertion on treeRef.current.dragNodeProps.
- Line 1503: Replace the loose any ref with the specific TreeRef type to match
other tests: change the creation from React.createRef<any>() to
React.createRef<TreeRef>() (or import/define TreeRef if missing), since methods
like onNodeLoad, triggerExpandActionExpand and onNodeDrop are exposed via
useImperativeHandle; if you intentionally need any to access non-public
internals, add a brief comment above the treeRef declaration explaining why any
is required.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 72f390ad-05e9-4cc7-84f1-c18a5b434f21

📥 Commits

Reviewing files that changed from the base of the PR and between adba5f0 and a1c60cb.

📒 Files selected for processing (1)
  • tests/Tree.spec.tsx

Copy link
Copy Markdown

@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)
src/Tree.tsx (1)

1750-1862: useImperativeHandle 暴露了过多内部实现细节。

TreeRef 接口暴露了大量内部方法(如 onNodeClickonNodeExpandcleanDragState 等),这些本应是私有的。这会增加 API 维护负担,且外部调用这些方法可能导致状态不一致。

建议仅暴露真正需要的公共 API(如 scrollTostate 读取),将内部方法保留为私有实现。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Tree.tsx` around lines 1750 - 1862, The useImperativeHandle currently
exposes many internal handlers which should remain private; update the exposed
API to only public members (e.g., scrollTo, listRef, getters for
state/destroyed, and any truly public helpers) and remove internal methods like
onNodeClick, onNodeExpand, onNodeDragStart/Enter/Over/Leave/End/Drop,
cleanDragState, resetDragState, onListChangeStart/End, onMouseDown,
onGlobalMouseUp, onFocus, onBlur, onKeyDown, getTreeNodeRequiredProps,
setUncontrolledState, triggerExpandActionExpand, getActiveItem, offsetActiveKey,
setExpandedKeys, onNodeLoad, onNodeSelect, onNodeCheck, onNodeMouseEnter/Leave
from the object returned by useImperativeHandle and from its dependency array;
also update the TreeRef interface to match the reduced public surface and adjust
any external callers/tests to use only the new public methods (leave internal
functions as module/private refs).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Tree.tsx`:
- Around line 589-591: 当前组件在渲染体内直接调用 setInnerState(mergedState)(比较由
shallowEqual(innerState, mergedState) 判断)属于 React 反模式;请把派生状态更新移出渲染流程:要么用 useMemo
计算 mergedState 并直接在渲染中使用该 memoized 值而不维护 innerState(移除 setInnerState 调用),要么保留
innerState 但将条件更新放入 useEffect(() => { if (!shallowEqual(innerState,
mergedState)) setInnerState(mergedState); }, [mergedState]),并确保引用
shallowEqual、innerState、mergedState、setInnerState 的地方一致以避免不必要的重渲染或循环。

In `@src/TreeNode.tsx`:
- Around line 45-46: TreeNode directly destructures the result of
React.useContext(TreeContext) which can be null and causes runtime crashes when
TreeNode is used outside a provider; change the code to guard against a null
context by providing safe defaults or checking before destructuring (e.g.,
ensure the value from React.useContext(TreeContext) is non-null or replace it
with a fallback object) so that symbols like classNames/treeClassNames, styles
and context.disabled are read safely (use the guarded context or optional
chaining) to prevent TypeError when TreeNode is imported/used without a
surrounding TreeContext.Provider.

---

Nitpick comments:
In `@src/Tree.tsx`:
- Around line 1750-1862: The useImperativeHandle currently exposes many internal
handlers which should remain private; update the exposed API to only public
members (e.g., scrollTo, listRef, getters for state/destroyed, and any truly
public helpers) and remove internal methods like onNodeClick, onNodeExpand,
onNodeDragStart/Enter/Over/Leave/End/Drop, cleanDragState, resetDragState,
onListChangeStart/End, onMouseDown, onGlobalMouseUp, onFocus, onBlur, onKeyDown,
getTreeNodeRequiredProps, setUncontrolledState, triggerExpandActionExpand,
getActiveItem, offsetActiveKey, setExpandedKeys, onNodeLoad, onNodeSelect,
onNodeCheck, onNodeMouseEnter/Leave from the object returned by
useImperativeHandle and from its dependency array; also update the TreeRef
interface to match the reduced public surface and adjust any external
callers/tests to use only the new public methods (leave internal functions as
module/private refs).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0d0b1e61-0f3b-4226-aa50-00ce1983f574

📥 Commits

Reviewing files that changed from the base of the PR and between a1c60cb and 19193cc.

📒 Files selected for processing (5)
  • src/Tree.tsx
  • src/TreeNode.tsx
  • src/index.ts
  • tests/util.spec.js
  • tests/util.ts
✅ Files skipped from review due to trivial changes (1)
  • tests/util.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/index.ts

Comment thread src/Tree.tsx
Comment on lines +589 to 591
if (!shallowEqual(innerState, mergedState)) {
setInnerState(mergedState);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

在渲染期间调用 setInnerState 是 React 反模式。

innerStatemergedState 不相等时,直接在渲染函数体内调用 setInnerState(mergedState) 会触发立即重新渲染。虽然 React 18 对此有一定的批处理优化,但这仍然是不推荐的模式,可能导致:

  1. 性能问题:每次 props 变化都会触发额外的重渲染
  2. 可能的无限循环风险(如果 shallowEqual 判断不准确)
🔧 建议使用 useEffect 或调整派生状态逻辑

考虑将派生状态计算移入 useMemo 并直接使用 mergedState,而非同步更新 innerState

  const mergedState = React.useMemo(
    () =>
      mergeState(
        innerState,
        getDerivedStateFromProps(props, mergedProps, innerState, prevPropsRef.current),
      ),
    [innerState, mergedProps, props],
  );

- if (!shallowEqual(innerState, mergedState)) {
-   setInnerState(mergedState);
- }
+ // 直接使用 mergedState 作为当前状态,无需同步回 innerState
+ // innerState 仅用于存储用户交互产生的状态变更

或者如果确实需要同步,使用 useEffect

- if (!shallowEqual(innerState, mergedState)) {
-   setInnerState(mergedState);
- }
+ React.useEffect(() => {
+   if (!shallowEqual(innerState, mergedState)) {
+     setInnerState(mergedState);
+   }
+ }, [innerState, mergedState]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Tree.tsx` around lines 589 - 591, 当前组件在渲染体内直接调用
setInnerState(mergedState)(比较由 shallowEqual(innerState, mergedState) 判断)属于 React
反模式;请把派生状态更新移出渲染流程:要么用 useMemo 计算 mergedState 并直接在渲染中使用该 memoized 值而不维护
innerState(移除 setInnerState 调用),要么保留 innerState 但将条件更新放入 useEffect(() => { if
(!shallowEqual(innerState, mergedState)) setInnerState(mergedState); },
[mergedState]),并确保引用 shallowEqual、innerState、mergedState、setInnerState
的地方一致以避免不必要的重渲染或循环。

Comment thread src/TreeNode.tsx
Comment on lines 45 to +46
const context = React.useContext(TreeContext);
const { classNames: treeClassNames, styles } = context || {};
const { classNames: treeClassNames, styles } = context;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 检查 TreeNode 是否被导出并可能被独立使用
rg -n "export.*TreeNode" --type=ts --type=tsx
rg -n "import.*TreeNode.*from" --type=ts --type=tsx -g '!src/Tree.tsx' -g '!src/index.ts'

Repository: react-component/tree

Length of output: 119


🏁 Script executed:

#!/bin/bash
# Check TreeNode exports and imports with corrected rg syntax
echo "=== TreeNode exports ==="
rg -n "export.*TreeNode" --type=jsx

echo "=== TreeNode imports outside Tree.tsx ==="
rg -n "import.*TreeNode" --type=jsx | grep -v "src/Tree.tsx"

echo "=== Check index.ts exports ==="
cat -n src/index.ts 2>/dev/null || cat -n src/index.tsx 2>/dev/null

echo "=== Check TreeContext definition ==="
rg -n "createContext" --type=jsx -A 2

Repository: react-component/tree

Length of output: 791


🏁 Script executed:

#!/bin/bash
# Try without type filter
echo "=== TreeNode imports (all files) ==="
rg "import.*TreeNode" -n

echo "=== TreeContext definition ==="
cat -n src/contextTypes.ts | head -100

Repository: react-component/tree

Length of output: 6453


🏁 Script executed:

#!/bin/bash
# Check actual usage of TreeNode in tests - is it standalone or within Tree?
echo "=== TreeNode usage in tests ==="
grep -n "TreeNode\|Tree" tests/Tree.spec.tsx | head -40

echo "=== Check src/TreeNode.tsx lines around 45-46 ==="
sed -n '40,55p' src/TreeNode.tsx

echo "=== Check src/TreeNode.tsx destructuring pattern ==="
rg -n "context\?" src/TreeNode.tsx | head -20

Repository: react-component/tree

Length of output: 2462


🏁 Script executed:

#!/bin/bash
# Check if there's git diff or if we can see the original code
echo "=== Git log for TreeNode.tsx ==="
git log --oneline -10 src/TreeNode.tsx 2>/dev/null || echo "Git history unavailable"

echo "=== Check full usage of context in TreeNode.tsx ==="
sed -n '45,65p' src/TreeNode.tsx

echo "=== Check if context.disabled is safe with null ==="
grep -n "context\." src/TreeNode.tsx | head -15

Repository: react-component/tree

Length of output: 1984


删除 context 的空值保护存在运行时崩溃风险。

TreeContext 的默认值为 null,而 TreeNode.tsx 第 46 行直接解构 context 属性而无空值检查:const { classNames: treeClassNames, styles } = context;。如果 TreeNodeTreeContext.Provider 外部渲染(例如独立导入使用),会抛出 TypeError: Cannot destructure property 'classNames' of 'null'。此外,后续代码如第 55 行 context.disabled 也会引发错误。

虽然当前所有测试和示例都在 Tree 组件内渲染 TreeNode,但由于 TreeNodesrc/index.ts 中被公开导出,消费者可以独立导入和使用它,这构成了一个潜在的破坏性变更。

建议保留防御性检查:

  const context = React.useContext(TreeContext);
- const { classNames: treeClassNames, styles } = context;
+ const { classNames: treeClassNames, styles } = context || {};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/TreeNode.tsx` around lines 45 - 46, TreeNode directly destructures the
result of React.useContext(TreeContext) which can be null and causes runtime
crashes when TreeNode is used outside a provider; change the code to guard
against a null context by providing safe defaults or checking before
destructuring (e.g., ensure the value from React.useContext(TreeContext) is
non-null or replace it with a fallback object) so that symbols like
classNames/treeClassNames, styles and context.disabled are read safely (use the
guarded context or optional chaining) to prevent TypeError when TreeNode is
imported/used without a surrounding TreeContext.Provider.

@afc163
Copy link
Copy Markdown
Member

afc163 commented Apr 3, 2026

看下 AI 的 review

@afc163 afc163 requested a review from Copilot April 3, 2026 07:31
@afc163
Copy link
Copy Markdown
Member

afc163 commented Apr 3, 2026

@codex review

Copy link
Copy Markdown

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

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.


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

Comment thread src/TreeNode.tsx
@@ -43,7 +43,7 @@ const TreeNode: React.FC<Readonly<TreeNodeProps>> = props => {
const nodeId = getId(treeId, eventKey);

const context = React.useContext(TreeContext);
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

TreeContext is created with a default value of null (see src/contextTypes.ts), so React.useContext(TreeContext) can be null when TreeNode is rendered outside of a provider (or during import-loop edge cases called out in contextTypes.ts). Destructuring context directly will throw at runtime. Consider restoring a safe fallback (e.g. const { ... } = context || {}) or ensuring TreeContext is created with a non-null default value.

Suggested change
const context = React.useContext(TreeContext);
const context =
React.useContext(TreeContext) ||
({} as NonNullable<React.ContextType<typeof TreeContext>>);

Copilot uses AI. Check for mistakes.
Comment thread src/Tree.tsx
Comment on lines +580 to 592
const mergedState = React.useMemo(
() =>
mergeState(
innerState,
getDerivedStateFromProps(props, mergedProps, innerState, prevPropsRef.current),
),
[innerState, mergedProps, props],
);

// ================= loadedKeys ==================
if (needSync('loadedKeys')) {
newState.loadedKeys = props.loadedKeys;
if (!shallowEqual(innerState, mergedState)) {
setInnerState(mergedState);
}

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

Calling setInnerState during render (if (!shallowEqual(...)) { setInnerState(...) }) is an unsafe pattern and can cause render loops / React warnings (state updates during render). This derived-state sync should be moved into an effect (useEffect/useLayoutEffect) or refactored so state is computed without a render-time state update.

Copilot uses AI. Check for mistakes.
Comment thread src/Tree.tsx
Comment on lines +297 to +305
const shallowEqual = (objA: object, objB: object) => {
if (objA === objB) {
return true;
}
const keysA = Object.keys(objA);
return (
keysA.length === Object.keys(objB).length && keysA.every(key => Object.is(objA[key], objB[key]))
);
};
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

shallowEqual indexes into parameters typed as object (objA[key] / objB[key]), which is not type-safe and can produce TS errors depending on compiler options. Consider typing these as Record<string, unknown> (or a generic constrained to record types) to avoid unsafe indexing.

Copilot uses AI. Check for mistakes.
Comment thread src/Tree.tsx
Comment on lines +597 to 601
mergedState.loadedKeys.forEach(key => {
pendingLoadedKeysRef.current.delete(key);
loadingKeysSetRef.current.delete(key);
});

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

This forEach mutates refs (pendingLoadedKeysRef, loadingKeysSetRef) during render, which is a side effect and can behave incorrectly under StrictMode/concurrent rendering (render may run more than once). Consider moving this cleanup into an effect that runs when mergedState.loadedKeys changes.

Suggested change
mergedState.loadedKeys.forEach(key => {
pendingLoadedKeysRef.current.delete(key);
loadingKeysSetRef.current.delete(key);
});
React.useEffect(() => {
mergedState.loadedKeys.forEach(key => {
pendingLoadedKeysRef.current.delete(key);
loadingKeysSetRef.current.delete(key);
});
}, [mergedState.loadedKeys]);

Copilot uses AI. Check for mistakes.
Comment thread tests/util.spec.js
Comment on lines +237 to +243
it('convertTreeToEntities keeps wrapper when initWrapper returns nothing', () => {
const wrapper = convertDataToEntities([{ key: 'test' }], {
initWrapper: jest.fn(() => undefined),
});

expect(wrapper.keyEntities.test.key).toBe('test');
});
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

Test name says convertTreeToEntities, but the test is exercising convertDataToEntities. Renaming the test case will make failures easier to interpret.

Copilot uses AI. Check for mistakes.
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.

3 participants