React CSS Transition Not Triggering for Specific Elements After State Update

32 Views Asked by At

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.

1

There are 1 best solutions below

0
Drew Reese On BEST ANSWER

This basically seems like a React key and array rendering issue. The tiles elements with keys 2 and 3 are moved to the end of the tiles array 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 tiles element 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 transitionState value from

// Array elements and keys reordered
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 },
];

to

// Array elements and key order maintained
const transitionState = [
  { x: 0, y: 3, key: 1, moved: true },
  { x: 2, y: 2, key: 2, moved: true },
  { x: 1, y: 3, key: 3, moved: true },
  { x: 2, y: 3, key: 4, moved: true },
  { x: 3, y: 3, key: 5, moved: true },
];

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: 2, key: 2, moved: true },
  { x: 1, y: 3, key: 3, moved: true },
  { x: 2, y: 3, key: 4, moved: true },
  { x: 3, y: 3, key: 5, moved: true },
];

function App() {
  const [tiles, setTiles] = React.useState(initState);

  function transition() {
    setTiles(transitionState);
  }

  function reset() {
    setTiles(initState);
  }

  return (
    <React.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>
    </React.Fragment>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
.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;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root" />