6

The below snippet has four boxes. The purpose is that these boxes order will be shuffled and a transition animation occurs as they go to a new location. Each box's key corresponds with a color value from the source array in useState. Each update via the shuffle button, the source array's values are shuffled. Then I map through the array in the return function. I set 2 classNames for each box. One classname corresponds with the index and is for positioning. The other classname corresponds with the source array value and is always in unison with the key for that box.

My issue is that react seems to randomly be deciding what keys to pay attention to and reconcile, and what keys to disregard and just remount those elements. You can see here, some elements properly transition while others just jump to their target location. I'm at a loss as to why this is occuring. Can someone help?

EDIT: I don't believe this is a reconcile issue with respect to unwanted remounting. React is properly respecting the keys and not remounting any. So the issue is with how React handles CSS transition classes added during updates. Some transitions work and others don't. It may just be a limitation of the engine, but if anyone has any further incite please share.

const {useState} = React;

function App() {
  const [state, setState] = useState(['Red', 'Green', 'Blue', 'Black'])

  function handleShuffle() {
    const newState = _.shuffle(state)
    setState(newState)
  }

  return ( 
    <div className="App"> 
      {state.map((sourceValue, index) => {
        return ( 
          <div className={
            'box positionAt' + index + ' sourceValue' + sourceValue
          }
          key={sourceValue} ></div>
        )
      })}

      <button id="shuffle" onClick={handleShuffle}> shuffle < /button> 
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render( <
  App / > ,
  rootElement
);
.App {
  position: relative;
  width: 200px;
  height: 200px;
  background-color: gray;
}

.box {
  width: 25px;
  height: 25px;
  position: absolute;
  transition: transform 1s;
}

.positionAt0 {
  transform: translate(0px, 0px);
}

.positionAt1 {
  transform: translate(175px, 0px);
}

.positionAt2 {
  transform: translate(0px, 175px);
}

.positionAt3 {
  transform: translate(175px, 175px);
}

.sourceValueGreen {
  background-color: green;
}

.sourceValueBlue {
  background-color: blue;
}

.sourceValueRed {
  background-color: red;
}

.sourceValueBlack {
  background-color: black;
}

#shuffle {
  position: absolute;
  top: 0px;
  left: 75px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<div id="root"></div>
Jordy
  • 415
  • 5
  • 13
  • Just for the sake of info, it looks like things only transition when their new spot is a spot before their previous spot. If it's a spot after, then they don't transition. Also, I don't think this is an issue with map because I've tried implementing this without map and still have the same problem. – Jordy May 22 '20 at 19:07
  • I'm facing the same problem, the component go up will remain the transition, while the component go down will lose the transition – Quoc Van Tang Dec 02 '20 at 13:34

1 Answers1

2

Took me a while to figure it out, because setting the correct keys for the boxes seemed to be just the right thing.

I verified with the developer tools that the keys work, by inspecting a box and storing it in a variable (b = $0), shuffling, re-inspecting the box with the same key (color) and comparing it with the stored node ($0 === b, was true). So The DOM nodes for each key are stable.

But it's not sufficient for CSS transitions because the way browsers are changing the order of elements in the DOM.

You can see it here in a minimized example for efficiently reordering elements in the DOM (I assume that React does similar things internally when elements have to be reordered):

function reorder() {
    const list = document.querySelector("ul");
    list.appendChild(list.firstElementChild);
}
 <ul>
    <li>List-item #1</li>
    <li>List-item #2</li>
</ul>
<button onclick="reorder()">reorder!</button>

Run the example and set a DOM breakpoint on the resulting <ul> DOM node for "Subtree modification", see screenshot.

enter image description here

If you click on "reorder!", the browser breaks first on the removal of a <li>. If you continue, and immediately after continuing (Firefox: <F8>) the browser breaks again with an insertion of a <li>.

(In my tests, the information Chrome gave about the breaks was a bit misleading, Firefox was better at that)

So the browsers implement reordering technically as "remove and insert", which breaks CSS transitions.

With that knowledge the code can easily be fixed by having fixed order of the boxes in the DOM (The order in DOM doesn't need to be changed, because the position is only set via classes):

(Note: *HTML and CSS unchanged, changes in JavaScript are marked with NEW or CHANGE *)

const {useState} = React;

// NEW: List of boxes for a stable order when rendering to the DOM:
const boxes = ['Red', 'Green', 'Blue', 'Black'];

function App() {
  const [state, setState] = useState(boxes); // CHANGE: reuse boxes here

  function handleShuffle() {
    const newState = _.shuffle(state)
    setState(newState)
  }

  return ( 
    <div className="App"> 
      {/* CHANGE: loop over boxes, not state and lookup position, which is used for the positionAt... class */ 
       boxes.map((sourceValue, index) => {
         const position = state.indexOf(sourceValue);
         return ( 
           <div className={
             'box positionAt' + position + ' sourceValue' + sourceValue
           }
           key={sourceValue} ></div>
         )
      })}

      <button id="shuffle" onClick={handleShuffle}> shuffle < /button> 
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render( <
  App / > ,
  rootElement
);
.App {
  position: relative;
  width: 200px;
  height: 200px;
  background-color: gray;
}

.box {
  width: 25px;
  height: 25px;
  position: absolute;
  transition: transform 1s;
}

.positionAt0 {
  transform: translate(0px, 0px);
}

.positionAt1 {
  transform: translate(175px, 0px);
}

.positionAt2 {
  transform: translate(0px, 175px);
}

.positionAt3 {
  transform: translate(175px, 175px);
}

.sourceValueGreen {
  background-color: green;
}

.sourceValueBlue {
  background-color: blue;
}

.sourceValueRed {
  background-color: red;
}

.sourceValueBlack {
  background-color: black;
}

#shuffle {
  position: absolute;
  top: 0px;
  left: 75px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<div id="root"></div>

Note: it now works even if no key is set.

Sebastian B.
  • 2,141
  • 13
  • 21
  • If I could somehow donate rep to you I would. This is so significant for what I am trying to do with respect to an interactive image gallery. And I believe it will work beautifully! I can't wait to see how it performs. Thank you again. – Jordy May 25 '20 at 16:44