Search code examples
reactjsanimationkeytransition

React Collapse transition doesn't animate when wrapped in a div with a dynamic key


This is really weird. It took me many hours to figure out how to fix this. But even with it fixed, I don't know why it breaks in one configuration but not the other.

The best way to explain this is with the StackBlitz live example: https://stackblitz.com/edit/react-collapse-transition-breaks-with-dynamic-key

The live example details the exact issue, but here's the synopsis:

  • It's a React app using Material UI.
  • I have a <List> that is populated with an array of <ListItem>s.
  • Those <ListItem>s are clickable. When you click them, it uses the <Collapse> transition to expose a sub-<List> of "subheaders". The subheader <List>s are also populated with an array of <ListItem>s.
  • Here's where it gets weird: I have a simple <div> that holds the "header" <ListItem>s and the <Collapse>-ible <List> of "subheaders".
  • Because these are part of an array, React complains if I don't add a "key" attribute to top-level element (the <div>).
  • If I add a dynamically-generated key value to that containing <div>, it somehow kills the transition animation on the <Collapse> element. The <Collapse> still opens-and-closes, but it doesn't animate over a set number of milliseconds. It just opens (immediately) or closes (immediately).
  • If I add a static key value to that same containing <div>, the animation works just fine.

Why does the dynamic-key approach break the animation??


Solution

  • TLDR: Do not dynamically-generate globally-unique keys for array elements in React.

    OK, after repeated attempts at Googling, I think I finally understand what's going on.

    When you add items to an array in React, a warning is thrown if you don't add a unique "key" to each element in the array. As long as you use something unique as the "key" value for each element, the warning goes away and, for the most part, React seems to manage the array elements just fine.

    When I was starting React development (a few years ago), I thought, "I can solve this easily by using a random GUID-generating function to add unique keys to all my array elements. So I would frequently use code that looks like this:

    let newArray = [];
    someMasterArrayOfObjects.forEach(object => {
       if (someConditionIsMet) {
          // SEE HOW CLEVER I THOUGHT I WAS??  USING A RANDOMLY-GENERATED GUID QUIETS THE 
          // UNIQUE-KEY WARNINGS THAT ARE THROWN BY REACT
          newArray.push(
             <div key={createRandomGuid()}>
                {object.title}
             </div>
          );
       }
    });
    

    But I missed one key word from the ReactJS documentation (emphasis, mine):

    Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a STABLE identity:

    They use the word "stable" a few times in the documentation, but it didn't really sink into my thick skull. Like most devs, my first concern was removing the warning that is thrown when array elements are missing a unique key. And using a randomly-generated GUID solved that problem.

    But React uses those keys to figure out what should be re-rendered during state changes. If you randomly-generate a new, globally-unique key every time you build the elements in the array, then React will assume that all of these elements need to be completely rebuilt from scratch every time you set state.

    At the very least, this is inefficient. You may not notice any performance effects in small arrays/apps, but there's no reason to artificially force a re-rendering of every array element every time that state is set for any reason. You may not notice any visual problem in your app, but it's poor practice.

    At the worst, it will actually break some of your functionality. In my case, it was breaking the <Collapse> transition animation because the rendering engine couldn't compare the incremented "height" value between one state change and the next - because on every attempt to change that height, I was assigning a brand new, globally-unique identifier to the "key" value and React was seeing it as an entirely new, entirely different component.

    Once I finally figured out what to Google, I also found this great JSFiddle:

    http://jsfiddle.net/frosas/S4Dju/

    Notice that the center row of inputs in his example are titled "Unique random keys". When you manually change the value of one of the those unique random keys, then you click on "Add item", it blows away the results of your changes, because it re-renders the input elements as brand-new elements with no ties to their previous state.