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.
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); }
4. Slow-Loading Logos
Like most animations, this one depends on a set of static assets that have to be delivered to the browser from multiple sources. The automation code — React/Javascript in this case — then assembles the static assets into a delightful on-screen presentation. Unless that is, some of these assets are missing in action, in which case, your automation looks broken or poorly designed. The logo cube suffered from this problem, and I spent an enormous amount of time meticulously story-boarding and choreographing the arrival of the static assets so that the animation rendered smoothly for “normal” Internet connectivity.
The cube sides are rendered by CSS, which renders quickly and smoothly on all browsers. But, the nature of the problem is that the cube side background-image properties depend on a collection of around 75 logo images; the URLs of which are provided by a REST API. I have to request this, then wait indefinitely, then parse the URLs, and then only afterward can I begin to download and paint the cube sides with these images. On a slow internet connection you would initially see a bare-naked cube for several seconds, and then afterwards you could see each new logo downloading on the cube side, which looked terrible.
What I describe here as the “slow-loading logo problem” really gets to the heart of the challenge of creating sophisticated animations in React. Slow-loading logos are a big problem that requires a multi-prong solution. Note that the long list of logos is a design requirement: I want to show site visitors a huge list of technologies. Furthermore, the logos need to be high quality, and, they need transparent backgrounds. Following is how I solved this for the logo cube.
Solution:
- I store the first six logos locally so that these are included in the React bundles and are delivered to the browser immediately, before the logo cube begins to render.
- I created an image pre-fetcher inside of Redux that downloads each logo as soon as the REST api returns with the image URLs.
- I employ some sleight of hand: the 1-second animation of the cube being rendered resolves itself to an in-motion cube (covered with the six preloaded logos) that is visually satisfying for around three or four seconds. This buys time for React to do the remainder of its work to query the URL list from the REST api and preload the other logo images.
- I web-optimized all logo files to decrease data transfer. In my case I only needed to reduce the file size to match that of the logo cube sides.
- I make AWS Cloudfront gzip all of the images, further reducing data transfer by an additional 30% or so.
5. Reverse Rotation (aka “Wagon Wheel” Effect)
I had trouble visually tracking the motion of the original version of the cube’s spinning animations.
The cube would sometimes appear to be spinning left-to-right, and other times right-to-left. In
the latter cases the cube appeared mal-constructed and disfigured. Here’s a technical explanation
of the nature of this problem: wikipedia.org/Wagon-wheel_effect
solution:
First, i tinkered with the opacity of the cube sides so that the side that is supposed to be in front is brighter,
and the reverse side is darker. Secondly, adding the cube rendering helped a lot because the cube begins
as a 2-dimensional spinning square, which afterwards grows to become a cube. The 2D spinning image is easier for your
visual cortex to process, and the growth animation provides a way for your mind to bootstrap and retain what’s
happening on-screen.
6. Lack of Impact
The logo cube is supposed to be a creative way of showcasing my most important technology skills to site visitors. However, most visitors will only see the cube for a couple of seconds and then move on to some other page. Thus, a random selection of logos works at cross purposes to this design objective since most of the logos pertain to lesser-known supporting technologies.
solution: I select six “featured” logos with maximum impact to use when initializing the cube. Their placement on cube sides remains randomized so that, to the casual eye at least, the cube seems to be different each time the page reloads.
7. Disappointing Logo Shuffle.
It bothered me that sometimes all six cube sides would display only the logos of unimpressive technologies for an extended period of time. I found myself staring at the cube anxiously waiting for a “good” logo to appear.
Solution: I reserve one of the six cube sides to use exclusively for presenting the six featured logos, in random order. This ensure that an impactful technology is being presented at all times.
8. Duplicate Logos
It bothered me that the same logo would sometimes appears on two (or more) sides.
Solution: I added some “no collision” logic to prevent the random logo selector from choosing any logos that are currently being displayed.
9. Erratic Logo Updates
Logos change at random intervals. However, it looks bad if a logo is replaced too quickly. That is, a certain amount of randomness in the presentation of the logos keeps the animation fresh and interesting. But too much randomness creates visual tension and unease. We want to maximize the former while minimizing the latter.
Solution: I added 6 timers to track the elapsed lifetime of each logo, and then I only replace a logo if its been visible for a minimum length of time.
ReactJS is a popular open source JavaScript
reactjs training in hyderabad