删除React Draggable组件时避免位置跳动 保持已拖拽笔记位置
问题场景
- 基于React实现便签功能,通过
notes.map()渲染Note组件,初始组件定位为position: static,借助react-draggablenpm模块实现笔记拖拽能力 - 预期目标:删除任意一条笔记时,用户已经拖拽调整过位置的笔记保持原有位置不变
- 现有方案bug:尝试给拖拽过的笔记设置
position: absolute使其脱离文档流时,首次拖拽会出现位置跳动,具体现象:- 初始状态:

- 首次触发拖拽时,笔记直接跳动到其他笔记上层:

- 首次跳动完成后,后续拖拽操作可正常进行:

- 初始状态:
原有代码实现
Note组件代码
function Note(props) { const [dragDisabled, setDragDisabled] = useState(true); const [beenDragged, setBeenDragged] = useState(props.beenDragged); const [position, setPosition] = useState({ xPos: props.xPos, yPos: props.yPos }); // Drag Note Functions function handleClick() { setDragDisabled(prevValue => { return !prevValue; }) } function firstDrag(event) { if (!beenDragged) { axios.post("api/note/beenDragged", {id: props.id}) .then(setBeenDragged(true)); } } function finishDrag(event, data) { setPosition({ xPos: data.x, yPos: data.y }); } useEffect(() => { axios.post("/api/note/updateposition", {position, id: props.id }); }, [position]); return <Draggable disabled={dragDisabled} onStart={firstDrag} onStop={finishDrag} defaultPosition={{ x: props.xPos, y: props.yPos }} // position={location} > <div className='note' style={{position: beenDragged ? 'absolute' : 'static'}}> <h1>{props.title}</h1> <p>{props.content}</p> <button onClick={handleClick}> {dragDisabled ? <LockIcon /> : <LockOpenIcon />} </button> <EditPopup title={props.title} content={props.content} editNote={editNote} /> <DeletePopup deleteNote={deleteNote} /> </div> </Draggable> }
样式代码
.note { background: #fff; /* background-image: url("https://www.transparenttextures.com/patterns/notebook-dark.png"); */ background-image: url("https://www.transparenttextures.com/patterns/lined-paper-2.png"); border-radius: 7px; box-shadow: 0 2px 5px rgb(120, 150, 179); padding: 10px; width: 240px; margin: 16px; float: left; }
App组件渲染逻辑
function App() { const [notes, setNotes] = useState([]); return ( <div id="bootstrap-override"> {notes.map((note) => { return <Note key={note._id} id={note._id} title={note.title} content={note.content} xPos={note.xPos} yPos={note.yPos} beenDragged={note.beenDragged} deleteNote={deleteNote} editNote={editNote} /> })} </div>); }
问题原因
跳动的核心原因是定位模式切换的坐标基准不匹配:
- 首次拖拽触发时才将元素从
position: static切换为absolute,两种定位模式下的坐标参考规则完全不同:static属于正常浮动文档流,位置受同级元素影响;absolute相对于最近的定位父元素偏移,脱离文档流不受同级元素影响。 - 切换定位的瞬间没有提前计算元素在文档流中的原始位置并转换为absolute对应的偏移值,
react-draggable的defaultPosition仅在组件首次挂载时生效,定位切换后不会重新校正坐标,直接叠加transform偏移就会出现位置跳变。
修复方案
核心思路:全程不切换定位模式,所有笔记统一使用position: absolute,未拖拽过的笔记挂载时自动计算其在正常文档流的位置作为初始absolute坐标,既避免定位切换导致的跳变,又能保证已拖拽笔记的位置不受文档流变动(比如删除其他笔记)影响。
步骤1:修改CSS样式
给笔记容器加相对定位作为坐标参考,去掉笔记的浮动属性,统一设置绝对定位:
#bootstrap-override { position: relative; /* 作为绝对定位笔记的坐标参考父容器 */ min-height: 100vh; } .note { background: #fff; background-image: url("https://www.transparenttextures.com/patterns/lined-paper-2.png"); border-radius: 7px; box-shadow: 0 2px 5px rgb(120, 150, 179); padding: 10px; width: 240px; margin: 16px; /* 移除float: left,统一绝对定位 */ position: absolute; }
步骤2:重构Note组件逻辑
- 去掉动态切换position的行内样式
- 用受控
position属性替换defaultPosition,保证坐标和组件状态实时同步 - 未拖拽过的笔记挂载时先测量其在文档流的初始位置,转换为绝对定位坐标后再渲染可拖拽组件,避免偏移
import { useState, useEffect, useRef } from 'react'; import Draggable from 'react-draggable'; function Note(props) { const [dragDisabled, setDragDisabled] = useState(true); const [beenDragged, setBeenDragged] = useState(props.beenDragged); const noteRef = useRef(null); // 已拖拽过的笔记直接读取存储的坐标,未拖拽的初始为null,挂载后计算 const [position, setPosition] = useState( props.beenDragged ? { x: props.xPos, y: props.yPos } : null ); // 计算未拖拽笔记的初始文档流位置,转为绝对定位坐标 useEffect(() => { if (!beenDragged && noteRef.current) { const noteRect = noteRef.current.getBoundingClientRect(); const parentRect = noteRef.current.offsetParent.getBoundingClientRect(); const initX = noteRect.left - parentRect.left; const initY = noteRect.top - parentRect.top; setPosition({ x: initX, y: initY }); // 同步初始坐标到后端,下次加载直接使用 axios.post("/api/note/updateposition", { position: { xPos: initX, yPos: initY }, id: props.id }); } }, [beenDragged]); // 切换拖拽锁定状态 function handleClick() { setDragDisabled(prev => !prev); } // 首次拖拽标记 function firstDrag() { if (!beenDragged) { axios.post("api/note/beenDragged", {id: props.id}) .then(() => setBeenDragged(true)); } } // 拖拽结束保存坐标 function finishDrag(event, data) { setPosition({ x: data.x, y: data.y }); } // 坐标变化时同步到后端 useEffect(() => { if (position) { axios.post("/api/note/updateposition", { position: { xPos: position.x, yPos: position.y }, id: props.id }); } }, [position]); // 初始位置未计算完成时,渲染隐藏占位元素测量位置,避免可见的跳动 if (!position) { return ( <div ref={noteRef} className='note' style={{ visibility: 'hidden' }}> <h1>{props.title}</h1> <p>{props.content}</p> </div> ) } return ( <Draggable disabled={dragDisabled} onStart={firstDrag} onStop={finishDrag} position={position} > <div ref={noteRef} className='note'> <h1>{props.title}</h1> <p>{props.content}</p> <button onClick={handleClick}> {dragDisabled ? <LockIcon /> : <LockOpenIcon />} </button> <EditPopup title={props.title} content={props.content} editNote={props.editNote} /> <DeletePopup deleteNote={props.deleteNote} /> </div> </Draggable> ) }
可选优化
如果需要保留未拖拽笔记的自动流式排列效果,只需要在新增/删除笔记后,遍历所有beenDragged=false的笔记,重新触发一次初始位置计算即可,已经拖拽过的笔记坐标不会被修改,位置保持不变。
内容的提问来源于stack exchange,提问作者Aaron Lam
相关产品推荐
相关产品推荐

