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

react-rnd组件拖拽交换位置时过渡效果异常问题排查

问题描述

使用react-rnd的<Rnd/>组件渲染两个场景元素,期望拖拽其中一个靠近另一个时,列表与UI均交换位置(如[first, second]变为[second, first]),且<Rnd/>位置为受控状态。但当前代码存在问题:拖拽左侧元素到右侧时交换正常,反向拖拽时交换虽触发但无过渡效果,控制台显示反向操作时DOM元素未交换,仅内容发生变化。

代码示例:

import { useState } from "react";
import "./styles.css";
import { Rnd, Position, DraggableData } from "react-rnd";

interface IScene {
  id: string;
  name: string;
  position: Position;
}

function Scene({
  scene,
  activeScene,
  setActiveScene,
  onDrag
}: {
  scene: IScene;
  activeScene: string;
  setActiveScene: (id: string) => void;
  onDrag: (d: DraggableData) => void;
}) {
  const [dragged, setDragged] = useState(false);

  return (
    <Rnd
      default={{
        x: 0,
        y: 0,
        width: "200px",
        height: "100px"
      }}
      position={scene.position}
      onDragStart={() => setDragged(true)}
      onDragStop={() => setDragged(false)}
      onDrag={(_, d) => onDrag(d)}
      onMouseDown={() => setActiveScene(scene.id)}
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        backgroundColor: "gray",
        transition: dragged ? "" : "transform 0.5s",
        border: "1px solid",
        borderColor: activeScene === scene.id ? "white" : "gray",
        zIndex: activeScene === scene.id ? "1" : "0"
      }}
    >
      {scene.name}
    </Rnd>
  );
}

const initialScenesState = [
  {
    id: "1",
    name: "first",
    position: {
      x: 0,
      y: 0
    }
  },
  {
    id: "2",
    name: "second",
    position: {
      x: 200,
      y: 0
    }
  }
];

export default function App() {
  const [scenes, setScenes] = useState<IScene[]>(initialScenesState);
  const [activeScene, setActiveScene] = useState<string>("");

  const handleStackScenes = () => {
    setScenes((scenes) => {
      let currentPosition = 0;
      return scenes.map((scene) => {
        const result = {
          ...scene,
          position: {
            x: currentPosition,
            y: 0
          }
        };
        currentPosition += 200;
        return result;
      });
    });
  };

  const swapScenes = (first: IScene, second: IScene) => {
    setScenes((scenes) => {
      return scenes.map((scene) => {
        if (scene.id === first.id) {
          return second;
        } else if (scene.id === second.id) {
          return first;
        } else return scene;
      });
    });
    handleStackScenes();
  };

  const handleDrag = (scene: IScene) => (d: DraggableData) => {
    console.log(d.x);
    for (let i = 0; i < scenes.length; i++) {
      if (
        Math.abs(scenes[i].position.x - d.x) < 30 &&
        scenes[i].id !== scene.id
      ) {
        swapScenes(scene, scenes[i]);
      }
    }
  };

  console.log(scenes);

  return (
    <div className="App">
      {scenes.map((scene) => (
        <Scene
          key={scene.id}
          scene={scene}
          activeScene={activeScene}
          setActiveScene={setActiveScene}
          onDrag={handleDrag(scene)}
        />
      ))}
    </div>
  );
}
问题原因
  1. 交换逻辑错误:swapScenes仅交换了两个场景对象的引用,未调整数组元素的实际位置。React基于key(场景id)识别DOM元素,会认为元素无需重新排序,仅更新内容,导致过渡动画失效。
  2. 状态更新冲突:swapScenes连续调用两次setScenes,第二次handleStackScenes会覆盖第一次交换结果,反向操作时位置计算混乱。
  3. 拖拽判断逻辑缺陷:用拖拽的绝对位置d.x与目标元素位置比较,反向拖拽时易触发多次交换,打断过渡动画。
修复方案

1. 正确调整数组元素位置

修改swapScenes,直接调整数组索引位置,确保React识别DOM顺序变化:

const swapScenes = (firstId: string, secondId: string) => {
  setScenes(prevScenes => {
    const firstIndex = prevScenes.findIndex(s => s.id === firstId);
    const secondIndex = prevScenes.findIndex(s => s.id === secondId);
    // 交换数组元素位置
    const newScenes = [...prevScenes];
    [newScenes[firstIndex], newScenes[secondIndex]] = [newScenes[secondIndex], newScenes[firstIndex]];
    // 直接设置交换后的正确位置
    let currentPosition = 0;
    return newScenes.map(scene => ({
      ...scene,
      position: { x: currentPosition, y: 0 }
    }));
  });
};

2. 优化拖拽位置判断

基于元素中心位置判断交换时机,避免频繁触发:

const handleDrag = (scene: IScene) => (_, d: DraggableData) => {
  const draggedCenterX = d.x + 100; // 元素宽度200,取中心偏移
  for (const targetScene of scenes) {
    if (targetScene.id === scene.id) continue;
    const targetCenterX = targetScene.position.x + 100;
    // 中心重叠时触发交换,避免重复触发
    if (Math.abs(draggedCenterX - targetCenterX) < 50) {
      swapScenes(scene.id, targetScene.id);
      break;
    }
  }
};

3. 修复过渡动画触发条件

确保拖拽时禁用过渡,非拖拽时启用:

// Scene组件style修改
style={{
  display: "flex",
  justifyContent: "center",
  alignItems: "center",
  backgroundColor: "gray",
  transition: dragged ? "none" : "transform 0.5s",
  border: "1px solid",
  borderColor: activeScene === scene.id ? "white" : "gray",
  zIndex: activeScene === scene.id ? "1" : "0"
}}

4. 移除冗余状态更新

swapScenes已包含位置设置,删除单独的handleStackScenes调用,避免状态冲突。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 02:50:27