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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 02:49:53