如何为React列表项上下移动添加translateY过渡动画?
列表项上下移动动画的最佳实现方案
问题分析
你当前的实现直接通过交换数组元素触发React重渲染,但存在两个核心问题:
- 用列表项内容作为
key,交换后React会复用原有DOM节点,无法触发过渡动画; - 没有给元素添加动画触发的状态标记,无法精准控制动画的触发时机和方向。
实现步骤
- 给列表项添加唯一标识:避免用内容作为
key,改用唯一ID确保React能正确识别DOM节点的变化。 - 跟踪移动状态:维护一个状态记录正在移动的元素ID和方向,用于给对应元素添加动画类。
- 控制动画生命周期:在元素交换完成后,延迟移除动画类,确保动画完整执行。
- 精准计算动画位移:结合列表项的高度和间距,计算正确的
translateY值,避免定位偏差。
修改后的完整代码
React 代码
const { useState } = React; // 给每个item添加唯一ID,避免用内容当key const ITEMS = [ { id: 1, text: "item 1" }, { id: 2, text: "item 2" }, { id: 3, text: "item 3" }, { id: 4, text: "item 4" } ]; const App = () => { const [items, setItems] = useState(ITEMS); // 记录正在移动的元素:{ id: number, direction: 'up' | 'down' } const [movingItem, setMovingItem] = useState(null); const handleMoveItem = (index, direction) => { const updatedItems = [...items]; const targetIndex = direction === "up" ? index - 1 : index + 1; if (targetIndex >= 0 && targetIndex < items.length) { // 标记当前移动的元素和方向 setMovingItem({ id: updatedItems[index].id, direction }); // 交换元素 [updatedItems[index], updatedItems[targetIndex]] = [ updatedItems[targetIndex], updatedItems[index] ]; setItems(updatedItems); // 动画结束后清除移动状态 setTimeout(() => setMovingItem(null), 300); } }; return ( <div className="wrapper"> <ul> {items.map((item, index) => { // 判断当前元素是否正在移动,添加对应动画类 const isMoving = movingItem?.id === item.id; const animationClass = isMoving ? `move-${movingItem.direction}` : ""; return ( <li key={item.id} className={`list-item ${animationClass}`}> {item.text} <div> <button onClick={() => handleMoveItem(index, "up")} disabled={index === 0} > up </button> <button onClick={() => handleMoveItem(index, "down")} disabled={index === items.length - 1} > down </button> </div> </li> ); })} </ul> </div> ); }; ReactDOM.render(<App />, document.getElementById("root"));
CSS 代码
:root { --item-gap: 1rem; --item-height: 60px; } .wrapper { min-height: 100vh; display: flex; align-items: center; justify-content: center; } ul { list-style: none; width: 500px; margin: 0 auto; display: flex; flex-direction: column; gap: var(--item-gap); } .list-item { text-align: center; display: flex; justify-content: space-between; align-items: center; padding: 0 1rem; background-color: lightblue; height: var(--item-height); transition: transform 0.3s ease; } /* 计算位移:元素高度 + 间距 */ .move-up { transform: translateY(calc(-1 * (var(--item-height) + var(--item-gap)))); } .move-down { transform: translateY(calc(var(--item-height) + var(--item-gap))); } button { margin-left: 0.5rem; padding: 0.3rem 0.6rem; cursor: pointer; } button:disabled { cursor: not-allowed; opacity: 0.5; }
HTML 代码
<div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>
关键说明
- 唯一ID作为key:确保React在元素交换时能正确识别DOM节点变化,触发过渡动画;
- 移动状态跟踪:通过
movingItem状态精准控制动画类的添加和移除,确保动画只在移动时触发; - CSS变量计算位移:统一管理元素高度和间距,避免硬编码数值导致的定位偏差;
- 禁用边界按钮:优化交互体验,避免无效点击。
内容的提问来源于stack exchange,提问作者Adrian
相关产品推荐
相关产品推荐

