UI Animations with React — The Right Way
When building web application UIs, I’ve always been a big fan of transitions. Instead of just popping things in and out, everything that moves needs to fade or slide in and out of existence, bringing the super polished and professional look. jQuery made this extremely easy, and for a long time, it was the standard for bringing that web 2.0 look to your pages.
Then came the front-end frameworks like Knockout, Backbone, Angular and React, and suddenly it wasn’t cool to use jQuery to manipulate the DOM anymore because that was the job of your shiny new framework. If any changes get made to the UI, they should be the result of the underlying data model changing and the framework altering the UI to reflect those changes. So jQuery began to take a back seat as it violated this principle.
If you’re like me, when you first started using a front-end framework, you probably came up with some pretty creative ways to implement your precious animations. When I first started using React, I was immediately sold on how easy it was to build very complicated UIs in a simple and straightforward way. It was powerful, scalable, fast and fun. However, I noticed that it didn’t seem to lend itself well, at least at that time (2015), to UI transitions.
Here is an example of my initial strategy for implementing a fade with React.
In the componentWillReceiveProps life-cycle method I’m checking the visibility prop and then using jQuery to fade the component in or out based on the value of the prop. And this works, but… yuck!
I rationalized that this was a good solution because, even though I am using jQuery to manipulate the DOM (big no no), I was doing it in response to a data change. So I’m following the rules even though I’m breaking the rules. This is not an ideal solution in at least 3 other ways.
- Although this works in this context, you often will run into problems in more complicated scenarios. For example: if you want to fade something out before removing it completely from the DOM, you would have to wait for the completion of the fade before you update the data which would in turn unmount the component. This is manageable, but it generally introduces more complication than you would want just to add some UI sugar.
- jQuery isn’t exactly a light library. It clocks in at around 85k minified. Tossing that extra weight into your client download is costly, especially when there are better options available. (read on)
- With the advent of stateless functional components, the component life-cycle methods including componentWillReceiveProps that is used in this example, are no longer available. Stateless functional components encourage good coding practices, are extremely easy to test, and offer performance optimizations over their class based counterparts.
Good News! There’s a better way.
We can achieve the same effect in a stateless functional component using CSS transitions.
In this example component, a single boolean value is being passed in as a prop to determine if the ‘show’ CSS class should be added to the component. As the value is flipped between true and false, the class will be added and removed from the component root element. Then we simply need to rely on the power of CSS transitions. The example-component class begins invisible, but when we add the additional class of show, it will animate a fade-in.
Note that the transition is taking place over the ‘opacity’ and ‘visibility’ CSS rules as ‘display’ does not animate and will simply snap the element in and out. Opacity is responsible for the fade and visibility is responsible for removing click and hover events.
Problems with CSS Transitions
This method isn’t without it’s own nuances as well. First and possibly the most obvious is that CSS transitions are a relatively new feature and are not supported in older browsers. This however could be considered a non-issue as non-supporting browsers will fail gracefully. A browser that does not support the transition style rules will simply ignore them and your page transitions will snap from one state to the other without animation.
Another problem you may run into is that it leaves white space behind where the content had been prior to the fade animation as the ‘visibility:hidden’ style rule does not actually collapse the content. If this ends up being a problem, you can reduce the height or width of the element to zero as part of the transition. As doing so may alter the arrangement of the content during the animation, you may want the fade out animation to complete before size reduction animation begins. This was easily accomplished with jQuery using callbacks. But callbacks don’t exist in CSS so an alternate means has been provided: transition-delay.
Using transition delays to order the animations one way in and the other way out can be a little tricky.
In this example we have a div with the CSS class of ‘box’ which sets all of it’s initial properties including a transition-delay. When you want the content to fade out you add the hide class. As soon as the hide class is applied, the transition rule from that class overwrites the original one in the box class and it will be the transition rule that is followed for the altered properties of the hide class.
The ‘transition’ rule will take a comma separated list of CSS properties you wish to animate. For each property you wish to animate you put the property name, the transition-duration, and then the transition-delay. Note that in the transition rule in the hide class, both height and opacity have a 500ms duration, but height has a 500 ms delay where opacity has no delay. This will cause the height animation to wait until after the opacity animation is complete. When we remove the hide class the transition rule immediately changes to the one listed originally in the box class, wherein the opacity has the 500ms delay and height has no delay. Thus the order in which the animations execute is reversed. Like I said, this can be tricky, especially if you try to get overly complex with the number of things you are trying to animate at different times. You can see a short clip of this working below.
OK, Awesome! But there is still one more problem we need to cover.
When working with React, there are times when you want to animate a component directly after it has been mounted, or directly prior to it being unmounted. Let’s say you map over an array of objects and render a component for each object to create a list in your application. Now let’s say you want to add animations to fade-in new items that have been added to the array or fade-out items as they are removed from the array. This can be problematic because as soon as you alter the underlying data in the array by removing an item, React will unmount the associated component immediately before you have a chance to fade it out. Likewise, if you are using stateless functional components (which we’ve already established is a good idea whenever possible) you have no life cycle methods to trigger the fade-in animation when a component is first mounted as new objects are added to your array.
Luckily, the folks over at facebook have realized this and have blessed us with a new add-on, ReactCSSTransitionGroup, to solve the problem. ReactCSSTransitionGroup is itself a component in which you can nest child components. Any child component that mounts will undergo the ‘enter’ transition right after it mounts, and any child component that unmounts will undergo a leave transition before it unmounts. The ReactCSSTransitionGroup component takes in transitionEnterTimeout and transitionLeaveTimeout as props. What these values represent are the duration in milliseconds of your enter and leave transitions. Essentially these props help ReactCSSTransitionGroup know when to add and remove the various CSS classes involved in the transitions and when it can unmount child components.
The CSS classes that ultimately cause the animations need to be defined by hand. The class names that will be applied by ReactCSSTransitionGroup are defined by convention based on the value passed in to the transitionName prop. The values for the two timeout props should correspond to the transition durations in the CSS classes that you define.
By convention, if the value of ‘example’ is passed in to the transitionName prop of ReactCSSTransitionGroup, it will look for these CSS classes:
- example-enter : defines the beginning state of the enter transition
- example-enter.example-enter-active : defines the actual enter transition
- example-leave : defines the beginning state of the leave transition
- example-leave.example-leave-active : defines the actual leave transition
Once you get it wired up, the result is really smooth and satisfying.
That about Sums it up!
With this set of tools, you should be able to create just about any transition you can think of in a React application, and you’ll be doing it without breaking the rules of modifying the DOM manually.
One more Thing…
Someone tweeted a comment to me recommending another great library for doing React animations and I felt remiss if I didn’t make mention of it here. react-motion — It seems to be very powerful, but perhaps at the expense of being quite a bit more complicated. Check it out!
I’d be happy to answer any questions. Hit me up in the comments below or find me on Twitter @JoeTheDave.