Sophisticated animations pose some interesting challenges in ReactJS, especially when using CSS keyframes. I recently created a cool CSS/JS animation for a ReactJS project, and it turned out to be harder than I’d anticipated. Hopefully this article will help you to avoid, or at least anticipate some of the challenges that I faced.

Summary

First, some background on the inspiration and design objectives of the subject animation for this article, which is a spinning logo cube on the homepage of my new ReactJS personal web site (source code: https://github.com/lpm0073/lawrencemcdaniel.com). The design objective of the logo cube is to display a randomized collection of logos representing technologies with which I’m familiar. The logos change every few seconds so that, over time, it shows lots of different logos. Related, there are a few technologies that I want to showcase (like for example, ReactJS) which introduces some design challenges as well.

My personal site is basically an online CV, and client prospects and technical recruiters are my primary audience. The goal of the spinning cube is to (hopefully) convey a lengthy list of my technologies skills to the visitor. The longer they stare at the cube the more technologies I’ll be able to inform them of, and hence, I decided to invest some serious time and effort into making the cube as cool as possible.

The completed logo cube works well and actually exceeds my original design objectives. It is implemented as a ReactJS component (see source code: github.com), with a healthy dose of CSS. It bears mentioning that I’d originally intended to create the cube as a D3 animation but later changed strategy once it became apparent that ReactJS provides everything that I needed for the project. In a nutshell, the cube animation is done entirely with CSS, and, as you’ll see below, cube side logo selection is driven by some rather unique business rule requirements that are better handled by ReactJS. And finally, you should note that mixing D3 with ReactJS is tricky business given that both of these technologies manipulate the DOM.

Mind you, I encountered a multitude of unforeseen challenges, even after having found source code written by Noah Veltman on the D3 web site for a spinning cube that exactly suited my requirements and tastes. I’ve arranged these into two groups: Technical Challenges, and Design Challenges. Let’s start with the technical challenges.

1. React Amnesia

ReactJS, on its own, does not have a way to track component state over the life of an entire site visit. Every time the cube rendered, React would behave as if the site visitor had just arrived to the site, even when this was not the case. For example, if a user lands on the home page then they would see the glory of the Logo Cube being rendered. But if they navigated to a different page on the site and then later returned to the home page then they’d see the entire rendering of the cube once again, and again and again and again. This looked amateurish.

Solution: Enter Redux. I solved the amnesia problem by using Redux to keep track of whether or not the cube had been rendered. The basic pattern is to set a boolean variable, “isSet”, to true when the logo component is about to unmount, and then to use this boolean variable to decide whether to include the intermediate animation class when rendering the cube. Redux is easy is implement, once you get used to it.

See: Managing your React state with Redux, by Onsen UI & Monaca Team

2. Screen Flicker

React mounts/unmounts components multiple times, often for reasons that are external to your component. React also potentially calls render() more than once during normal component initialization. Both of these are problematic for CSS animations because invoking render() will kill and restart any animations that were in-progress at the moment of invocation, causing screen flicker which looks terrible.

solution: It turned out that in the case of the logo cube, ReactJS was mounting / un-mounting the logo cube component around 12 times. Most of these rendering cycles were caused by the way that I initialize Redux for various REST apis; none of which are related to the logo cube. I made creative use of the component lifecycle method shouldComponentUpdate() in the module where my Redux initializations where located to eliminate nearly all of the superfluous mount cycles. After this fine tuning ReactJS now mounts the logo cube component exactly two times in the span of a few milliseconds, which however is still enough to cause some noticeable screen flicker. I eliminated this remaining flicker by launching the component’s painter() method on a timer delay of around 100ms. The painter() methods is responsible for setting the background image URLs on the sides of the cube. Noting that ReactJS invokes render() each time painter() changes a background-image setting, this delay timer defers rendering of the cube until React has had enough time to finish fully mounting the logo cube component.

3. Orphaned Threads

Launching the painter() method with a timer delay introduces some new complications however. The timer itself is actually an entirely new thread which runs asynchronously and therefore is not governed by the lifecycle methods of the components from which it was invoked. In my case this led to the logo cube component creating one such thread each time the component mounted, and recall that during much of development the component was mounting around 12 times. That in turn meant that at run time there were around a dozen asynchronous processes making random updates to the cube side background-image properties, which was complete anarchy!

solution: I fixed the threading issue by keeping track of, and explicitly killing all background threads that I create.

    componentDidMount() {
        //  create a time delay to launch repaint()
        const self = this;
        const myTimeout = setTimeout(function() {
            self.repaint();
        }, 100);    
        this.setState({
            repaintDelay: myTimeout
        });

    }

    componentWillUnmount() {
        // kill any pending background threads that were
        // invoked in componentDidMount().
        clearTimeout(this.state.repaintDelay);
    }