React animations for a single component

React is a great framework for building web UIs. (And perhaps other UIs as well) I’ve recently started using it in some side projects, and love its ability to easily manage view state and efficiently update the DOM, reducing a lot of the “grunt work” of building dynamic web UIs. On the other hand, it feels relatively lightweight in that it doesn’t impose a lot of structure in how you design your app. This may be a Good or a Bad thing, but the upside is that there isn’t a steep learning curve.

One thing I recently wanted to do with React was to build a component that would animate during its transition from state A to B; an example would be an On/Off button. It is possible to do so with React’s built-in animation addon, but it just required some attention to detail.

Animated Reaction

First, a little background: Animation in React is fairly well-documented, and is usually accomplished with the React.addons.CSSTransitionGroup addon. This addon provides a relatively easy way to hook in CSS transitions when adding or removing child components from a parent component.

However, the most common way of doing animations appears to be geared towards adding and removing child components. For example: add a component and fade it in; remove a component and fade it out. But what if we want to update a single child component and animate its transition from one state to another?

Button example

I’ll use a simple example, available as a JSFiddle here:

Clicking the button simply toggles between “On” and “Off” states. What if we want to animate this UI transition using React? Doing so using jQuery would be trivial using the .fadeIn()/.fadeOut() methods, or perhaps even .animate() for something custom. But since we’re already using React, sticking with it is the best solution.

The React official documentation hints at this with the section, “Animating One or Zero Items“.

Back to basics

To understand the solution, it’s important to understand how React’s React.addons.CSSTransitionGroup addon works. When a new child component is added to a CSSTransitionGroup component, the following happens:

  1. Child component rendered with CSS class of *-enter
  2. A very short time later (on the next tick, presumably with window.setTimeout()), the CSS class *-enter-active is added. This should trigger a transitionend event after the CSS transition completes.
  3. After the transitionend event fires, React removes both the *-enter and *-enter-active classes from the DOM element.

Conversely, when a child component is removed, the following happens:

  1. Child component has the CSS class *-leave added.
  2. On the next tick, the CSS class *-leave-active is added.
  3. When transitionend fires, the element is removed from the DOM.

The complexity here is due to the fact that CSS transitions only happen when the specified properties change; this is why the first CSS class (*-enter or *-leave) is added first, and then the second (*-enter-active or *-leave-active) is added a short time later. The addition of the second CSS class should trigger the CSS transition, and thus eventually fire the transitionend event. This is especially critical when removing child components as React relies on that event to actually remove the element from the DOM.

(If you don’t trigger the transitionend event, you’ll see an error like this in your JavaScript console if you’re using a non-minified version of React: “transition(): tried to perform an animation without an animationend or transitionend event after timeout (5000ms). You should either disable this transition in JS or add a CSS animation/transition.“)

The key is using key

If you read the section on Multiple Components, you’ll remember that React relies on the key attribute to uniquely identify child components; this is how React knows whether a component is being added, removed or updated.

Currently, ReactCSSTransitionGroup can only animate components that are being added or removed, not those being updated. Thus, if we want to simulate transition between two UI states on a single component, we’ll have to ensure the transition looks like one component being added and another being removed.

We can accomplish by this by linking the UI state of the component to its key attribute. Check out the updated implementation (via JSFiddle), complete with animations using CSS transitions:

The important part is in the render() method:

render: function() {
  var text = this.state.on ? "ON" : "OFF";
  var className = this.state.on ? "on" : "";
  className += " button"
  
  var key = text;
  return (
    <ReactCSSTransitionGroup transitionName="switch">
      <div key={key} className={className} onClick={this.toggleOnOff}>{text}</div>
    </ReactCSSTransitionGroup>
  );
}

By setting the key attribute to something that changes with the UI state, we’re effectively signaling to React that a component should be removed and then added on a state change. This sets us up for the animations.

The important part of the CSS looks like this:

.button.switch-enter {
    opacity: 0.01;
}
.button.switch-enter.switch-enter-active {
    opacity: 1.0;
}
.button.switch-leave {
    /* Completely hide the button while it's being animated and before it's removed from the DOM. */
    visibility: hidden;
    height: 0px;
    width: 0px;
    
    /* Starting opacity */
    opacity: 1.0;
}
.button.switch-leave.switch-leave-active {
    /* Ending opacity: 
    Trigger opacity change so the "transitionend" event will fire, causing React to remove from the DOM. */
    opacity: 0;
}

In this simple example, I merely want the new state of the button to fade in via a transition/change in the CSS opacity property. That is accomplished by the .button.switch-enter and .button.switch-enter.switch-enter-active selectors.

However, when a component is removed, I want it gone immediately so that it’s not visible while the new component is fading in. This is appropriate for our example here, because the “On” and “Off” UI states of the button are exclusive and should not be shown at the same time. The above approach simple hides the component, triggers a CSS transition and then waits for React to remove it from the DOM. The reasons for this roundabout way are:

  1. Simply setting display: none doesn’t work because CSS transitions don’t work for this.[1][2] Without a CSS transition, there is no transitionend event and thus React won’t be signaled remove it from the DOM.
  2. Merely doing a really fast transition results in a “flash” of the component (where both states are visible), something we don’t want.

Note that in my example, the transition is done on the opacity property, but it could just as easily have been done on another.

Conclusion

React is a great framework, but sometimes things that are simple using another approach (i.e. jQuery) require a different approach so as to do things the React way and avoid any complications.

References

  1. http://stackoverflow.com/questions/3331353/transitions-on-the-display-property
  2. http://stackoverflow.com/questions/22103006/css3-transition-doesnt-work-with-display-property

12 Comments »

  1. This is a nice hack, but I hope eventually React adds some kind of support for Angular-style class add and remove animations, because in Angular sometimes I have enter and leave animations on multiple elements AND animations when a class is added to or removed from a single one of those elements.

  2. @Andy – I haven’t worked with Angular, but I agree with you that this is a lot of effort for what seems like a simple piece of functionality. If there’s an easier way, I’d sure like to know of it!

  3. We’d just been wrangling with this for nearly an hour in the office. Thank you!

  4. Manipulating the key attribute to make React think an entirely new object is being rendered causes the button to flash off the screen. For this case I would find that undesirable. Is there a method we can use to make one state smoothly transition to the other?

  5. Disgusting, this is a step back from ngAnimate in almost every way.

  6. This hack only works if you don’t care about the “update” animation being exactly the same as “leave” + “enter”. Not going to work in the majority of cases.

  7. I created a component to make animation on change a bit easier: react-animate-on-change

  8. Excellent! This got me out of a situation.

    Although it shouldn’t be this hard to do animation in a UI library 😉

  9. I have a slightly different approach – here

  10. Great idea, but does this really work when in situations where you need to keep the same component because it maintains some kind of state.

    For example, I am looking for a way to animate expansion and collapsing of a sub tree in a tree-viewer. But if I replace the key of the child subtree upon collapse, will React still have the component with the right key, when I open up that subtree again?

  11. You can also include “transitionLeave={false}” Which will not apply any classes when the components are leaving the group, instead of hacking together how to hide them.

  12. https://github.com/html-monster/ReactUpdateAnimation
    https://html-monster.github.io/ – Demo
    I’ve modified an extension of ReactCSSTransitionGroup component. It expands component capabilities, now the class will be added not only while the appearance or removal of an element, but also when an item is updated

Post a Comment

(required)

(will not be published) (required)

XHTML tags allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Note: rel="nofollow" will be added to all links in comments.

Comments will be closed on Wednesday, January 10th, 2018.