A shallow dive into React Router v4 Animated Transitions

A slightly edited conversation from Reactiflux

It can be a bit confusing getting animated transitions setup with React Router v4. This article won’t cover every scenario, but we’ll go over the basics so that you can add animated transitions to your application.

react-transition-group's <CSSTransitionGroup> is the easiest way to animate our navigation transitions, so we’ll be using that.

Understanding the Transition Group

The basic explanation of how the <CSSTransitionGroup> works is that it keeps track of the key prop of each of its children. It stores its children in state and when it updates it compares the children in state with its new props.children to determine which ones are "entering", which are "leaving", and which are staying the same. It then renders all of its children (merges state.children and props.children), giving them classes based on their state.

This means that if you initially create the following elements:

<CSSTransitionGroup>
<Thing key='one'>
</CSSTransitionGroup>

and then re-render using these elements:

<CSSTransitionGroup>
<Thing key='two'>
</CSSTransitionGroup>

The <CSSTransitionGroup> will actually render both child elements (until the leave timeout happens)

<CSSTransitionGroup>
<Thing key='one'> // leaving
<Thing key='two'> // entering
</CSSTransitionGroup>

Of course I’m skipping over all of the props that you pass to the <CSSTransitionGroup>, but the more important thing here is to understand that what gets rendered by the <CSSTransitionGroup> isn’t just its current children prop.

Adding Transitions in React Router

By default, React Router’s <Route> component uses the location from the context to determine whether or not it matches. Normally this is fine, but it runs into an issue when you have routes that are “leaving”. A “leaving” route should actually be rendering with the previous location.

The way that React Router deals with this is with the location prop, which you can pass to both <Route>s and <Switch>es. Both of those components will prefer to match using props.location over context.location.

// this will render using context.location
<Route
path='/context'
component={FromContext}
/>
// while this will render using props.location
<Route
path='/props'
component={FromProps}
location={{ pathname: '/props', ... }}
/>

Animated <Switch>es, not <Route>s

This isn’t a requirement, but I find that you should almost always wrap your <Route>s in a <Switch>. This means that only one route will be rendering at a time.

With this pattern, we will be animating our <Switch> instead of our <Route>s. This can be weird to think about because the <Switch> is just a container, but I find it easier to reason about if you mentally transform this:

<Switch>
<Route path='/one' component={One}>
<Route path='/two' component={Two}>
</Switch>

into this:

<Switch
routes={[
<Route path='/one' component={One}>,
<Route path='/two' component={Two}>
]}
/>

Maybe that is just me, but when there is only one element, I feel that it is more obvious that we need to transition the <Switch> and not our <Route>s.

Actually Animating

We can translate those above examples to <Switch> components.

We know that we need to set a unique key on the element we are transitioning, so we will use thelocation.key property. There are a few ways to ensure that we have access to a location object.

// If the component that renders our component has the location
// prop, we can just pass it directly
// <SomeComponent location={props.location} />
//
// If the component does not have the location, we can render it
// using a <Route> to inject the location as a prop
// <Route component={SomeComponent} />
//
// Last, we can just wrap the component with the withRouter
// higher order component, which is what we'll do here
const SomeComponent = withRouter(({ location }) => (
<CSSTransitionGroup>
<Switch key={location.key}>
<Route path='/one' component={One} />
<Route path='/two' component={Two} />
</Switch>
</CSSTransitionGroup>
)

If we just do the above, then our <Switch> will match using the location from context. Let’s take a look at what that will render.

We start by rendering using the first location.

// location = { pathname: '/one', key: 'first' }
<CSSTransitionGroup>
<Switch key={location.key}>...</Switch> // key = 'first'
</CSSTransitionGroup>

Our <CSSTransitionGroup> will render our <Switch>, which uses context.location to determine which <Route> matches. In this case, that is the <Route> with the path /one.

Next, the user navigates to page /two, so we will re-render using the new location. The new location has a different key, so we will end up with a new <Switch> element.

// location = { pathname: '/two', key: 'second' }
<CSSTransitionGroup>
<Switch key={location.key}>...</Switch> // key = 'second'
</CSSTransitionGroup>

Our new <Switch> uses context.location to match routes, which matches the <Route> with the path /two.

While we create a new <Switch> element, our transition group has stored our original <Switch> element, so we actually end up rendering the following:

// location = { pathname: '/two', key: 'second' }
<CSSTransitionGroup>
<Switch key={'first'}>...</Switch> // matches the /two route
<Switch key={'second'}>...</Switch> // matches the /two route
</CSSTransitionGroup>

We can see this code in action here:

When we transition using the context, both <Switch>es animate using the new location

Uhh, shoot. Our original <Switch> is matching using context.location, whose pathname is /two, so now it is rendering <Route path='/two'> as well.

Props!

This is where the location prop comes into play. If we add the current location to each <Switch>, then when they re-render, they will match using the location prop that they were given instead of context.location.

Let’s walk back over the example code from above, this time adding the location prop to our <Switch>es. We again start at /one.

// location = { pathname: '/one', key: 'first' }
<CSSTransitionGroup>
<Switch
location={location} // { pathname: '/one', ... }
key={location.key}
>...</Switch>
</CSSTransitionGroup>

That matches the /one <Route>. Next, we transition to /two.

// location = { pathname: '/two', key: 'second' }
<CSSTransitionGroup>
<Switch
location={location} // { pathname: '/two', ... }
key={location.key}
>...</Switch>
</CSSTransitionGroup>

That matches the /two <Route>. Of course, because we are using <CSSTransitionGroup>, we are actually rendering two <Switch>es, so our element tree will be the following:

// location = { pathname: '/two', key: 'second' }
<CSSTransitionGroup>
<Switch
key={'first'}
location={{ pathname: '/one', ... }}
>...</Switch>
<Switch
key={'second'}
location={{ pathname: '/two', ... }}
>...</Switch>
</CSSTransitionGroup>

Now, both of our <Switch>es will match using the correct location.

We can see that it works with the followingCodeSandbox demo. Also, please note that this is not a full <CSSTransitionGroup> demo. In a real application, you would be applying some CSS transition that fades, slides, or does some other fancy transition. This demo is just to demonstrate that using <Switch key> will result in the correct locations being rendered.

Review

The key and location props are key to animating route transitions for React Router. The key needs to be a unique value for each location, so location.key is a convenient value to use. The location needs to be the location object that a <Switch>/<Route> should matching using.

This works well for flat route structures, but can run into issues when you attempt to animate nested routes. If there is enough interest (ping me on Twitter @pshrmn or leave a response here), I can write an additional article on how to deal with that, but it isn’t something that you can accomplish with just the <Route>/<Switch> components.