Animating a React Component
Posted on: 2018-06-26
I have always been a fan of having user interfaces that are smooth for the eyes. One way to do it is to animate element that moves from one position to another one. When I was working at Microsoft in VSTS (Visual Studio Team Services) I developed the animation around widgets. A widget is a small block of a dashboard and one task possible is to edit this one. Instead of having the edit panel coming in place and having a blunt experience. I animated the panel to come from the right edge of the browser inside the viewport and also animating the widget to move from its position in the dashboard in the middle of the screen while blurring the background. The experience was great. The panel wasn't hiding a lot the viewport, the user knew exactly where was the widget before the edition and it was clear which widget was in an edition. This was done many years ago, all in JQuery with some CSS animation. It was dirty by today's standard but not a huge feat to accomplish in my perspective. Today, I am not relying on JQuery but on React and the process for something is similar with the difference that it requires a little bit more thought.
React is great on many points but if you want to customize how a component change from a position/size to another it can be mesmerizing. The reason is that React when receiving new information will render and the rendered component will be at its final position. In this article, I will cover how to handle the case of a React's component that needs to move its "left" and "top" position but it could be any property you want.
Before jumping in the code, let's describe what we will do. The idea is to get the actual data right before we are getting the new value and save a copy of it inside the component before it renders. A good place is componentWillReceiveProps
where you can access the DOM element by reference and save a copy of it before the new props are propagated into the whole React's lifecycle of your component. React render function does not paint to the screen when the render function is invoked. It is once the componentDidUpdate
is called that the browser paint. With the information in mind, and the data saved in the componentWillReceiveProps
we can access the before and after the position/size of a component. The before being gathered in componentWillReceiveProps
and after in render
function.
What you can do is to create one variable to hold the value that change and that you care for your animation. You also need one variable to have a reference to the HTML element. This should be a variable in the class component and not in the state. Changing the state invokes the lifecycle of your component to render again which is not required in that case. In the following example, I am saving my HTML element that I want to animate with the left and top position.
private domCluster: HTMLDivElement | null = null;
private lastLeftPosition: number = 0;
private lastTopPosition: number = 0;
The next step is to save the actual value in the variable of the class. In my example, I'll get from the saved reference of the HTML element the top and left position using the JavaScript function getBoundingClientRect().
public componentWillReceiveProps(nextProps: FillWindowsClusterProps): void {
if (this.domCluster !== null) {
const coord = this.domCluster.getBoundingClientRect();
this.lastLeftPosition = coord.left;
this.lastTopPosition = coord.top;
}
}
The next function to define is the render. The render function has the role to set up the component at its final place and to set up the reference to the HTML element.
public render(): JSX.Element {
return <div key={"myComponentName_" + this.props.elementUniqueId}
ref={(e) => this.domCluster = e}
style={ { left: this.props.left, top: this.props.top } }
>
Your Component
</div>; }
The last React piece is in the code in the componentDidUpdate
. This code will calculate the difference between the final position that we will get again using the getBoundingClientRect() from the element generated by the render function and the last position saved from the componentWillReceiveProps.
public componentDidUpdate(previousProps: FillWindowsClusterProps): void {
if (this.domCluster !== null) {
const coord = this.domCluster.getBoundingClientRect();
const deltaPositionLeft = this.lastLeftPosition - coord.left;
const deltaPositionTop = this.lastTopPosition - coord.top;
if (this.domCluster !== null) {
this.domCluster.style.transform = `translate(${deltaPositionLeft}px, ${deltaPositionTop}px)`;
this.domCluster.style.transition = "transform 0s";
}
requestAnimationFrame(() => {
if (this.domCluster !== null) {
this.domCluster.style.opacity = "1";
this.domCluster.style.transform = "";
this.domCluster.style.transition = "transform 850ms, opacity 800ms";
}
});
}
}
The heavy lifting is done by CSS. The idea is to use translate on the x and y coordinates and to set the value back to the old position. Then, we are using requestAnimationFrame to tell the browser that on the next rendering loop to remove the transform setting back the position to the final one (the one determined in the last render).
In the end, the animation will work flawlessly. The user will not see the trick we just put in place. In reality, we had a life cycle in React that position the element at its final place but we used CSS to return the element back into its original place -- the place the component was before the render. And then we let the browser render the element that has a style that put immediately the component into its initial place with an animation to let go the component into its final position.
From a JQuery perspective, it's a lot of more work with React. However, that is the cost to pay to have an optimized framework that takes care of the DOM/HTML for you. In that particular case, where the HTML must be accessed, there is a little tax to set up everything correctly. However, since most of the React's components are not animated and that the core goal is to let React handle the painting it is a small pain for an overall nice performance gain.