A shallow dive into React Router v4 Animated Transitions

A slightly edited conversation from Reactiflux

Edit (9–2–2017): This article has been updated to use react-transition-group v2. If you are using v1 (or the version that used to be bundled with React addons), you can see the context example here and the props example here.

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 <TransitionGroup> 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 <TransitionGroup> 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 "exiting", 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:

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

and then re-render using these elements:

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

The <TransitionGroup> will actually render both child elements (until the exit timeout happens)

<TransitionGroup>
<Thing key='one'> // exiting
<Thing key='two'> // entering
</TransitionGroup>

The most important thing here is to understand that what gets rendered by the <TransitionGroup> isn’t just its current children prop.

Children Component Types

In the previous version of react-transition-group, the children of a <TransitionGroup> could be any component type. However, in v2, these should be either <Transition>s or <CSSTransition>s. Also, transition props (transitionName, transitionEnterTimeout, etc.) were placed on the <TransitionGroup> in previous versions. In v2, these props are placed on the <Transition>/<CSSTransition> components.

<TransitionGroup>
<CSSTransition
key='...'
classNames='fade'
timeout={{ enter: 500, exit: 300 }}
>
<div />
</CSSTransition>
</TransitionGroup

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 “exiting”. A “exiting” 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 }) => (
<TransitionGroup>
<CSSTransition key={location.key}>
<Switch>
<Route path='/one' component={One} />
<Route path='/two' component={Two} />
</Switch>
</CSSTransition>
</TransitionGroup>
)

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' }
<TransitionGroup>
<CSSTransition key={location.key}> // key = 'first'
<Switch>...</Switch>
</CSSTransition>
</TransitionGroup>

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' }
<TransitionGroup>
<CSSTransition key={location.key}> // key = 'second'
<Switch>...</Switch>
</CSSTransition>
</TransitionGroup>

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>
// renders the two route
<CSSTransition key={'first'}>...</CSSTransition>
// renders the two route
<CSSTransition key={'second'}>...</CSSTransition>
</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' }
<TransitionGroup>
<CSSTransition key={location.key}
<Switch
location={location} // { pathname: '/one', ... }
>...</Switch>
</CSSTransition>
</TransitionGroup>

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

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

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>
<CSSTransition key={'first'}>
<Switch location={{ pathname: '/one', ... }}>...</Switch
</CSSTransition>
<CSSTransition key={'second'}>
<Switch location={{ pathname: '/two', ... }}>...</Switch
</CSSTransition>
</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 <TransitionGroup> 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 location> 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.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.