Introducing the VelocityReact Library

by Pete Hopkins, Software Engineer

If you’re used to animating with CSS, Velocity.js feels like cheating.

Like so many, we had long since given up jQuery animations for CSS transitions (when we were lucky) and keyframes (when we weren’t). We hacked in setTimeouts that added animation-triggering classes post-render, we took care not to get flummoxed by bubbling “animationEnd” events, and we animated max-height instead of height, hoping we never had more than 9999px of text to show.

We did it because it was performant. Hardware-accelerated or whatever. It was a pain, but isn’t good performance supposed to be a pain? Eventually, the level of effort became self-justifying, and the conventional wisdom around animation took hold: “jQuery (and by extension, JavaScript) might be easy, but it’s too slow. CSS is hard and fast.”

And we were proud to take the hard route.

Velocity.js, in these terms, is cheating. It is, quite literally, a return to jQuery: Velocity hooks into $ and its velocity method is API-compatible with that scoundrel animate. The difference, as explained by Velocity creator Julian Shapiro, is in a few little tricks: a single timer loop and rigorously correct ordering of DOM operations, which combined have a night-and-day effect on performance. Suddenly, JavaScript-controlled animations are possible, and with them a variety of features you can build when you have a programming language at your disposal: custom tweening (including spring physics), interruptibility, chaining, workarounds for implicit heights, and reliable completion callbacks — not to mention Velocity’s UI Pack of ready-made effects.

Our team embraced Velocity in the same iconoclastic spirit with which we embraced React. Here was something that eliminated a giant engineering headache around animations, and we were eager to adopt it to easily bring additional fun and liveliness into our pages.

That being said, React and animation don’t actually play well together at first. There’s an impedance mis-match: React’s power stems from abstracting away the state transitions in your UIs (it does them quickly and correctly, enabling the declarative paradigm we’ve come to love), while animations live entirely within those state transitions.

The CSS transition properties modeled this pretty well: “whenever this other property is changed, apply that change gently.” The animations are declared, the transitions are derived. Keyframes are less successful: “when this is applied, set these properties to these specific values over time.” These semantics lead to CSS keyframe animations being applied at undesirable times (such as initial render), always from the very beginning, and without any means of dynamically modifying them.

To integrate smoothly with React’s declarative nature, we’ll need our animation behavior to be fully described by the output of our render methods: a desired end state.

Happily, Velocity is set up to give us just what we need. If you start a Velocity animation on an element, you know it will eventually come to your desired end state[1]. But unlike, say, CSS keyframes, it can be stopped on a dime, or started from the element’s current appearance, taking a smooth, direct path to the new animation’s conclusion[2]. Think of it like React’s reconciliation, just in (relatively) slow motion.

Our Velocity / React integration can then follow this simple algorithm:

  • Initially, an animated component will immediately appear as it would at the end of its given animation (at least by default). That way we can display a component immediately in a fresh state.

  • If that given animation ever changes, we run it to get to the new end state. If there’s an animation currently in progress, we stop it first, and then proceed smoothly from whatever intermediate state it left us in.

We built this, and called it VelocityComponent.

release_summary.gif

Here’s how we use it to animate the stat “flaps” in the Beta UI’s sidebar. When you mouse over one of the stats, its flap flips up to reveal additional data. When you mouse away, the flap falls back down, with a small swing effect.

render: function () {
  var animationProps;
  if (this.state.hovering) {
    animationProps = {
      duration: 200,
      animation: {
        rotateX: 160
      }
    };
  } else {
    animationProps = {
      duration: 1100, // longer due to swinging
      animation: {
        rotateX: [0, 'spring']
      }
    };
  }

  return (
    <div onMouseEnter={function () { this.setState({hovering: true}); }}
         onMouseLeave={function () { this.setState({hovering: false}); }}>
      <VelocityComponent {...animationProps}>
        {this.renderTopState()}
      </VelocityComponent>
      {this.renderUnderneathStats()}
    </div>
  );
}

 

Just as with non-animated React, our render remains a function over the component’s props and state. Changing that state changes the desired animation, which causes VelocityComponent to run it.

Under the hood, VelocityComponent is quite simple. When it mounts, it uses Velocity’s finish method to instantaneously run the initial animation, immediately getting us the desired end appearance. Its componentWillReceiveProps method then keeps an eye out for for changes to the “animation” property. When they arise, it stops any current animation and triggers the new one.

VelocityComponent is for animating a component that typically stays on the page during the animations. We also built a Velocity wrapper that integrates with the ReactTransitionGroup addon component, which runs animations on child components as they are added to or removed from a parent (ReactTransitionGroup handles keeping “removed” components in the DOM until their transition animation completes).

A canonical use of VelocityTransitionGroup is on a list of components, like the list of real-time events in the Beta sidebar. This list updates while you’re looking at the page, so we animate new events in the top while letting old ones slide out the bottom. The hint of movement signals that there’s something new to see, without being as jarring as an complete repaint of the component.

render: function () {
  return React.createElement(VelocityTransitionGroup, {
      component: 'div',
      enter: {
        animation: EventAnimations.In,
        duration: 500
      },
      leave: {
        animation: EventAnimations.Out,
        duration: 500
      },
    }, this.state.events.map(this.renderEvent));
}

*For a complete implementation of this effect, including the animation definitions, see the demos in the GitHub repo.

 

The real value of these components is the way they can quickly add a touch of liveliness to your app without overcomplicating the code. By sprinkling VelocityComponents and VelocityTransitionGroups in your render methods, your elements can slide and swing rather than just pop in and out.

Let’s see how this all comes together in our UI to show a beta tester’s devices. We’ll start with the toggle button. This will switch our component between its collapsed and expanded states.

renderDeviceToggle: function () {
  var arrowAnimation = {
    rotateX: this.state.expanded ? 180 : 0,
    transformOriginY: ['42%', '42%']
  };

  return (
    <div className="device-toggle"
        onClick={function () { this.setState({expanded: !this.state.expanded}); }}>
      <div className="device-icon icon huge"></div>
      <VelocityComponent duration={300} animation={arrowAnimation}>
        <div className="icon color-blue flex">
          <i className="icon-caret-down"></i>
        </div>
      </VelocityComponent>
    </div>
  );
}

 

The component renders a down arrow by default, but if it’s in the “expanded” state, it rotates it to be an up arrow.

Next comes the list of the tester’s devices. In a previous life, we may have been tempted to render these hidden for every row and use jQuery to show them on expand. With React, it’s trivial to render the devices only when we’re in the “expanded” state, saving a good deal of time and memory on DOM elements that the user isn’t going to see.

To show the row expanding smoothly, we’ll turn to Velocity’s “slideDown” and “slideUp” animations, which are built-in effects that animate an element to and from its intrinsic height (try doing that without JavaScript). We can use VelocityTransitionGroup to automatically trigger the “slideDown” and “slideUp” by simply rendering or not rendering the device list within it.

render: function () {
  return (
    <div className="row">
      {this.renderTesterInfo()}
      {this.renderDeviceToggle()}
      <VelocityTransitionGroup component="div" enter="slideDown" leave="slideUp">
        {this.state.expanded ? this.renderDeviceList() : null}
      </VelocityTransitionGroup>
    </div>
  );
}

 

To trace it through, clicking the toggle button sets the state to “expanded,” which causes React to re-render. The toggle’s animation changes, triggering VelocityComponent to animate it to the “up” position. Meanwhile, we start actually rendering the device list. This is picked up by our VelocityTransitionGroup, which runs a “slideDown” on its new child.

Clicking the button again runs things in reverse: the arrow animates back down, we stop rendering the device list, and the VelocityTransitionGroup smoothly slides it up and out of the DOM.

Still, there’s one thing missing: the data! When the list is first toggled open, we haven’t actually fetched any details about the devices from the server. We’ll need to show a loading state while that request is underway. While we could toss up an animated spinner and pop in the results when they happen, we have a much slicker effect in mind.

When the row is expanded, we’ll render an appropriate number of ghostly “loading” devices in the list. Then, when the data does come in, we can smoothly crossfade to the complete list.

renderDeviceList: ->
  var loaded = this.state.devices != null;
  var deviceList = loaded ? this.state.devices : Array.apply(null, Array(this.state.deviceCount));

  return React.createElement(VelocityTransitionGroup, {
      style: { position: 'relative' },
      enter: {
        animation: { opacity: [1, 0] },
      },
      leave: {
        animation: { opacity: 0 },
        style: {
          position: 'absolute',
          top: 0,
          left: 0,
          bottom: 0,
          right: 0,
          zIndex: 1
        }
      }
    }, 
    React.DOM.div({ key: loaded ? 'devices' : 'loading'},
      deviceList.map(this.renderDevice)));
}

 

We can use VelocityTransitionGroup to power the crossfade. An entering element has its opacity animated from 0 to 1, while a leaving element goes from its current opacity (presumably 1) to 0. Furthermore, we use absolute positioning so that the two elements overlap during the transition.

There are a few aspects of this code that are worth calling out:

  • Our renderDevice method takes either a device or undefined. If it’s given the latter, it renders a “loading” version of a device. We do this in the same method so that we can make sure to maintain a consistent appearance for loading and loaded, which is essential for a smooth crossfade.

  • Changing the key property is a signal to React’s reconciliation algorithm that it should treat the “loading” and “loaded” versions of the list as different elements. This will cause the underlying ReactTransitionGroup to see an old element leaving and a new element entering.

We liked the crossfade effect so much that we encapsulated it into a LoadingCrossfadeComponent, which you can find in the demo code in our GitHub repo. Now, adding a crossfade can be done with a single wrapper around any place where we’re choosing among subelements to render. This ease speaks to the value of React’s component model, which makes it trivial to factor out reusable UI pieces at any level of the view hierarchy.

With VelocityComponent, animating any component in your app is straightforward to set up and maintain. We’ve been using it for anything from little flips to sliding tab indicators to long, multi-step easter eggs. Velocity’s ability to animate from an arbitrary current appearance of the element makes it a perfect fit for augmenting the transitions of capricious render methods.

VelocityTransitionGroup keeps elements from jarringly popping in and out of the page. We slide, rotate and crossfade components without any more effort than simply choosing to render them or not.

You can use these two components today with your React app. NPM install the velocity-react package, and clone our GitHub repo to check out the demos we’ve built. Let us know how it works out for you, and file any issues that you find.

Footnote:

1: We’d like to note here that while it is generally a bad idea to directly modify the DOM elements that React creates for you, in our experience having Velocity update their styles is safe. Even if the element’s other attributes or even style properties change, the ones added by Velocity are preserved. Nevertheless, you should avoid changing the same style property with Velocity and React at the same time.

2: There is a caveat here that you should at least be aware of. While Velocity can animate from arbitrary starting values, it does so with a consistent duration. So, if you animate to 0 opacity in 500ms, that will take 500ms for both a fully opaque element and an already half-transparent element. In practice this is not much of an issue since animations typically do have the consistent start points of their natural resting states. When it does come up due to interrupted animations it is often barely noticeable.


We're helping our customers build the best apps in the world. Want to be a part of the Fabric team and build awesome stuff? Check out our open positions!