Using React Refs and Animating between Modals - Part 1

React JS – animating between modals

Following on from my previous article Tasks and Portals in React I wanted to take a look at how you could add some nice animation between screens within a given task. A good example would be filling out a simple form, sending that form off, waiting for a response and then giving the user some feedback.

This simple task can be thought of as four separate screens:

  1. User Input
  2. Processing the user input
  3. Success
  4. Error

This covers the basic XHR/Async flow for any given UI task. Here’s the outcome that we want:

Animated tasks

In my previous article I looked at how you can create and render this series of tasks in the UI, now we want to add some polish and bring in some animation between each screen in the Async task flow.

 

 

The problems

Firstly

We want the animations to be nice and smooth, so really we want to avoid animating any attributes that will be expensive. We want to avoid animating anything that will cause layout changes and expensive re-paints, so things like height and width are out. We’ll need to stick with cheap to animate properties like scale, position and opacity (Here’s a good primer if you want a little more background).


Secondly

In order to know what we’re animating we’ll need to measure each of our screens before they are rendered to the DOM. This is important because if we want our screens to morph between each other, we’ll need to know what the difference in dimensions are between each screen. This will give us a series of deltas, which we’ll then use to drive our animation.

For example, let’s say that screen A is 200px by 200px and screen B is 100px by 400px, then our animation has to show the change in dimensions between each screen, reducing in size by 100px on the X axis and 200px on the Y axis. If we then have 20 different screens, we want to know the difference or delta, in size between each screen.


Finally

Once we know the number of screens and the change in dimension between each, we’ll then need a controller to manage the animation between each screen.

 

 

Getting our screen dimensions

To do this, we’re going to make use of the composable nature of React components. By that I mean that we’ll separate out our components so that they are only interested in a single task. In this first instance we’ll start with a single component whose job it will be to take a list of screens, get their dimensions, and then pass those onto a child component as props. This allows us to split out the task of getting the screen dimensions from that of animating between those screens.

So, with this in mind, let’s go ahead and create the API for our Screen Measuring Component. We know it must take a list of screens to animate, and we know it must pass the dimensions of those screens onto its children, so something like this should do the trick:

Looks good, so how are we going to go about measuring and storing the dimensions of each screen? Let’s start with measuring each screen’s dimensions.

Here’s our list of screens:

Next up, we’ll create a simple component that acts as our <ComponentThatWillReceiveDimensions />. Its job will be to report back with the number of screens it can see in its props and also the dimensions of each screen: 

Okay, now let’s jump in and create the component that will measure each screen:

As you can see it’s nothing complicated, but let’s break it down.

First we check to see if we have any screenDimensions stored for our list of screens. If we do then we call the addPropsToChildren method, which as you’d expect returns a list of any child components that we pass in, with the screen dimensions props added on.

Next we are rendering the screens that we want to measure inside a hidden area of the screen. This allows us to query the DOM and retrieve each screen’s dimensions. Notice also that we’re using the assignRefs method to render our list of screens. This is important as it will allow us to retrieve the DOM elements for each screen and interrogate their dimensions.

We can see that the ‘getListOfScreens’ method simply returns returns an array consisting on the keys of each screen and then we pass that array to the assignRefs method. This is important as the assignRefs method does a number of things:

Firstly it actually renders the screens (by returning an array of react components for each screen).

Secondly it assigns a ref to each of those components ref={(el) => this.refsStore[screen] = el} (If you don’t know, React has changed the API for assigning refs to a component now, you can read more on this here). It then stores the reference to each element inside a global property of the Class named this.refsStore. This is great as you then have a way to look and retrieve the DOM Node for each screen that we rendered inside of our hidden container.

Finally, we then position each screen, absolutely, so that they don’t affect the layout of our page, but can still be measured.

Now that we have a reference to each screen’s DOM node, let’s go ahead and actually get the dimensions of each screen.

componentDidMount() {
 this.setState(this.getDimensions())
}

As you can see, we can only start this process after the initial render of our screens. This gives us some real DOM to query. This is done inside the getDimensions method.

We’re using a reduce to iterate over our list of screens. On each iteration we retrieve the ref that we stored earlier inside this.refsStore, then we call the getDimensionsOfNode method on that screen, which in turn calls ReactDOM.findDOMNode on the screen. This goes and retrieves the screen’s DOM Node. Using this we can call the DOM method getBoundingClientReact() to get the dimensions of the screen… Phew!!

We then return the screen dimensions and add it to our array of accumulated list of screen dimensions.

accum[screen] = this.getDimensionsOfNode(this.refsStore[screen])

Finally we return the entire array of screen dimensions as an object with a single property of screenDimensions, which will be assigned to our component’s state.

return {
  screenDimensions,
}

Now that we finally have our screen dimensions stored in state, we just need to pass them onto our child components as props. This is where the addPropsToChildren method comes in. Remember, it was used to render our child components in the render method. Let’s take a look at what it’s doing:

The first thing to note here is that we’re taking any props that are passed to this component MeasureScreenDimensions and passing them directly to our child component using the spread operator…

const newProps = {
  ...this.props,
  screenDimensions: this.state.screenDimensions,
}

Normally this would be something that you’d want to avoid, but as this is a Higher Order Component and we’re specifically passing on dimension props to any child component, to use as they wish, we’ll want to make sure that the child component still has access to any other props passed to its immediate parent.

Next we simply add the screenDimensions from state onto our newProps. Finally we iterate over each of our child components, remember that we don’t know how many there could be, so we need to assume that we have an unlimited amount, assigning our newProps to each. We do this, by cloning each child element before assigning our new props to each.

return React.Children.map(this.props.children,
  (child) => React.cloneElement(child, newProps)
)

So that’s it, we now have a component that will measure our set of screens and pass those dimensions onto a given set of child components. You can see a working example here: https://codepen.io/jeanpaulgorman/pen/GEKNYx

Next up, we need to be able to transition/animate between each screen. You can read about this in my next article: Using React Refs and Animating between Modals — Part 2.


Written by Jean-Paul Gorman, Software Engineer at Moneyhub Enterprise