Skip to content

fix(tree): stale closure in React.memo causing sibling folder collapse#1015

Merged
oldj merged 2 commits into
oldj:masterfrom
beyond-infra:fix/folder-collapse-stale-closure
Jul 4, 2026
Merged

fix(tree): stale closure in React.memo causing sibling folder collapse#1015
oldj merged 2 commits into
oldj:masterfrom
beyond-infra:fix/folder-collapse-stale-closure

Conversation

@beyond-infra

Copy link
Copy Markdown
Contributor

问题

展开一个文件夹后,其他已展开的文件夹被自动折叠。Closes #1014

根因

Node.tsx 使用 React.memo,其 isEqual 比较函数不检查 onChange(即 onNodeChange)prop 是否变化。当一个 Node 的 data 未变时,React.memo 复用旧 VNode,保留了旧的 onNodeChange handler。

旧 handler 的闭包中 tree 是创建时的快照,不包含之后其他文件夹的折叠/展开操作。当该 handler 被调用时,cloneDeep(旧tree) 将其他文件夹的状态回退到过去的值。

触发链路

1. 初始:A 折叠,B 展开,C 展开
2. 折叠 C → Tree 重渲染,onNodeChange_2 创建
3. A 的 data 未变 → isEqual=true → A 保留旧的 onNodeChange_1
4. 展开 A → 触发 onNodeChange_1(闭包中 tree=[A折叠, B展开, C展开])
5. cloneDeep(旧tree) → A 展开,C 也被「恢复」为展开

修复

Tree.tsxonNodeChange 改为 useCallback + ref

const treeRef = useRef(tree)
treeRef.current = tree
const onChangeRef = useRef(onChange)
onChangeRef.current = onChange

const onNodeChange = useCallback((id, data) => {
  const tree2 = lodash.cloneDeep(treeRef.current)
  // ...
  onChangeRef.current?.(tree2)
}, [])

useCallback 空依赖保证函数引用永不变(React.memo 稳定),内部通过 ref 始终读取最新 treeonChange

onNodeChange in Tree was re-created every render with the current
tree in its closure. Node.tsx uses React.memo with an isEqual that
does not compare the onChange prop, so when a node's data hasn't
changed it retains the old onNodeChange handler.

The old handler's closure captures a stale tree snapshot that
doesn't include subsequent collapse/expand operations on sibling
folders. When invoked, cloneDeep(oldTree) rewinds other folders
to their previous state, causing them to auto-collapse.

Fix: wrap onNodeChange in useCallback([], ...) with a stable ref
(treeRef) so the function identity never changes, but always
reads the latest tree via the ref.

Closes #1014
@beyond-infra
beyond-infra force-pushed the fix/folder-collapse-stale-closure branch from 0f9ea76 to bec0819 Compare July 3, 2026 17:11
… test

The stale-closure fix assigned treeRef/onChangeRef during render, which
trips eslint's react-hooks/refs ("Cannot update ref during render").
Move the assignments into an effect; behavior is unchanged because the
refs are only read from event handlers, which fire after commit.

Also add a Tree.test.tsx regression for #1014: collapsing one folder
must not revert a sibling folder's collapse state.
@oldj
oldj merged commit 7d98214 into oldj:master Jul 4, 2026
1 check passed
@oldj

oldj commented Jul 4, 2026

Copy link
Copy Markdown
Owner

感谢!

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