You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React+react-dnd拖拽应用疑似stale closure:添加子项后根项丢失

树形拖拽列表添加子项时根项丢失的Bug排查

问题场景

我正在开发一个拖拽应用,支持用户通过拖拽构建树形列表,包含两种拖拽操作:

  • 将项拖到列表区域,作为根项添加
  • 将项拖到现有项上,作为其子项添加

添加子项时存在Bug:子项添加成功,但目标父项下方的根项会被意外移除。

复现步骤

  1. 在复现环境中拖拽3个及以上项到列表中
  2. 拖拽一个项到第一个或第二个现有根项上
  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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 19:45:57