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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 16:03:25