React状态更新后特定元素CSS Transition未触发问题排查
React棋盘Tile动画异常:Key=2和Key=3的Tile无过渡效果
我正在开发一个React棋盘应用,通过CSS动画实现Tile移动,用React State跟踪Tile位置。调用transition函数更新位置后,其他Tile都能平滑过渡,但key=2和key=3的Tile直接跳转,没有动画,尽管状态已经正确更新。
代码实现
App.tsx
import './index.css'; import { useState, Fragment } from 'react'; function bcls(...parts) { return parts.filter(Boolean).join(' '); } const initState = [ { x: 0, y: 0, key: 1, moved: false }, { x: 2, y: 0, key: 2, moved: false }, { x: 1, y: 1, key: 3, moved: false }, { x: 2, y: 1, key: 4, moved: false }, { x: 3, y: 2, key: 5, moved: false }, ]; const transitionState = [ { x: 0, y: 3, key: 1, moved: true }, { x: 2, y: 3, key: 4, moved: true }, { x: 3, y: 3, key: 5, moved: true }, { x: 2, y: 2, key: 2, moved: true }, { x: 1, y: 3, key: 3, moved: true }, ]; export default function App() { const [tiles, setTiles] = useState(initState); function transition() { setTiles(transitionState); } function reset() { setTiles(initState); } return ( <Fragment> <button onClick={transition}>transition</button> <button onClick={reset}>reset</button> <div className="board"> {Array.from({ length: 16 }).map((_, i) => ( <div key={i} className="cell"></div> ))} {tiles.map((tile) => ( <div key={tile.key} className={bcls('tile', tile.moved && 'tile-moved')} style={{ '--x': tile.x, '--y': tile.y }} > {tile.key} </div> ))} </div> </Fragment> ); }
index.css
.board { --grid: 4; --size: 50px; --gap: 12px; background-color: #8f8b8b; padding: var(--gap); position: relative; display: grid; gap: var(--gap); grid-template-columns: repeat(var(--grid), var(--size)); grid-template-rows: repeat(var(--grid), var(--size)); } .cell { background-color: #aaa; } .tile { background-color: #afe544; width: var(--size); height: var(--size); display: flex; justify-content: center; align-items: center; position: absolute; top: calc(var(--y) * (var(--size) + var(--gap)) + var(--gap)); left: calc(var(--x) * (var(--size) + var(--gap)) + var(--gap)); } .tile-moved { transition: 4000ms ease-in-out; }
main.tsx
import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.tsx'; ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <App /> </React.StrictMode> );
问题原因
核心问题是React diff算法与CSS过渡的时机不匹配:
- 初始状态下Tile数组顺序是
[1,2,3,4,5],过渡状态顺序变为[1,4,5,2,3] - React根据key匹配DOM节点时,key=2和key=3的节点在数组中的位置靠后,React会同时更新这些节点的
style(位置)和className(添加tile-moved) - CSS过渡需要捕捉到样式变化的前后状态,但由于样式更新和过渡类添加同时发生,浏览器直接渲染了最终位置,没有触发过渡动画
解决方案
方案一:简化CSS,默认启用过渡
把过渡样式直接加到.tile类中,不需要依赖tile-moved类,这样只要位置变化就会触发动画:
.tile { background-color: #afe544; width: var(--size); height: var(--size); display: flex; justify-content: center; align-items: center; position: absolute; top: calc(var(--y) * (var(--size) + var(--gap)) + var(--gap)); left: calc(var(--x) * (var(--size) + var(--gap)) + var(--gap)); /* 直接添加过渡 */ transition: top 4000ms ease-in-out, left 4000ms ease-in-out; } /* 移除tile-moved类 */
同时可以简化State,去掉moved字段。
方案二:分两步更新State,确保过渡类先生效
如果需要保留moved状态控制动画,可以分两步更新:先给所有Tile加上moved: true,再延迟更新位置,让浏览器先渲染过渡类,再捕捉位置变化:
function transition() { // 第一步:先添加动画类,保持位置不变 setTiles(prev => prev.map(tile => ({ ...tile, moved: true }))); // 第二步:延迟更新到目标位置,确保过渡类已生效 setTimeout(() => { setTiles(transitionState); }, 0); }
这里的setTimeout是让React完成第一次渲染后再更新位置,确保CSS能捕捉到样式变化的起始状态。
内容的提问来源于stack exchange,提问作者its9527
相关产品推荐
相关产品推荐

