fix(tree-select): hoist Row to module scope to prevent row remounts

The row renderer was defined inside VirtualizedTree, giving it a new
component identity on every parent render — react-window then remounted
every row, destroying row DOM/state. Move Row to module scope and pass
flattened/tree/renderNode through react-window's rowProps instead of the
closure.

Verified at runtime: rows render, expand/collapse and select work, and
selection state survives re-renders, with no console errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zhangheng
2026-06-05 11:32:18 +08:00
parent dbd01ee15b
commit f930432ffe

View File

@@ -4,8 +4,8 @@ import {
TreeNodeData, TreeNodeData,
UseTreeReturnType, UseTreeReturnType,
} from "@mantine/core" } from "@mantine/core"
import React, { CSSProperties, useMemo } from "react" import React, { useMemo } from "react"
import { List } from "react-window" import { List, RowComponentProps } from "react-window"
import "./mantine-scrollbar.css" import "./mantine-scrollbar.css"
export interface VirtualizedTreeProps { export interface VirtualizedTreeProps {
@@ -51,30 +51,24 @@ function flattenByExpanded(
return res return res
} }
/** 行渲染所需的闭包数据,通过 react-window 的 rowProps 传入 */
interface RowData {
flattened: { node: TreeNodeData; level: number }[]
tree: UseTreeReturnType
renderNode: (payload: RenderTreeNodePayload) => React.ReactNode
}
/** /**
* VirtualizedTree - Mantine v8 compatible virtualized Tree * 单行渲染组件 —— 提到模块作用域,避免每次父组件渲染都生成新的组件实例
* - Accepts tree (useTree() result) and data: TreeNodeData[] * (否则 react-window 会在每次渲染时 remount 所有行,销毁行内状态)
* - Passes RenderTreeNodePayload to renderNode exactly like Mantine
* - Uses react-window FixedSizeList for virtualization
*/ */
export function VirtualizedTree({ function Row({
index,
style,
flattened,
tree, tree,
data,
renderNode, renderNode,
// levelOffset = 24, }: RowComponentProps<RowData>) {
height = 400,
itemSize = 36,
overscanCount = 5,
}: VirtualizedTreeProps) {
// 扁平化数据:根据 tree.expandedState 决定哪些子节点可见
const flattened = useMemo(
() =>
flattenByExpanded(data, (tree && (tree as any).expandedState) || {}, 0),
[data, tree]
)
// react-window 的行渲染
/** 单行渲染组件 */
const Row = ({ index, style }: { index: number; style: CSSProperties }) => {
const { node, level } = flattened[index] const { node, level } = flattened[index]
// 构造 Mantine 的 RenderTreeNodePayload // 构造 Mantine 的 RenderTreeNodePayload
@@ -129,14 +123,36 @@ export function VirtualizedTree({
return <div style={paddedStyle}>{renderNode(payload)}</div> return <div style={paddedStyle}>{renderNode(payload)}</div>
} }
/**
* VirtualizedTree - Mantine v8 compatible virtualized Tree
* - Accepts tree (useTree() result) and data: TreeNodeData[]
* - Passes RenderTreeNodePayload to renderNode exactly like Mantine
* - Uses react-window FixedSizeList for virtualization
*/
export function VirtualizedTree({
tree,
data,
renderNode,
// levelOffset = 24,
height = 400,
itemSize = 36,
overscanCount = 5,
}: VirtualizedTreeProps) {
// 扁平化数据:根据 tree.expandedState 决定哪些子节点可见
const flattened = useMemo(
() =>
flattenByExpanded(data, (tree && (tree as any).expandedState) || {}, 0),
[data, tree]
)
return ( return (
<List <List
rowCount={flattened.length} rowCount={flattened.length}
style={{ height, width: "100%" }} style={{ height, width: "100%" }}
className="custom-scrollbar" className="custom-scrollbar"
rowHeight={itemSize} rowHeight={itemSize}
rowComponent={({ index, style }) => <Row index={index} style={style} />} rowComponent={Row}
rowProps={{}} rowProps={{ flattened, tree, renderNode }}
overscanCount={overscanCount}></List> overscanCount={overscanCount}></List>
) )
} }