How to manage page transition animations in React and GSAP

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

CodeSandbox

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.

V1

Transition Group V1 utilizes lifecycle methods:

  • componentWillAppear()
  • componentDidAppear()
  • componentWillEnter()
  • componentDidEnter()
  • componentWillLeave()
  • componentDidLeave()
// 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') }}
  • react-addons-css-transition-group: old garbage
  • react-addons-transition-group: old garbage

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

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.

  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
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`)}

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.

react-gsap-enhancer

Your animations can escalate quickly with GSAP. More advanced users may enjoy reading about 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
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')
)
  • No 404 handling
  • Routes list doesn’t contain respective Component

GRADUATION

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

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 }) => {

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.

Third

Render the correct Component, but first…

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

Fourth

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

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

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.

import { handleEnterAnimation, handleExitAnimation } from './route_animations'
onEnter={handleEnterAnimation}onExit={handleExitAnimation}
$ npm install --save gsap
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.'),
})
}

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.

ADDITIONAL FACTOIDS

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

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

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Adam Mackintosh

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.