I'm working on a simple React application that involves moving tiles on a board. The application uses CSS for animations and React state to track the tiles' positions. I have a transition function that updates the tiles' positions, expecting each tile to animate to its new location. However, I've noticed that the animations for tiles with key=2 and key=3 do not trigger after calling the transition function, despite the state updating correctly and other tiles animating as expected.
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">
<div className="cell"></div>
<div className="cell"></div>
<div className="cell"></div>
<div className="cell"></div>
<div className="cell"></div>
<div className="cell"></div>
<div className="cell"></div>
<div className="cell"></div>
<div className="cell"></div>
<div className="cell"></div>
<div className="cell"></div>
<div className="cell"></div>
<div className="cell"></div>
<div className="cell"></div>
<div className="cell"></div>
<div 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>
);
Here's a StackBlitz Demo.
I expected each tile to smoothly transition to its new position when the transition function is called. This works for all tiles except those with key=2 and key=3. For these two tiles, they jump to their new position without any animation.
This basically seems like a React key and array rendering issue. The
tileselements with keys2and3are moved to the end of thetilesarray and so React unmounts them from their previous array positions, indices 1 and 2, and remounts them at the current indices, 3 and 4.Keeping the same
tileselement order keeps the React keys in the same order and the tile elements mounted. Only the (x, y ) "coordinate" of each tile element should update.Update the
transitionStatevalue fromto