How to manage page transition animations in React and GSAP

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

CodeSandbox

First, Critical Legacy Information

V1

  • componentWillAppear()
  • componentDidAppear()
  • componentWillEnter()
  • componentDidEnter()
  • componentWillLeave()
  • componentDidLeave()
// Note the package nameimport ReactTransitionGroup from 'react-addons-transition-group'const ReactTransitionGroup = require('react-addons-transition-group')

V2

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

$ 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

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

IMPLEMENTATION DETAILS

A transition has an exit event and an enter event.

Transition Lifecycle Hooks

  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

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

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

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

Second

We utilize the blessed Splat Route.

Third

  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

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

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

ADDITIONAL FACTOIDS

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

--

--

--

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

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Running React with Ionic Capacitor and Live Reload

Using GOV.UK Notify in a Dynamics CRM Plugin Without ILMerge

Hackerrank Challenge: Solving the Repeated String in Javascript

The essential difference between pure and impure pipes in Angular and why that matters

toLocaleString() and world clock demo

Symphony is Live On Testnet

ng-content: The hidden docs

Project Planning for a MERN Stack Project

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.

More from Medium

How to design like a front-end developer — Design Stack & Tech Stack

Icon set Tech Stack and Design Stack

Create a React web application and publish it for free using mdb

Three things for any web application design and resources ( to find ‘em)

Designing

Color Scheme and Dark/Light Mode Theming in React 17.x using CSS Only — Part 1