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:
@@ -4,8 +4,8 @@ import {
|
||||
TreeNodeData,
|
||||
UseTreeReturnType,
|
||||
} from "@mantine/core"
|
||||
import React, { CSSProperties, useMemo } from "react"
|
||||
import { List } from "react-window"
|
||||
import React, { useMemo } from "react"
|
||||
import { List, RowComponentProps } from "react-window"
|
||||
import "./mantine-scrollbar.css"
|
||||
|
||||
export interface VirtualizedTreeProps {
|
||||
@@ -51,30 +51,24 @@ function flattenByExpanded(
|
||||
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[]
|
||||
* - Passes RenderTreeNodePayload to renderNode exactly like Mantine
|
||||
* - Uses react-window FixedSizeList for virtualization
|
||||
* 单行渲染组件 —— 提到模块作用域,避免每次父组件渲染都生成新的组件实例
|
||||
* (否则 react-window 会在每次渲染时 remount 所有行,销毁行内状态)
|
||||
*/
|
||||
export function VirtualizedTree({
|
||||
function Row({
|
||||
index,
|
||||
style,
|
||||
flattened,
|
||||
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]
|
||||
)
|
||||
// react-window 的行渲染
|
||||
/** 单行渲染组件 */
|
||||
const Row = ({ index, style }: { index: number; style: CSSProperties }) => {
|
||||
}: RowComponentProps<RowData>) {
|
||||
const { node, level } = flattened[index]
|
||||
|
||||
// 构造 Mantine 的 RenderTreeNodePayload
|
||||
@@ -129,14 +123,36 @@ export function VirtualizedTree({
|
||||
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 (
|
||||
<List
|
||||
rowCount={flattened.length}
|
||||
style={{ height, width: "100%" }}
|
||||
className="custom-scrollbar"
|
||||
rowHeight={itemSize}
|
||||
rowComponent={({ index, style }) => <Row index={index} style={style} />}
|
||||
rowProps={{}}
|
||||
rowComponent={Row}
|
||||
rowProps={{ flattened, tree, renderNode }}
|
||||
overscanCount={overscanCount}></List>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user