Navigate back to the homepage

Dynamic transitions with react-router and react-transition-group

Nicolas Girault
March 18th, 2018 · 3 min read

Here is a demo and the associated source code of what is explained in this article.

The problem

React-router and react-transition-group are two widely used librairies that can be combined to create transitions between routes.

However, in react-transition-group, once a component is mounted, its exit animation is specified, not changeable. Thus, dealing with transitions depending of the next state (what I call dynamic transitions) is challenging with this library.

The exiting transition of state A does not depend only from state A (dynamic transition)

The exiting transition of state A does not depend only from state A (“dynamic transition”)

Although a simple example is available on react-router doc, it is not easy to tweak it to create more sophisticated use cases such as dynamic transitions. In this article, I’ll explain how to do so thanks to react-router v4 and react-transition-group v2.

1. Understanding the simple example

If you are on your way developing transitions between pages of your react app, you might have already met this code snippet from react-router doc (adapted with state A/B):

1<TransitionGroup>
2 <CSSTransition key={location.key} classNames="fade" timeout={300}>
3 <Switch location={location}>
4 <Route exact path="/state-a" component={A} />
5 <Route exact path="/state-b" component={B} />
6 </Switch>
7 </CSSTransition>
8</TransitionGroup>

Understanding why this piece of code allows a transition between two routes is not obvious. However, it is necessary to implement a more sophisticated use case such as dynamic transitions.

About TransitionGroup

I will rephrase what is already written in this article: a shallow dive into react router animated transitions.

When transitioning from state A to state B, location.key value changes (let’s say from A to B) so without a <TransitionGroup> wrapping <CSSTransition key={location.key}> , the <CSSTransition key='A'> would be unmounted and a new <CSSTransition key='B'> would be mounted (because react identifies elements thanks to key).

However, <TransitionGroup> tracks its children by key and when one of its children disappears, it keeps rendering it for the time of the transition. So during the time of the transition, the above TransitionGroup would render something similar to this:

1<div>
2 <CSSTransition key="A" leaving>
3 <Switch location={location}>
4 <Route exact path="/state-a" component={A} />
5 <Route exact path="/state-b" component={B} />
6 </Switch>
7 </CSSTransition>
8 <CSSTransition key="B" entering>
9 <Switch location={location}>
10 <Route exact path="/state-a" component={A} />
11 <Route exact path="/state-b" component={B} />
12 </Switch>
13 </CSSTransition>
14</div>

About Switch

You simply need to understand that when location.pathname is /state-a, this:

1<Switch>
2 <Route exact path="/state-a" component={A} />
3 <Route exact path="/state-b" component={B} />
4</Switch>

it renders:

1<A />

Why you need to pass a location prop to the switch?

By default, a switch uses history.location to select the route to render. However you can provide a location prop to the switch that will override the default history.location value:

1<TransitionGroup>
2 <CSSTransition key={location.key} classNames="fade" timeout={300}>
3 <Switch location={location}>
4 <Route exact path="/state-a" component={A} />
5 <Route exact path="/state-b" component={B} />
6 </Switch>
7 </CSSTransition>
8</TransitionGroup>

So why this location (provided by withRouter or available within a route component) must be added as a prop to the switch in a transition use case? (see the origin of this requirement, in this issue)

history.location is a live object whereas the location provided by withRouter is immutable (see doc). Thus, without providing a location prop to the switch, the switch would always match the route according to the current location (the location of history.location). So during the transition (the current location is B), the <TransitionGroup> would render:

1<div>
2 <CSSTransition key="A" leaving>
3 <B />
4 </CSSTransition>
5 <CSSTransition key="B" entering>
6 <B />
7 </CSSTransition>
8</div>

However, if you pass a location to the switch, the switch will use this prop instead of history.location and since location is immutable, the previous <CSSTransition> received the previous location and the new <CSSTransition> receives the new location.

1<div>
2 <CSSTransition key="A" leaving>
3 <A />
4 </CSSTransition>
5 <CSSTransition key="B" entering>
6 <B />
7 </CSSTransition>
8</div>

Thereby the leaving <CSSTransition> will still render an old route even if a new location has been pushed to the history.

2. Dealing with dynamic transitions

Dealing with dynamic transitions is not straight forward. An issue on react-transition-group is open to consider this problem.

As explained in the issue:

once a component is mounted, its exit animation is specified, not changeable.

Indeed, in this code snippet:

1<TransitionGroup>
2 <CSSTransition key={location.key} classNames="fade" timeout={300}>
3 <Switch location={location}>
4 <Route exact path="/state-a" component={A} />
5 <Route exact path="/state-b" component={B} />
6 </Switch>
7 </CSSTransition>
8</TransitionGroup>

only the current (entering) child is accessible. The exiting one has already been removed. It is only living within the <TransitionGroup> state.

Fortunately the <TransitionGroup> component can receive a childFactory. The doc says:

If you do need to update a child as it leaves you can provide a childFactory to wrap every child, even the ones that are leaving.

So the childFactory prop makes it possible to specify the leaving transition of a component after rendering it (and thus solves the problem of dynamic transitions)

1<TransitionGroup
2 childFactory={child =>
3 React.cloneElement(child, {
4 classNames: "newTransition",
5 timeout: newTimeout
6 })
7 }
8>
9 <CSSTransition key={location.key}>
10 <Switch location={location}>
11 <Route exact path="/state-a" component={A} />
12 <Route exact path="/state-b" component={B} />
13 </Switch>
14 </CSSTransition>
15</TransitionGroup>

In the above code snippet, the previous <CSSTransition> will be updated with the new transition class name and timeout.

A possible implementation of dynamic transitions

The question is now: how do you give the right classNames value according to the state transition?

A possible solution is to use the location state.

About location state in the location doc:

Normally you just use a string, but if you need to add some “location state” that will be available whenever the app returns to that specific location, you can use a location object instead. This is useful if you want to branch UI based on navigation history instead of just paths (like modals).

Here is how you could do:

1// state-a.js
2export default props => (
3 <div>
4 <button
5 onClick={() => {
6 history.push({
7 pathname: "/state-b",
8 state: { transition: "fade", duration: 300 }
9 });
10 }}
11 >
12 Go to state B
13 </button>
14 <button
15 onClick={() => {
16 history.push({
17 pathname: "/state-c",
18 state: { transition: "slide", duration: 500 }
19 });
20 }}
21 >
22 Go to state C
23 </button>
24 </div>
25);

In the routes definition file:

1<TransitionGroup
2 childFactory={child =>
3 React.cloneElement(child, {
4 classNames: location.state.transition,
5 timeout: location.state.duration
6 })
7 }
8>
9 <CSSTransition key={location.key}>
10 <Switch location={location}>
11 <Route exact path="/state-a" component={A} />
12 <Route exact path="/state-b" component={B} />
13 </Switch>
14 </CSSTransition>
15</TransitionGroup>

Now you should get 2 different transitions from the same exiting state 🎉🎉🎉. This is what we were trying to solve :-).

Demo + source code Demo + source code

Annexe A: Wrap your pages in a div

You should wrap your switch in a div until this issue is solved:

1<CSSTransition>
2 <div>
3 <Switch>...</Switch>
4 </div>
5</CSSTransition>

Otherwise you’ll get an uncaught error if any of the route you define renders null.

Annexe B: CSSTransition and styled-components

As CSS-in-JS is now a standard in react development, you might be looking for a solution to handle <CSSTransition> with CSS-in-JS. Here is my solution:

1// fade.js
2import { injectGlobal } from "styled-components";
3const transitionClassName = "fade";
4const duration = 400;
5injectGlobal`
6.${transitionClassName}-enter {
7 opacity: 0;
8}
9.${transitionClassName}-enter.${transitionClassName}-enter-active {
10 opacity: 1;
11 transition: all ${duration}ms;
12}
13.${transitionClassName}-exit {
14 opacity: 1;
15}
16.${transitionClassName}-exit.${transitionClassName}-exit-active {
17 opacity: 0;
18 transition: all ${duration}ms;
19}
20`;
21export default { transition: transitionClassName, duration };

More articles from Lalilo

Welcome

👋 This blog present Lalilo culture and is also a good place to share technical stuff

January 1st, 2018 · 1 min read

Redux-saga and Typescript, doing it right.

Redux-saga is a widely used library that helps to deal with side effects in a front-end app. Typing sagas properly is not obvious though. Here's a guide on how to do it!

February 19th, 2020 · 3 min read
© 2018–2020 Lalilo
Link to $https://twitter.com/LaliloAppLink to $https://github.com/lalaliloLink to $https://www.instagram.com/lalilo_lecture/Link to $https://www.linkedin.com/company/lalilo/