Animations with React Router

Stephen Cook
Onfido Product and Tech
7 min readJul 24, 2018

--

When used correctly, animations can greatly improve the user experience. But animating on a page transition which is handled by React Router can be a huge pain — one that we’ve recently had to deal with at Onfido.

This post is further to a talk I gave at JS Monthly, but I’ll go into a bit more technical detail here.

The end result we’re going for is something like this:

Demo for animated React Router code

The Challenge

First of all, let’s explore why using React Router makes animating inherently hard.

React Router is designed to immediately update the view-tree according to the current page location. When moving from Page APage B, React Router does not provide any hooks to keep Page A visible while it animates out. It immediately removes it from the DOM, once its route is no longer active.

There are some naive solutions that immediately jump to mind, but they have hefty drawbacks:

Naive Solution #1

Solution: Animating Page A manually before the actual history change.

Problem: Every single route away from Page A needs to trigger this animation hook, otherwise animation will not occur. Navigation using the browser directly (e.g. using the back button) will result in no animations.

Naive Solution #2:

Solution: Remove Page A and Page B from the main React Router code, manually listen to the history API, and trigger animations accordingly.

Problem: This will work, but your code will be horrible. Some of your routes will be using React Router, and some will handled manually. Even if you manage to make your custom routing code clean, it’s still two different routing solutions in one codebase — which is, at best, confusing.

Official Solution

So what’s the official recommendation? The React Router documentation recommends using react-transition-group.

react-transition-group is a small library that can help with handling animations using React. It also has a component called TransitionGroup which triggers exit/enter animations on components as they attempt to exit/enter the view-tree.

Using this library with React Router gives us code like this:

Demo for TransitionGroup animation with React Router

Let’s quickly break down the code here, so we know what’s happening:

  • <Route render={({ location }) => (...)} /> — this does nothing, other than give us easy access to the location object, which we need later on.
  • <TransitionGroup> — this component will interact with any direct Transition or CSSTransition children — either by animating them in when they attempt to enter the view-tree, or animating them out when they attempt to exit the view-tree. This component is what’s doing all the heavy lifting for us.
  • <CSSTransition key={location.pathname} classNames="fade" timeout={600}> — we give a key based on our current location. This means whenever the location.pathname changes (i.e. our page changes) then React sees this CSSTransition element as being a “new” element, different to the last one. This causes TransitionGroup to animate out the old one, and animate in the new one.
  • <Switch location={location}> — because we actually have 2 Switch elements on the view-tree at the same time (since TransitionGroup is holding onto our previous page to animate it out, and the previous page and current page both have Switchs), we need to manually pass in the correct location to each one (since one needs the old one, and one wants the new one.

Great! So this is everything we need, right? Unfortunately, not quite. This TransitionGroup solution is awesome, but falls short in a few ways:

  1. Multiple page changes: in the Code Sandbox above, there’s a CSS selector to colour the page red if there are more than 2 pages active at any point. If you change page twice quickly, you’ll see that there end up being 3+ pages visible at any one time. This is because TransitionGroup has no idea that you’re trying to route from Page A to Page B. All it knows is to animate out old elements before removing them, and that’s all it does. This might be fine for your use-case, but it also might be a deal-breaker (or at least a source of pain).
  2. Timing: if you want Page B to start animating in after Page A has animated out, then you’re going to have a bad time. TransitionGroup has no API to specify for animations to happen after some event. Similar to (1), all TransitionGroup understands is that when an element is added to the view-tree, it should animate it in.
  3. Customisation: both (1) and (2) are symptoms of the larger issue, that TransitionGroup lacks any way to customise behaviour for a routing use-case. TransitionGroup is really designed for adding/removing elements from a list — and it excels at this. In this routing use-case however, its API is simply not powerful enough.

So really TransitionGroup is just designed for other contexts. But it’s so close! How does it work? Maybe we can scavenge its code.

Turns out that the trick involved here is… just not rendering your children prop. Instead, store it in your local state, and render from that state. This way, we can continue to render our previous child out while React Router actually wants us to be rendering the new child.

This is a super smart idea… but also super simple! We can definitely scavenge that idea.

Better Solution

A simple implementation of this is pretty achievable, so let’s just get some basic sliding animations. This solution will have the following properties:

  • Only 1 page being shown at a time
  • Page B won’t animate in, until Page A has finished animating out

This is the end code we’re aiming for, but let’s break it down step by step.

Rendering

Our first step is to render our previous child until it slides off to the left, even when React Router wants us to render our new child.

So here you can see we’re saying prevChild || curChild, or in other words: “If React Router is telling us to render a new child, ignore it and render the old child, until we’ve finished everything we’re doing with the old child”

The only other thing of interest here is the Slider class, which I won’t dive too deeply into. It’s a very simple element, that takes 2 props:

  • position: the container will animate depending on the direction you tell it, e.g. position={Slider.FROM_LEFT} would start on the left side of the screen, and animate sliding into the centre of the screen.
  • animationCallback: a callback that will be invoked whenever a sliding animation has completed.

If you’re curious about how Slider works, its code is in the CodeSandbox.

Animating on Page Change

The next step is to actually initiate these animations whenever the page changes.

componentDidUpdate will get called whenever any prop (including the children prop) changes.

So the primary code of interest here is the if statement. This determines if the page has changed by seeing if the uniqKey of each page changes. We need this unique key since it’s hard in React to determine if an element is “new” or just the same element on a new render (with new props etc.). We will provide a unique key later, so let’s take it as a given for now.

When the key has changed, it means the page has changed, so we need to do the following things:

  1. Save our old page as prevChild so we will continue to render it
  2. Set our new page in state too, so we have access to it for later
  3. Set position={Slider.TO_LEFT} so our animation component will start animating the page from the centre of the page, over to the left
  4. Set the animationCallback so this.swapChildren gets called once the animation is completed

Swap the Children

Once the animation is complete, we trigger this.swapChildren. At this point, our page is off screen — it’s animated off to the left so far, it is no longer visible. So here, we can swap the page element and begin the new animation.

We get rid of our prevChild so our current child (the new page that React Router actually wants us to render) starts being rendered.

We also set the position to FROM_RIGHT which will cause our element to suddenly jump from the left side of the screen, to the right side of the screen. It will then start to animate in from the right side of the screen, to the centre.

This CodeSandbox should demonstrate more clearly the flow of events:

Slow + captioned animation example with React Router

Integrating with React Router

The above code is all we need to create a SlideOut component that performs a slide animation whenever its uniqKey changes. We can then use this with React Router like so:

But we can also do one better, and make ourselves a Higher-Order Component to remove some verbosity:

This means that to add animation to our old React Router code:

Old (animation-less) React Router code

We just need to make this tiny addition:

New React Router code (with animation)

And we can easily add in a new animations by simply creating a new Animator — e.g. consider this SwitchObnoxious:

Conclusion

React Router inevitably makes animations difficult, since it is designed to instantly route the current page position to its corresponding view-tree, with no regard for non-instantaneous animations.

But by applying a simple technique of storing our children prop in state, we can achieve animations on page transitions, by taking control of when we unmount our old child, even when our parent wants us to do it sooner.

This is code we actually have running in production, on our Client Dashboard sign-in page. If stuff like this sounds cool to you, then you should definitely come work at Onfido! We’re hiring.

And feel free to follow me on Twitter, if you’re interested in attending talks on stuff like this in the future.

--

--

Stephen Cook
Onfido Product and Tech

Software engineer at @Thread. Saving up to fulfil true dream of professional Mario Kart… https://stephencook.dev/