React网格布局拖拽问题:状态更新引发重渲染中断执行
解决React拖拽时状态更新阻止后续代码执行并保留GhostWidget的方案
问题根源
你原来的代码在handleDragStart中直接修改widgetState,会触发组件立即重渲染——这不仅可能导致拖拽元素的DOM上下文丢失,打断后续拖拽事件流程,还会因为频繁更新全量组件状态造成性能浪费。
优化方案:拆分状态+计算式渲染GhostWidget
核心思路是把拖拽状态(当前拖拽组件、Ghost位置)和业务数据(widget列表)拆分开,拖拽过程中只更新拖拽状态,GhostWidget通过计算生成而不是直接修改业务数据数组,直到Drop时才真正更新业务状态。
1. 调整状态定义
import { useState, useMemo } from 'react'; // 拖拽相关状态,独立于业务数据 const [draggingWidget, setDraggingWidget] = useState<IWidget | null>(null); const [ghostPosition, setGhostPosition] = useState<number | null>(null); // 原业务状态保持不变 const [widgetState, setWidgetState] = useState<IWidget[]>([]);
2. 重写handleDragStart
只记录拖拽目标和初始Ghost位置,不修改业务数据:
const handleDragStart = (widget: IWidget): void => { setDraggingWidget(widget); // 初始Ghost位置设为当前组件的索引 const widgetIndex = widgetState.findIndex(item => item.id === widget.id); setGhostPosition(widgetIndex); };
3. 计算渲染用的组件列表
通过useMemo实时计算要渲染的列表,自动插入GhostWidget:
const displayWidgets = useMemo(() => { if (!draggingWidget || ghostPosition === null) { return widgetState; } // 过滤掉正在拖拽的组件,在Ghost位置插入占位符 const filteredWidgets = widgetState.filter(item => item.id !== draggingWidget.id); const temp = [...filteredWidgets]; temp.splice(ghostPosition, 0, { id: -1, name: 'ghostWidget', elements: [], orderPriority: ghostPosition }); return temp; }, [widgetState, draggingWidget, ghostPosition]);
4. 新增handleDragOver控制Ghost位置
拖拽过程中实时更新Ghost的位置:
const handleDragOver = (e: React.DragEvent<HTMLDivElement>, targetIndex: number) => { e.preventDefault(); // 必须调用,否则Drop事件不会触发 // 仅当目标位置变化时更新,避免不必要的重渲染 if (ghostPosition !== targetIndex) { setGhostPosition(targetIndex); } };
5. 调整handleDrop逻辑
Drop时再真正修改业务数据:
const handleDrop = (e: React.DragEvent<HTMLDivElement>): void => { e.preventDefault(); if (!draggingWidget || ghostPosition === null) return; // 生成最终的组件数组 const updatedWidgets = [...widgetState.filter(item => item.id !== draggingWidget.id)]; updatedWidgets.splice(ghostPosition, 0, draggingWidget); // 更新排序优先级 updatedWidgets.forEach((item, index) => { item.orderPriority = index; }); // 更新业务状态并同步到后端 setWidgetState(updatedWidgets); dispatch(editMultipleWidgetsSagaAction(updatedWidgets)); // 重置拖拽状态 setDraggingWidget(null); setGhostPosition(null); };
6. 渲染时使用计算后的列表
在网格渲染部分用displayWidgets替代原widgetState:
<div className="grid-container"> {displayWidgets.map((widget, index) => ( <div key={widget.id} className={widget.id === -1 ? 'ghost-widget' : 'widget-item'} draggable={widget.id !== -1} onDragStart={() => handleDragStart(widget)} onDragOver={(e) => handleDragOver(e, index)} onDrop={handleDrop} > {/* 组件内容 */} </div> ))} </div>
方案优势
- 避免拖拽过程中频繁修改业务状态导致的异常重渲染,保证拖拽事件流程完整
- GhostWidget的位置实时更新,预览效果不受影响
- 仅在Drop时更新业务数据,符合数据更新的最小化原则,性能更优
内容的提问来源于stack exchange,提问作者Nikolina
相关产品推荐
相关产品推荐

