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

React中更新拖拽节点状态导致应用崩溃问题求助

问题

我正在开发一款搜索算法可视化工具,Grid组件初始化矩阵并渲染为网格,每个网格单元对应一个Node组件。需求是允许用户拖拽start和end节点到任意单元格,目前通过onDragStart和onDragEnter事件实现以下逻辑:

  • 追踪用户拖拽的节点类型(start/end);
  • 通过onDragStart记录节点原所在单元格;
  • 通过onDragEnter追踪节点当前悬停的单元格;
  • 将节点追加到目标单元格;
  • 更新矩阵中原节点和新节点的状态;
  • 需要更新Grid组件中start/end节点的行列状态,供后续dijkstra函数使用。

但更新该状态时应用崩溃,报错:DOMException: Node.removeChild: The node to be removed is not a child of this node。注释掉状态更新代码则功能正常,以下是Node组件代码:

const Node = ({ nodeMatrix, stateHandling, dragStateHandling, row, col, isStart, isEnd }) => {

    const { setStartNodeRow, setStartNodeCol, setEndNodeRow, setEndNodeCol } = stateHandling;
    const { draggedItem, setDraggedItem } = dragStateHandling;

    //Just handling the 'start' node for now
    const handleDragStart = (e) => {
        const isStartNode = e.target.classList.contains('start');

        const startNode = document.querySelector('.start');
        //const endNode = document.querySelector('.end');
        
        const [prevStartParentNodeRow, prevStartParentNodeCol] = startNode.parentElement.id.match(/\d+/g);
        //const [prevEndParentNodeRow, prevEndParentNodeCol] = endNode.parentElement.id.match(/\d+/g);

        if(isStartNode) {
            setDraggedItem('start');
            //Sets the data for the drag object to be retrieved later and
            //used to update the corresponding node in the matrix;
            e.dataTransfer.setData('prevNodeId', `node-${prevStartParentNodeRow}-${prevStartParentNodeCol}`);

        } else {
            setDraggedItem('end');
            //e.dataTransfer.setData('prevNodeId', `node-${prevEndParentNodeRow}-${prevEndParentNodeCol}`)
        }
    };

    const handleDragEnter = (e) => {
        const startNode = document.querySelector('.start');
        const endNode = document.querySelector('.end');

        //This check avoids the 'start' and 'end' divs and 
        //always selects the parent cell;
        const newParent = e.target.id ? e.target : e.target.parentElement.id ? e.target.parentElement : e.target.parentElement.parentElement;
        const hoveringOver = newParent.id;

        //Retrieves the data set in 'handleDragStart' from the drag object to use as keys
        //to update the previous 'start' node in the grid;
        const [prevNodeRow, prevNodeCol] = e.dataTransfer.getData('prevNodeId').match(/\d+/g);

        //Gets the id of the cell we're currently hovering over to update
        //the new 'start' node in the grid;
        const [newRow, newCol] = hoveringOver.match(/\d+/g);

        if(draggedItem === 'start') {
            //All this methods work as intended,
            //the previous 'start' node gets updated alongside the new one
            //in the nodesMatrix;
            newParent.appendChild(startNode);            
            nodeMatrix[prevNodeRow][prevNodeCol].isStart = false;
            nodeMatrix[newRow][newCol].isStart = true;

            //↓ If uncommented, this throws the following error
            //↓ as soon as we drag the start node:
            //(DOMException: Node.removeChild: The node to be removed is not a child of this node);

            /*
            setStartNodeRow(parseInt(newRow));
            setStartNodeCol(parseInt(newCol));
            */

        };
        
        if(draggedItem === 'end') {
            newParent.appendChild(endNode);
            nodeMatrix[prevNodeRow][prevNodeCol].isEnd = false;
            nodeMatrix[newRow][newCol].isEnd = true;

            /*
            setEndNodeRow(parseInt(newRow));
            setEndNodeCol(parseInt(newCol));
            */
        };
};

    return (
        <div id={`node-${ row }-${ col }`} onDragEnter={handleDragEnter} className={...} >
            { isStart && 
            <div onDragStart={handleDragStart} draggable={true} className={...} >
            <i className={...}/>
            </div>
            }
            { isEnd && 
            <div onDragStart={handleDragStart} draggable={true} className={...} >
            <i className={...}/>
            </div>
            }
        </div>
    )
}

export default Node

解决方案

问题根源

报错是因为手动操作DOM与React状态更新的冲突:
你通过newParent.appendChild(startNode)把start节点的DOM元素手动移到了新单元格,但调用setStartNodeRow/setStartNodeCol会触发Grid组件重新渲染。此时React会尝试移除旧单元格里的start节点DOM,但该节点已经被你移走了,React找不到目标节点,就抛出了Node.removeChild错误。

修复步骤

  1. 放弃手动DOM操作:React是状态驱动视图,所有DOM变化应该由状态更新触发,不要用appendChild这类原生DOM方法。
  2. 只维护状态作为唯一数据源:拖拽时仅更新nodeMatrix的节点状态和Grid的start/end行列状态,让React自动负责DOM渲染。
  3. 让React自动渲染节点:Node组件已经根据isStart/isEnd属性渲染对应节点,状态更新后React会自动把节点放到正确的单元格中。

修改后的关键代码

调整handleDragEnter函数,移除手动DOM操作,仅更新状态:

const handleDragEnter = (e) => {
    // 获取原节点行列
    const [prevNodeRow, prevNodeCol] = e.dataTransfer.getData('prevNodeId').match(/\d+/g);
    // 获取目标单元格
    const newParent = e.target.id ? e.target : e.target.parentElement.id ? e.target.parentElement : e.target.parentElement.parentElement;
    const [newRow, newCol] = newParent.id.match(/\d+/g);

    // 避免拖拽到原位置时重复更新
    if (prevNodeRow === newRow && prevNodeCol === newCol) return;

    if(draggedItem === 'start') {
        // 更新矩阵状态(注意:若nodeMatrix是不可变对象,需深拷贝后修改)
        nodeMatrix[prevNodeRow][prevNodeCol].isStart = false;
        nodeMatrix[newRow][newCol].isStart = true;
        // 更新Grid的start节点状态
        setStartNodeRow(parseInt(newRow));
        setStartNodeCol(parseInt(newCol));
    };
    
    if(draggedItem === 'end') {
        nodeMatrix[prevNodeRow][prevNodeCol].isEnd = false;
        nodeMatrix[newRow][newCol].isEnd = true;
        setEndNodeRow(parseInt(newRow));
        setEndNodeCol(parseInt(newCol));
    };
};

额外优化建议

  • 确保矩阵状态的不可变性:如果直接修改nodeMatrix的元素,React可能检测不到变化导致不渲染。建议创建矩阵的深拷贝再修改,或使用immer工具简化不可变更新。
  • 优化拖拽事件触发频率:可以在handleDragEnter中添加防抖逻辑,避免快速拖拽时频繁触发状态更新。

内容的提问来源于stack exchange,提问作者LeoM

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 22:06:25