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> ); }
问题原因
- 交换逻辑错误:
swapScenes仅交换了两个场景对象的引用,未调整数组元素的实际位置。React基于key(场景id)识别DOM元素,会认为元素无需重新排序,仅更新内容,导致过渡动画失效。 - 状态更新冲突:
swapScenes连续调用两次setScenes,第二次handleStackScenes会覆盖第一次交换结果,反向操作时位置计算混乱。 - 拖拽判断逻辑缺陷:用拖拽的绝对位置
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
相关产品推荐
相关产品推荐

