React+react-dnd拖拽应用疑似stale closure:添加子项后根项丢失
树形拖拽列表添加子项时根项丢失的Bug排查
问题场景
我正在开发一个拖拽应用,支持用户通过拖拽构建树形列表,包含两种拖拽操作:
- 将项拖到列表区域,作为根项添加
- 将项拖到现有项上,作为其子项添加
添加子项时存在Bug:子项添加成功,但目标父项下方的根项会被意外移除。
复现步骤
- 在复现环境中拖拽3个及以上项到列表中
- 拖拽一个项到第一个或第二个现有根项上
- 观察到子项添加成功,但该位置下方的根项被移除
代码概述
可拖拽项列表
export function DraggableItems() { const componentTypes = ["Car", "Truck", "Boat"]; return ( <div> <h4>These can be dragged:</h4> {componentTypes.map((x) => ( <DraggableItem itemType={x} /> ))} </div> ); } function DraggableItem({ itemType }: DraggableItemProps) { const [, drag] = useDrag(() => ({ type: "Item", item: { itemType: itemType } })); return <div ref={drag}>{itemType}</div>; }
这些项可拖至两个区域:DropPane 和已添加的 DroppedItem。
DropPane核心代码
export function DropPane() { const [items, setItems] = useState<Array<Item>>([]); const [, drop] = useDrop( () => ({ accept: "Item", drop: (droppedObj: any, monitor: any) => { if (monitor.didDrop()) { console.log("Drop already processed by nested dropzone"); } else { const newItem = makeRandomItem(droppedObj.itemType); setItems([...items, newItem]); } } }), [items] ); const deleteItem = (idToDelete: number) => { const deleteItemRecursively = (list: Array<Item>) => { const filteredList = list.filter((x) => x.id !== idToDelete); if (filteredList.length < list.length) { return filteredList; } else { return list.map((x) => { x.children = deleteItemRecursively(x.children); return x; }); } }; const listWithTargetDeleted = deleteItemRecursively(items); setItems(listWithTargetDeleted); }; const addItemAsChild = (child: Item, targetParent: Item) => { const addItemAsChildRecursively = (list: Array<Item>) => { return list.map((x) => { if (x.id === targetParent.id) { x.children.push(child); return x; } else { x.children = addItemAsChildRecursively(x.children); return x; } }); }; const reportComponentsWithChildAdded = addItemAsChildRecursively(items); setItems(reportComponentsWithChildAdded); }; return ( <div ref={drop} style={{ border: "1px solid black", marginTop: "2em" }}> <h4>You can any items anywhere here to add them to the list:</h4> {items.length === 0 || ( <DroppedItemList items={items} onDeleteClicked={deleteItem} addItemAsChild={addItemAsChild} indentation={0} /> )} </div> ); }
问题分析
推测addItemAsChild函数存在**过时闭包(stale closure)**问题:items状态在传入DroppedItemList时被闭包捕获,但deleteItem函数却能正常工作。
具体表现:添加5个项后,在addItemAsChild中打断点,拖拽项到第一个现有项上时,items仅包含1个项(实际应为5个);拖到第二个项上时,items仅包含2个项。似乎items状态在对应项渲染时被闭包捕获,但仅addItemAsChild出现该异常。
已尝试多种修复方案均未解决,希望得到问题修复建议或替代实现思路。
内容的提问来源于stack exchange,提问作者MSOACC
相关产品推荐
相关产品推荐

