React Page Transition Animations
While working on my capstone project BandHaven at coding bootcamp App Academy I learned a few things about the ReactCSSTransitionGroup add-on:
- The different usages and limitations of appear vs. enter/leave transitions.
- How to create CSS page sliding transition animations with React.
- How to map out react-route path branches with dynamic React page transitions.
Transition Animation Basics
Before we start, it may help to check out this live demo while you read to see what this all look like in action: live demo github repo
The core of CSS transition animations is the ability to transition a HTML element from one css state to another with the transition property. Therefore two states minimum are necessary. Quick example below:
div {
opacity: 1;
transition: opacity 0.5s;
}div:hover {
opacity: 0.01;
}
The above will select all divs on the document and cause them to fade away on hover due to transitioning the opacity. Note that this requires two css states, div and div:hover. What happens when it’s a new element being written onto the page? How will there be two css states?
ReactCSSTransitionGroup
In the old JQuery days people manually created two css states upon generating a new element by first giving the element one class, then using setTimeout to create a delay and finally giving the element another class through the callback within setTimeout. Thankfully we are past those days and the ReactCSSTransitionGroup does this all for us. ReactCSSTransitionGroup is a CSS focused high-level API for the ReactTransitionGroup add-on. The basic idea is the any children wrapped within the ReactCSSTransitionGroup tag will be given a delay and an extra “phase” before completion of mounting and unmounting in order to create the states necessary for CSS transitions. This sounds simple but there are some things to watch out for, namely the two different types of transitions with this add-on, enter/leave vs. appear.
Enter/Leave Transitions VS. Appear Transitions
Now we know that we have to wrap child components and HTML elements in the React transition group tag for transitions states to be applied to them. But there are certain conditions that must be met for each of the two transition types. In order to utilize the enter/leave transition type, the ReactCSSTransitionGroup tag must be mounted and rendered prior to mounting of any of its children that we want the transitions to apply to. What does this mean exactly? See example from React documentation below:
Note that in this simple Todo List example, an array of four items were stated in getInitialState. Functionality was also added to add or remove items from the Todo List. Note that in the render function, the items are mapped out and wrapped in the ReactCSSTransitionGroup tag. Now what do we expect to happen with this component? Which items will be subject to the 500ms transition delay stated in the tag?
Because the original array of four items are mounted synchronously at the same time as ReactCSSTransitionGroup, they will not be subject to transition in the example above. However any items that are added or removed through the add and remove function in the example will have transition applied to them. See css styling below for exactly how this works:
Note that by setting transitionName to example as part of the ReactCSSTransitionGroup tag, we have gained access to four css states, two for entering(mounting) and two for leaving(dismounting).
Note also, very important, that in order for ReactCSSTransitionGroup to recognize its children as distinct children and know which child will mount or unmount, each child must be given an unique key.
Appear transitions on the other hand are used for cases such as the four original items in the getInitialState array. These items may be mounted at the same time as ReactCSSTransitionGroup, but by setting the transitionAppear property to true in the tag, those items will be subject to the transition delay as well. However it must be noted that based on current capabilities of the ReactTransitionGroup, this synchronous Appear transition only applies for mounting and no such feature exists for unmounting of components. See my linked demo for how this works in action, note that the Child page has an exact copy of the Enter/Leave example given by the React documentations, and the Grandchild page has an Appear twist on the same example. Also see below for Appear transition example and related css:
Page Sliding Transition Animations with React
Now onto the good stuff. Page sliding transitions are actually fairly simple, same old animated transitions mentioned above. How it works is that upon mounting the new page, the initial css state will have the page positioned either to the left or to the right of the current page, and then the new page will be transitioned into the current page through the horizontal transform property (transform: translate3d(x, y, z)). The css for this typically looks something like this:
Doesn’t seem too complex right? You might have also guessed that we simply do the reverse of this when unmounting the page. Mounting and unmounting, sounds like a job for the React transition group to me. It also sounds like we will want to use the Enter/Leave transitions as opposed to the Appear transitions, because with the Appear transitions you can only animate while mounting, not unmounting. The question then becomes: Where exactly do we pop in this ReactCSSTransitionGroup tag, and how do we make sure its children (the pages) are mounted asynchronously? Also what value should we state as the key of each child page for the react transition tag to recognize them properly?
React Routes and React Page Transitions
To answer the questions above, the react transition tag is to be placed around a react route’s children routes. To learn more about the react router see the following documentation:
The idea is that you nest routes in a tree of routes, each route being able to have children routes with the root route being “/”. Now if we wrap the ReactCSSTransitionGroup tag around a route’s immediate children, then the resulting css states will be able to transition the children pages in and out as we desire. Note that switching to a child page is an asynchronous action that unmounts a current component and mounts a new component in typical use cases, therefore this is a perfect target for Enter/Leave transitions. Everything appears to be lined up for success.
However things are never that easy, there are a few caveats we need to watch out for. See code below:
The key issue being resolved with the code above is that every child wrapped in a react transition tag needs an unique key in order for the transition group to properly recognize when it mounts and unmounts. This is tricky for two reasons:
- One can’t simply pass props such as a key with the children props.
- The keys need to be just unique enough to cover the immediate level of children for the current route, if grandchildren routes are covered with unique keys as well it will confuse react and cause unwanted transitions.
The first problem is somewhat simple to solve, there’s a pattern with react routes that’s fairly commonly used for passing props into route children, and that’s to use React cloneElement to clone the route children and pass in props as part of the function as shown below.
{React.cloneElement(this.props.children, { key: segment})}
One thing to note about this is that if the current react route that you are in is such that there are no active children, this will break your code and throw an error. This happens at the root route of “/”, unless you do something to prevent it. One way to prevent it is to use IndexRoute to create a mock, or real component. The component referenced by IndexRoute will also have the root path of “/”, and it will be a child of the root path as well, thus avoiding any problems with cloneElement.
var routes = (
<Route path=”/” component={App}>
<IndexRoute component={MockIndex}/>
<Route path=”child” component={Child}>
<Route path=”grandchild” component={Grandchild}/>
</Route>
</Route>
);
Now that we are able to pass in a key prop, what key should we pass in? In the below example I used the current react route path name.
var path = this.props.location.pathname;
return (
<ReactCSSTransitionGroup transitionName=”pageSlider”
transitionEnterTimeout={600} transitionLeaveTimeout={600}>
{React.cloneElement(this.props.children, { key: path })}
</ReactCSSTransitionGroup>
)
This will give us an unique key to pass to the child, however the problem is that if the path is nested one level too deep such as a grandchild of root path, this will confuse react since the child component will still be mounted, but given a different key. This will result in react unmounting the child and remounting it with a different key (the grandchild path), pretty odd. Thus I split the path and picked what would be the child segment of the path and set that as the key below.
var path = this.props.location.pathname;
var segment = path.split(‘/’)[1] || ‘root’;
return (
<ReactCSSTransitionGroup transitionName=”pageSlider”
transitionEnterTimeout={600} transitionLeaveTimeout={600}>
{React.cloneElement(this.props.children, { key: segment })}
</ReactCSSTransitionGroup>
)
Note that choosing the proper segment as the key is a critical part of making page sliding work. In a more complex path branch, more logic will be involved for selecting the proper key for immediate children. For example if the immediate children have paths such as
this.props.location.pathname #=> "/path/:pathKey"
then one may want to choose
path.split(“/”)[2]
instead of
path.split(“/”)[1]
With a bit of thinking one should be able to write code that selects the exact unique keys for any sets of children paths.
Now just to nail this all in, let’s see some code that allows page sliding from child to grandchild of root path:
Note that in this example, we are already in a child path, therefore we can’t use IndexRoute to create a mock child for React.cloneElement so it doesn’t freak out. Instead, we conditionally check to see if there are children or not, and only call cloneElement if children are present. Also note that in this case I did not expect the children to go past grandchild tier, therefore I simply used the pathname as the key. Once again choosing the proper key with the perfect uniqueness is critical, and depends totally on your routes tree.
One last thing is that the transition name of the React transition group tag can be dynamically changed to compliment the changing react routes as well as other conditions that change dependent on what child is being mounted/unmounted. This will potentially create different sets of CSS states accessible for us to create CSS transitions that are child specific. See JSX and CSS example below from my capstone project BandHaven:
This created four transition states each for page swapping and reverse page swapping, allowing dynamic transition animations to take place depending on the path segment and which child is being mounted/unmounted.
Well that’s it! Hopefully some of you found this useful. Now go slide those pages.
Portfolio: huan-ji.github.io
GitHub: www.github.com/huan-ji
LinkedIn: www.linkedin.com/in/huanji