How to manage page transition animations in React and GSAP

Adam Mackintosh
14 min readApr 8, 2018

--

You need to handle page transitions and you want a production-grade solution. Continue reading to win.

Here are some things a normal human might use:

  • React Router V4
  • React Transition Group V2
  • 404 handling

Here is what a normal human could produce by reading further:

CodeSandbox

Radical Note: The animations aren’t quite coming through in the CodeSandbox. I alerted the creator of CodeSandbox, so he is aware. It works sometimes and usually not. Honestly, it’s probably because GSAP is some advanced stuff, and you are basically doing some React inception by loading a DOM inside the DOM of this Medium page. GSAP is mildly too advanced for us to handle. The technology we are discussing works, so delete your qualms.

First, Critical Legacy Information

For react-transition-group in React, there is an old API and a new API, and there are some significant differences, but the overarching concepts are the same. Overall, the solution to page transitions is fairly platform agnostic. We are setting up hooks that relate to routing events.

Similar to adding event listeners, we can add animation hooks. We generally want to hook in directly to the router because we want to intercept the routing event, fire custom logic, and then unmount the component after that custom logic has completed. Besides that, animating page entrance is relatively easy. A person could accomplish that using only CSS and a lifecycle method. The component unmounting is the hard part and central to this article. But first, we should revisit some history.

V1

Transition Group V1 utilizes lifecycle methods:

  • componentWillAppear()
  • componentDidAppear()
  • componentWillEnter()
  • componentDidEnter()
  • componentWillLeave()
  • componentDidLeave()

If you see code that utilizes these hooks, the operations inside these functions are still relevant. You can swap them into the V2 hooks.

If you prefer this V1 API, then

// Note the package nameimport ReactTransitionGroup from 'react-addons-transition-group'const ReactTransitionGroup = require('react-addons-transition-group')

V2

If you prefer the V2 API, then have a quick sample of the Documentation:

onEnter={() => { console.log(key + ' enter') }}
onEntering={() => { console.log(key + ' entering') }}
onEntered={() => { console.log(key + ' entered') }}
onExit={() => { console.log(key + ' exit') }}
onExiting={() => { console.log(key + ' exiting') }}
onExited={() => { console.log(key + ' exited') }}

Based on my experience:

  • react-addons-css-transition-group: old garbage
  • react-addons-transition-group: old garbage

NOTE: I’m just kidding — they aren’t garbage, but they are basically deprecated in favor of react-transition-group.

As with most programs ever made, the V2 version addresses problems some people were experiencing in V1. The difference is these libraries are affiliated with Facebook, so we are talking about at-scale production quality for both V1 and V2.

The reason V2 is a different library is that some people prefer V1 and engineers at Facebook currently do not like breaking-changes. If breaking-changes were a neighbourhood child, Facebook would not appreciate you associating with this child. Mr. Rogers would become irate if he saw that kind of activity. Long story short, it’s a different API solving the same challenges.

INSTALLATION

Ok, yolo the packages down from your package manager, such as NPM:

$ npm install --save react-router-dom
$ npm install --save react-router-redux
$ npm install --save react-transition-group
// Pro-style efficient:
npm install --save react-router-dom react-router-redux react-transition-group

NOTE: You can use BrowserRouter or any other combination of stuff, but I am pushing my ideals on you.

The thing that is special about react-router-redux is the associated middleware helpers — action creators such as goBack() and push(). I find them timely and ready for any occasion. I also agree with the routerReducer because of its utility.

BONUS NOTE: react-router-redux reminds me of react-navigation from React Native. I like it when I can solve the same problems with the same patterns. It’s less brutal cognitive load 24/7.

Many combinations of react-router are excellent.

Step 1

We don’t have time to explore this level of the architecture, but you should see something like this in your entry point:

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'react-router-redux'
import store, { history } from './store'
import Routes from './Routes'
import './index.css'
render(
<Provider store={store}>
<ConnectedRouter history={history}>
<Routes />
</ConnectedRouter>
</Provider>,
document.getElementById('root')
)

Step 2

WTF is going on in the ./Routes file you say? Good question, and the answer is everything we care about today. We will analyze the code closer soon.

IMPLEMENTATION DETAILS

Everything hinges on the moment the browser switches to a different route. It is important we talk about this. I want to be sure you are thinking in the same context as me. Let’s step back. We are animating a transition, so what even is a transition?

A transition has an exit event and an enter event.

Transition Lifecycle Hooks

You exit from one page and enter into the next page — unless it’s your first page ever. Then, you would just have an enter event. This would imply hooks would be useful. You and me both would want to hook into those events and trigger things… things such as functions that trigger animations.

Once you get the hooks setup, you are free to use whatever wild techniques you can dream up because the hook function provides a licence to unlimited operational power.

We could have hooks at these points in time:

  1. When we are about to start entering
  2. When we started entering
  3. When we are done entering
  4. When we are about to start exiting
  5. When we started exiting
  6. When we are done exiting

Some of those sound similar and maybe even a bit redundant, but they are timely when you want full control. The hook may signal to Batman or initiate other processes.

Example: When you are about to start exiting, let me know.

Think of hooks as places before, during, and after an event that Batman could grapple to or from.

For now, baby steps — all we care about are theconsole.logs. I’ll show them again:

onEnter={() => console.log(`${key} enter`)}
onEntering={() => console.log(`${key} entering`)}
onEntered={() => console.log(`${key} entered`)}
onExit={() => console.log(`${key} exit`)}
onExiting={() => console.log(`${key} exiting`)}
onExited={() => console.log(`${key} exited`)}

What you do with these hooks is mostly outside the scope of this article. Once we empower the app to listen to every stage in the page transition lifecycle, we can trigger the start or end of any animations. Each hook occurs at a very specific point in time.

Special Note: Remember, these are events and we are thinking in event streams. We don’t care when an event actually happens. We are seeking to declare what should happen whenever an event happens. We are looking to control these events.

Stopping animations mid-way is a little more advanced. We won’t get into that, but true to React’s form, there are ways to asynchronously cancel in-flight operations by using stream-related tactics (ie: functional reactive programming).

Pardon my digression. I am trying to jam pack extra-learning into your brain like cramming suitcases into a taxi on the side of a Bangladesh highway during rush hour. In short, only two things happen: one page exits and one page enters, but we have 6 hooks available to support the craziest of situations.

GSAP

GSAP with React allows you to turn the dial to 11 out of 10, like showing up to a knife fight with Elon Musk after he had 6 months to prepare.

GSAP is all about declarative nature. You specify how stuff looks before and how it should look after, and then GSAP does magic. We usually call them tweens, because stuff happens between point A and B. We don’t care how you do it GSAP, just make it happen ASAP, thanks. You can control timing as well. GSAP doesn’t have to do its magic ASAP.

react-gsap-enhancer

Your animations can escalate quickly with GSAP. More advanced users may enjoy reading about react-gsap-enhancer.

See: https://github.com/azazdeaz/react-gsap-enhancer

We like declarative animations.

React-Router and Transition Group in 3 steps:

  1. Declare routes
  2. Filter page requests based on URL path
  3. Decorate Component with transition hooks

You were probably already doing step 1 and 2 before you started reading this article. Let’s complete the triangular geometry by working on step 3:

import React from 'react'
import { render } from 'react-dom'
import { BrowserRouter, Route, Link } from 'react-router-dom'
import { TransitionGroup, Transition } from 'react-transition-group'
import matchPath from 'react-router-dom/matchPath'
// Declare your routes
const routes = [{
key : 'home',
path : '/',
content : 'Homepage',
exact : true
}, {
key : 'about',
path : '/about',
content : 'This is all about',
exact : true
}]
// Utility function
const filterRoutes = (location) => {
return routes.filter(({ path, strict, exact }) => {
return !!matchPath(location.pathname, {
path,
strict,
exact
})
})
}
// Rename App to Router for your app
const App = () => (
<div>
<div style={{ border : '1px solid black', padding : '.2em' }}>
{routes.map(({ path, key }) => (
<Link key={'link-' + key} to={path} style={{ margin : '0 1em'}}
>
{key}
</Link>
))}
</div>
<Route render={({ location }) => (
<TransitionGroup appear={true}>
{filterRoutes(location).map(({ key, content, ...props }) => (
<Transition
key={'child-' + key}
timeout={0}
onEnter={() => { console.log(key + ' enter') }}
onEntering={() => { console.log(key + ' entering') }}
onEntered={() => { console.log(key + ' entered') }}
onExit={() => { console.log(key + ' exit') }}
onExiting={() => { console.log(key + ' exiting') }}
onExited={() => { console.log(key + ' exited') }}
>
<Route
{...props}
location={location}
render={() => (
<div>{content}</div>
)}
/>
</Transition>
))}
</TransitionGroup>
)}/>
</div>
)
render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
)

That is the baby version for theoretical purposes. It works, and you can paste the code into your app or CodeSandbox and test those console.logs; however, you would want to die if you had to scale that.

What are we missing?

  • No 404 handling
  • Routes list doesn’t contain respective Component

GRADUATION

Ok, so now, things are about to escalate into full-blown wizard fuel.

The first code example was Bruce Wayne.

Here is Batman:

import React, { Component } from 'react'
import { Route, NavLink } from 'react-router-dom'
import { TransitionGroup, Transition } from 'react-transition-group'
import Nav from './components/nav/Nav'
import Home from './containers/home/Home'
import About from './containers/About/About'
import Products from './containers/Products/Products'
import Contact from './containers/Contact/Contact'
import NotFound from './containers/not-found'
import matchPath from 'react-router-dom/matchPath'
import './components/nav/Nav.css'
// Declare the routes again, this time more serious
const routes = [
{
component: Home,
showInMenu: false,
key: 'home',
path: '/',
id: 'home',
title: 'Welcome and YOLO | Site',
description: 'If you are using react-helmet, you can read this description.',
exact: true
},
{
component: About,
showInMenu: true,
key: 'about',
path: '/about',
id: 'about',
title: 'Learn about stuff | Site',
description: 'This is all about Lorem Ipsum',
exact: true
},
{
component: Products,
showInMenu: true,
key: 'products',
path: '/products',
id: 'products',
title: 'Check out our wares | Site',
description: 'This is all about my products',
exact: true
},
{
component: Contact,
showInMenu: true,
key: 'contact',
path: '/contact',
id: 'contact',
title: 'Let us know your negative feedback | Site',
description: 'Contact info',
exact: false
},
]
/**
* filterRoutes returns true or false and compares
* location.pathname with the path key
* in each Object declared above in the routes Array.
*/
const filterRoutes = (location) => {
return routes.filter(({ path, strict, exact }) => {
return !!matchPath(location.pathname, {
path,
strict,
exact
})
})
}
// I set this up as a Class Component
// You may find yourself adding Class Methods for the transition stages, etc.
class Routes extends Component {
constructor(props) {
super(props)
this.state = {}
}
render() {
// First, generate a Menu Link only if showInMenu === true
// showInMenu comes from the routes Array, above.
return (
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
<Nav>
{routes.map(({ showInMenu, path, key }) => {
return showInMenu && (
<NavLink
key={`link-${key}`}
to={path}
className="Nav_link"
activeClassName="activeRoute"
activeStyle={{ color: '#66FCF1' }}
style={{}}
>
{key}
</NavLink>
)
})}
</Nav>
<Route render={({ location }) => {
// Handle 404 with custom animations
if (!filterRoutes(location).length) {
return (
<TransitionGroup appear>
<Transition
key="404"
timeout={0}
onEnter={() => console.log('notFound enter')}
onEntering={() => console.log('notFound entering')}
onEntered={() => console.log('notFound entered')}
onExit={() => console.log('notFound exit')}
onExiting={() => console.log('notFound exiting')}
onExited={() => console.log('notFound exited')}
>
<NotFound location={location} />
</Transition>
</TransitionGroup>
)
}
// Otherwise, observe root level path to find correct Component
const path = `/${location.pathname.split('/')[1]}`
return (
<TransitionGroup appear>
{filterRoutes(location).map(({ key, ...props }) => (
<Transition
key={'child-' + key}
timeout={500}
onEnter={() => console.log(`key ${enter}`)}
onEntering={() => console.log(`key ${entering}`)}
onEntered={() => console.log(`key ${entered}`)}
onExit={() => console.log(`key ${edit}`)}
onExiting={() => console.log(`key ${exiting}`)}
onExited={() => console.log(`key ${exited}`)}
>
{React.createElement(routes.find(r => r.path === path).component, {
...props,
location,
}, null)}
</Transition>
))}
</TransitionGroup>
)
}}/>
</div>
)
}
}
export default Routes

FORENSIC ANALYSIS

First

The page GET request comes in:

<Route render={({ location }) => {

React-Router examines this.props.location.

Second

React-Router consults with the routes Array and asks it if a route exists with the requested pathname.

We utilize the blessed Splat Route.

This image also reminds me of oldschool Batman, so you have my permission to experience positive emotion.

If no route matches, render the 404 Component. The code assumes you want to display some kind of wild 404 animation sequence, like Batman walking in from the side and punching your screen from the inside, leaving a broken glass-looking overlay that fades out over 5 seconds.

For radical UX like this, we put a dedicated Transition Group with its own hooks. Feel free to modify it to suit. The code here is simply bifurcating its next step as its response to either match = no or match = yes.

If a route matches, proceed to next steps. If not, dump out to 404.

Third

Render the correct Component, but first…

The high-level overview of react-transition-group is like this:

  1. Wrap any component in a <Transition /> Component
  2. Wrap that with a <TransitionGroup /> Component
  3. Drag & drop this composed beast anywhere you like

Go and read this entire page and then come back and continue reading:

Source: https://reactcommunity.org/react-transition-group/

You don’t have to use GSAP or any other crazy solutions. You could trigger CSS transitions and couple what we have done with something like Animated.css. Your results will likely be fantastic.

Source: https://daneden.github.io/animate.css/

CSS transitions are perhaps simpler. You can add or remove CSS Classes at specified points in time or as a response to an event or action. If you find yourself having trouble with runaway transitions and strange behavior, you are probably doing something advanced and should investigate something like GSAP.

Remember, the reason you are here to begin with is that you are probably trying to harness the page exit event. Today is a good day.

Fourth

Wait, why are we using React.createElement()?

React.createElement(routes.find(r => r.path === path).component, {
...props,
location,
}, null)

Let’s break it down. That is essentially just the non-JSX version of this:

<Component {...props} location={location} />

We are using a functional approach to find the correct Component in the routes Array and then rendering it. We are required to use React.createElement() because it’s approximately the only way to render a Component that we only know when the time comes to render it. We can’t just be like: <[someVariable] />. This is why people told us we should know how to write the ugly style JSX.

React.createElement Syntax

React.createElement(ComponentName, { testProp: ‘value’ }, null)
  • ComponentName refers to a function that returns JSX or a DOM element.
  • { testProp: ‘value’ } refers to an object of supplied props
  • null refers to supplied children

Add that to your Batman Tool Belt when you think about creating enumerables and switches for deciding which component to load.

CONCLUSION

Today, you pretty much dominated it. The only thing you are missing is some actual animations. I won’t leave you empty handed.

Back in your router file, add this import:

import { handleEnterAnimation, handleExitAnimation } from './route_animations'

Then, scroll down and update the enter and exit hooks:

onEnter={handleEnterAnimation}onExit={handleExitAnimation}

Install GSAP:

$ npm install --save gsap

Finally, while installing GSAP, create route_animations.js and slap this code in there:

import { TweenLite, TweenMax } from 'gsap'// This function is called when when the animation is complete.
// Calling callbacks at the end of animations is pretty normal.
// This fancy clearProps jazz is simply wiping off any remnant CSS.
const handleComplete = target => TweenLite.set(target, {
clearProps: 'position, width, transform',
})
/**
* Notice how the node comes in as a parameter. This is incredibly
* important to understand because it means the only thing
* this function needs to operate is a node. You could call this
* function literally any time you have a node to give it.
* What is a node? It is a DOM element.
* ex: ref={(node) => {
* this.node = node
* handleAnimation(node)
* }}
* The key takeaway from this comment block is that GSAP
* doesn't care when or where you call it. It only cares about
* which DOM elements are animating and their before and after
* CSS properties.
*/
export const handleEnterAnimation = (node) => {
if (!node) return
// Cancel existing animations
TweenMax.killTweensOf(node)
const { parentNode } = node
const targetWidth = parentNode.clientWidth -
(parseFloat(getComputedStyle(parentNode).paddingLeft) * 2)
// Set element position
TweenLite.set(node, {
position: 'fixed',
scale: 0,
x: 0,
y: 100,
autoAlpha: 0,
width: targetWidth,
})
// Animate element
TweenLite.to(node, 0.5, {
force3D: true,
scale: 1,
autoAlpha: 1,
x: 0,
y: 0,
onComplete: handleComplete, // Fire this when animation finishes
onCompleteParams: [node],
})
}
export const handleExitAnimation = (node) => {
if (!node) return
// Cancel existing animations
TweenMax.killTweensOf(node)
const { parentNode } = node
const targetWidth = parentNode.clientWidth -
(parseFloat(getComputedStyle(parentNode).paddingLeft) * 2)
// Set element position
TweenLite.set(node, {
position: 'fixed',
x: 0,
width: targetWidth,
})
// Animate element
TweenLite.to(node, 0.5, {
force3D: true,
scale: 0,
position: 'fixed',
opacity: 0,
x: -100,
y: -100,
// I put this in here to force you to consider it as a hook
onComplete: () => console.log('Page exit complete.'),
})
}

I gave you a sampler pack of props so you can rapidly expedite GSAP learning. Pay bonus attention to the force3D prop because it forces usage of GPU, which as you may guess, can improve performance. You always want to put intense math calculations on the GPU if possible.

Next Steps

The fastest way to understand GSAP is to first decide what animation you want to try to pull off and second, fiddle with props until you figure it out. You will find an epic wealth of information by googling GSAP.

The important final note is that GSAP isn’t specific to React, so there is a lot of material out there about it. You aren’t limited to only using TweenLite and TweenMax as shown today. More advanced individuals will want to look at react-gsap-enhancer, TimelineLite, and TimelineMax. There are countless sweet mods you can investigate.

There is also a forum you can join: https://greensock.com/forums/

I’m not affiliated with it, so if it sucks, that’s not my problem.

PRO TIP: The forum doesn’t suck.

I’m going to discontinue writing now. If you follow me on Twitter, you may accelerate ahead of your peers by a significant margin. I tend to be comical while operating with extreme precision and accuracy. I like things to be perfect, but I also appreciate Pareto’s 80/20 rule. There is diminishing return near the upper bound of perfection.

ADDITIONAL FACTOIDS

If you are needing a little more data, you can start with this article here:

It contains info about various libraries that you should be aware of regardless, and it reveals how to do a complex, fluid UI animation. It contains some illustrative visuals and most importantly, some GitHub code sampler packs.

Fascinating Bonus: Another animation library came out recently, as of April 2018. It’s called react-spring, and I recommend you queue this article for delivery into your retinas:

Radical Ending Triplet

  1. Feel free to comment or troll below
  2. Feel free to clap 1–50 times
  3. Feel free to have a good day

--

--

Adam Mackintosh

I prefer to work near ES6+, node.js, microservices, Neo4j, React, React Native; I compose functions and avoid classes unless private state is desired.