An exploration of realizing transition animation by REACT-Router+REACT-Transition-Group

  animation, react-router4, react.js

Original address

1. Introduction

In daily development, transition animation during page switching is a relatively basic scene. In react project, we usually choosereact-routerTo manage routes, but react-router did not provide the corresponding transition animation function, but very stiffly directly replaced components. To some extent, the experience is not so friendly.

In order to realize animation effect in react, we actually have many choices, such as:react-transition-group,react-motion,AnimatedWait. However, due to the “enter, enter-active, exit, exit-active” hooks added to the element by the react-transition-group, it is simply designed for our page to enter and leave. Based on this, this paper chooses react-transition-group to realize animation effect.

Next, this article will combine the two to provide an idea to realize route transition animation, which is worth throwing bricks to attract jade ~

2. Requirements

Let’s first clarify what effect the transition animation to be completed is. As shown in the following figure:

3. react-router

First of all, let’s briefly introduce the basic usage of react-router (see in detailOfficial website introduction)。

Here we will use react-routerBrowserRouter,Switch,RouteThree components.

  • BrowserRouter: route implemented in the form of history api provided by html5 (there is also a route implemented in the form of hash).
  • Switch: when multiple Route components match at the same time, the default will be displayed, but the Route component wrapped by Switch will only display the first route that is matched.
  • Route: Routing component, path specifies the matching route, and COMPONENT specifies the component to be displayed when the route matches.
// src/App1/index.js
export default class App1 extends React.PureComponent {
  render() {
    return (
      <BrowserRouter>
        <Switch>
          <Route exact path={'/'} component={HomePage}/>
          <Route exact path={'/about'} component={AboutPage}/>
          <Route exact path={'/list'} component={ListPage}/>
          <Route exact path={'/detail'} component={DetailPage}/>
        </Switch>
      </BrowserRouter>
    );
  }
}

As shown above, this is the key implementation part of routing. We have created a total ofhome page,About pages,List page,Details pageThese four pages. The jump relation is:

  1. Home page about page
  2. First page list page details page

Let’s look at the current default route switching effect:

4. react-transition-group

From the above effect diagram, we can see that react-router has no transition effect at all when switching routes, but is directly replaced, which is very stiff.

As the saying goes, if a worker wants to do a good job, he must first sharpen his tools. Before introducing the realization of transition animation, we must first learn how to use react-transition-group. Based on this, the following will briefly introduce the CSSTransition and TransitionGroup provided by it.

4.1 CSSTransition

CSSTransition is a component provided by react-transition-group and its working principle is briefly introduced here.

When the in prop is set to true, the child component will first receive the class example-enter, then the example-enter-active will be added in the next tick. CSSTransition forces a reflow between before adding the example-enter-active. This is an important trick because it allows us to transition between example-enter and example-enter-active even though they were added immediately one after an other. Most notably, this is what makes it possible for us to animate appearance.

This is a description from the official website, which means that when the in attribute of CSSTransition is set to true, CSSTransition will first add xxx-enter’s class to its subcomponents, and then add xxx-enter-active’s class at the next tick. Therefore, we can make use of this, through the transition attribute of css, to make the element smoothly transition between the two states, thus obtaining the corresponding animation effect.

On the contrary, when the in property is set to false, CSSTransition adds classes of xxx-exit and xxx-exit-active to the subcomponent. (More details can be stamped.)The official websiteView)

Based on the above two points, should we just write the css style corresponding to class in advance? Can do a small demo try, as shown in the following code:

// src/App2/index.js
export default class App2 extends React.PureComponent {

  state = {show: true};

  onToggle = () => this.setState({show: !this.state.show});

  render() {
    const {show} = this.state;
    return (
      <div className={'container'}>
        <div className={'square-wrapper'}>
          <CSSTransition
            in={show}
            timeout={500}
            classNames={'fade'}
            unmountOnExit={true}
          >
            <div className={'square'} />
          </CSSTransition>
        </div>
        <Button onClick={this.onToggle}>toggle</Button>
      </div>
    );
  }
}
/* src/App2/index.css */
.fade-enter {
  opacity: 0;
  transform: translateX(100%);
}

.fade-enter-active {
  opacity: 1;
  transform: translateX(0);
  transition: all 500ms;
}

.fade-exit {
  opacity: 1;
  transform: translateX(0);
}

.fade-exit-active {
  opacity: 0;
  transform: translateX(-100%);
  transition: all 500ms;
}

Let’s take a look at the effect, is it a bit similar to the entry and exit effect of the page?

4.2 TransitionGroup

Although it is very convenient to use CSSTransition to handle animation, animation directly used to manage multiple pages is still slightly thin. For this reason, let’s introduce the TransitionGroup component provided by react-transition-group again.

The <TransitionGroup> component manages a set of transition components (<Transition> and <CSSTransition>) in a list. Like with the transition components, <TransitionGroup> is a state machine for managing the mounting and unmounting of components over time.

As described on the official website, the TransitionGroup component is a component used to manage the mounting and unmounting processes of a pile of nodes, which is very suitable for handling the situation of multiple pages here. This introduction seems a bit difficult to understand, so let’s look at the code and explain how the TransitionGroup works.

// src/App3/index.js
export default class App3 extends React.PureComponent {

  state = {num: 0};

  onToggle = () => this.setState({num: (this.state.num + 1) % 2});

  render() {
    const {num} = this.state;
    return (
      <div className={'container'}>
        <TransitionGroup className={'square-wrapper'}>
          <CSSTransition
            key={num}
            timeout={500}
            classNames={'fade'}
          >
            <div className={'square'}>{num}</div>
          </CSSTransition>
        </TransitionGroup>
        <Button onClick={this.onToggle}>toggle</Button>
      </div>
    );
  }
}

Let’s look at the effect first and then explain:

Comparing the codes of App3 and App2, we can find that this CSSTransition has no in attribute, but uses the key attribute. But why can it still work normally?

Before answering this question, let’s think about a question:

Since react’s dom diff mechanism uses the key attribute, if the key is different before and after, react will unload the old node and mount the new node. In the above code, the old node should disappear immediately due to the change of key, but why can we still see the animation process of it fading out?

The key lies in the TransitionGroup, because when it senses the change of its children, it will first save the node to be removed, and will only really remove the node at the end of its animation.

So in the above example, when we press the toggle button, the process of change can be understood as follows:

<TransitionGroup>
  <div>0</div>
</TransitionGroup>

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

<TransitionGroup>
  <div>0</div>
  <div>1</div>
</TransitionGroup>

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

<TransitionGroup>
  <div>1</div>
</TransitionGroup>

As explained above, we can skillfully use the change of key value to let the TransitionGroup take over the creation and destruction of pages during the transition, and only need to pay attention to how to select the appropriate key value and what css style is needed to realize the animation effect.

5. Page transition animation

Based on the introduction of react-router and react-transition-group, we have mastered the foundation, and then we can combine the two to make transition animation for page switching.

At the end of the previous section, it was mentioned that after the TransitionGroup was used, our problem became how to choose the appropriate key value. So in the routing system, what is more appropriate as the key value?

Since we trigger the transition animation when switching pages, it is natural that the value related to routing is appropriate as the key value. But in react-routerlocationThe object has a key attribute that changes as the address in the browser changes. However, it does not seem suitable in actual scenes, because changes in query parameters or hash will also cause changes in location.key. however, it is often not necessary to trigger transition animation in these scenes.

Therefore, I feel that the selection of the key value still depends on different items. In most cases, it is recommended to use location.pathname as the key value, because it is the route for our different pages.

Having said so much, let’s see how the specific code applies react-transition-group to react-router:

// src/App4/index.js
const Routes = withRouter(({location}) => (
  <TransitionGroup className={'router-wrapper'}>
    <CSSTransition
      timeout={5000}
      classNames={'fade'}
      key={location.pathname}
    >
      <Switch location={location}>
        <Route exact path={'/'} component={HomePage} />
        <Route exact path={'/about'} component={AboutPage} />
        <Route exact path={'/list'} component={ListPage} />
        <Route exact path={'/detail'} component={DetailPage} />
      </Switch>
    </CSSTransition>
  </TransitionGroup>
));

export default class App4 extends React.PureComponent {
  render() {
    return (
      <BrowserRouter>
        <Routes/>
      </BrowserRouter>
    );
  }
}

This is the effect:

App4 has the same code idea as App3, except that the original div is replaced by a Switch component, and withRouter is also used.

WithRouter is a high-level component provided by react-router, which can provide location, history and other objects for your component. Because we want to use location.pathname as the key value of CSSTransition, we have used it.

In addition, there is a pit here, which is the location attribute of Switch.

A location object to be used for matching children elements instead of the current history location (usually the current browser URL).

This is the description in the official website, which means that the Switch component will use this object to match the route in its children, and the default is the url of the current browser. If we don’t specify it in the above example, a strange phenomenon will occur in the transition animation, that is, two identical nodes are moving at the same time. . . Just like this:

This is because although the TransitionGroup component will retain the Switch node to be remove, when the location changes, the old Switch node will use the changed location to match the route in its children. Since location is up-to-date, the pages matched by the two Switch are the same. Fortunately, we can change the location attribute of Switch, as shown in the above code, so that it will not always match with the current location.

6. Page dynamic transition animation

Although a simple transition animation was implemented by react-transition-group and react-router, there is a serious problem. Looking closely at the diagram in the previous section, it is not difficult to find out that our animation effect of entering the next page is in line with the expectation, but what is the animation effect of retreating? . . Clearly, the previous page should fade in from the left and the current page should fade out from the right. But why does it become that the current page fades out from the left and the next page fades in from the right, the same effect as entering the next page. In fact, the reason for the mistake is very simple:

First of all, we divide routing changes into forward and back operations. In the forward operation, the exit effect of the current page is to fade out to the left; During the back operation, the exit effect of the current page is to fade out to the right. Therefore, we only use fade-exit and fade-exit-active as two class. Obviously, the animation effect obtained is definitely the same.

Therefore, the solution is also very simple, we can use two sets of class to manage the animation effect of forward and back operations respectively.

/* src/App5/index.css */

/* 路由前进时的入场/离场动画 */
.forward-enter {
  opacity: 0;
  transform: translateX(100%);
}

.forward-enter-active {
  opacity: 1;
  transform: translateX(0);
  transition: all 500ms;
}

.forward-exit {
  opacity: 1;
  transform: translateX(0);
}

.forward-exit-active {
  opacity: 0;
  transform: translateX(-100%);
  transition: all 500ms;
}

/* 路由后退时的入场/离场动画 */
.back-enter {
  opacity: 0;
  transform: translateX(-100%);
}

.back-enter-active {
  opacity: 1;
  transform: translateX(0);
  transition: all 500ms;
}

.back-exit {
  opacity: 1;
  transform: translateX(0);
}

.back-exit-active {
  opacity: 0;
  transform: translate(100%);
  transition: all 500ms;
}

However, css support alone is not enough. We have to add the appropriate class to different routing operations. Then there is the problem again. under the management of TransitionGroup, once a component is mounted, its exit animation has already been determined. you can read this on the official website.issue. In other words, even if we dynamically add different ClassNames attributes to CSSTransition to specify animation effects, it is actually invalid.

The solution is actually thereissueAs shown below, we can override its className by using the ChildFactory property of the TransitionGroup and the React.cloneElement method. For example:

<TransitionGroup childFactory={child => React.cloneElement(child, {
  classNames: 'your-animation-class-name'
})}>
  <CSSTransition>
    ...
  </CSSTransition>
</TransitionGroup>

After the above problems have been solved, the remaining problem is how to choose the appropriate animation class. The essence of this problem lies in how to judge whether the current route change is a forward or a back operation. Fortunately, react-router has been prepared for us, and the history object it provides has an action attribute that represents the type of current route change. Its value is'PUSH' | 'POP' | 'REPLACE'. So, let’s adjust the code again:

// src/App5/index.js
const ANIMATION_MAP = {
  PUSH: 'forward',
  POP: 'back'
}

const Routes = withRouter(({location, history}) => (
  <TransitionGroup
    className={'router-wrapper'}
    childFactory={child => React.cloneElement(
      child,
      {classNames: ANIMATION_MAP[history.action]}
    )}
  >
    <CSSTransition
      timeout={500}
      key={location.pathname}
    >
      <Switch location={location}>
        <Route exact path={'/'} component={HomePage} />
        <Route exact path={'/about'} component={AboutPage} />
        <Route exact path={'/list'} component={ListPage} />
        <Route exact path={'/detail'} component={DetailPage} />
      </Switch>
    </CSSTransition>
  </TransitionGroup>
));

Let’s look at the animation effect after modification:

7. Optimize

In fact, the content of this section is not optimized. The idea of transition animation has basically ended here. You can open your mind wide and add css to realize more cool transition animation. However, I still want to talk about how to make our route more configurable.

We know that react-router made a major revision when upgrading v4. Dynamic routing is preferred over static routing. However, specific problems are analyzed in detail. In some projects, individuals prefer to centralize the management of routes. For the above example, they hope to have a RouteConfig, just like the following:

// src/App6/RouteConfig.js
export const RouterConfig = [
  {
    path: '/',
    component: HomePage
  },
  {
    path: '/about',
    component: AboutPage,
    sceneConfig: {
      enter: 'from-bottom',
      exit: 'to-bottom'
    }
  },
  {
    path: '/list',
    component: ListPage,
    sceneConfig: {
      enter: 'from-right',
      exit: 'to-right'
    }
  },
  {
    path: '/detail',
    component: DetailPage,
    sceneConfig: {
      enter: 'from-right',
      exit: 'to-right'
    }
  }
];

Through RouterConfig above, we can clearly know which component each page corresponds to, and also know what the transition animation effect is, such asAbout pagesIt enters the page from the bottom.List pageAndDetails pageThey all enter the page from the right. In a word, we can get a lot of useful information directly through this static route configuration table, without going deep into the code to get the information.

So, how do we need to adjust our corresponding routing codes for the above requirement? Please look at the following:

// src/App6/index.js
const DEFAULT_SCENE_CONFIG = {
  enter: 'from-right',
  exit: 'to-exit'
};

const getSceneConfig = location => {
  const matchedRoute = RouterConfig.find(config => new RegExp(`^${config.path}$`).test(location.pathname));
  return (matchedRoute && matchedRoute.sceneConfig) || DEFAULT_SCENE_CONFIG;
};

let oldLocation = null;
const Routes = withRouter(({location, history}) => {

  // 转场动画应该都是采用当前页面的sceneConfig,所以:
  // push操作时,用新location匹配的路由sceneConfig
  // pop操作时,用旧location匹配的路由sceneConfig
  let classNames = '';
  if(history.action === 'PUSH') {
    classNames = 'forward-' + getSceneConfig(location).enter;
  } else if(history.action === 'POP' && oldLocation) {
    classNames = 'back-' + getSceneConfig(oldLocation).exit;
  }

  // 更新旧location
  oldLocation = location;

  return (
    <TransitionGroup
      className={'router-wrapper'}
      childFactory={child => React.cloneElement(child, {classNames})}
    >
      <CSSTransition timeout={500} key={location.pathname}>
        <Switch location={location}>
          {RouterConfig.map((config, index) => (
            <Route exact key={index} {...config}/>
          ))}
        </Switch>
      </CSSTransition>
    </TransitionGroup>
  );
});

Since there is a little bit of css code, it will not be pasted here, but it is just the corresponding transition animation configuration, and the complete code can be seen.Warehouse on github. Let’s look at the current effect:

8. Summarize

Firstly, this paper briefly introduces the basic usage of react-router and react-transition-group. The working principle of animation using CSSTransition and TransitionGroup is also analyzed. Then try to complete a transition animation by combining react-router and react-transition-group. The problem of dynamic transition animation is solved by using the childFactory attribute of TransitionGroup. Finally, the route is configured to realize the unified management of routes and the configuration of animation, thus completing an exploration of realizing transition animation by REACT-ROUTER+REACT-TRANSITION-GROUP.

9. Reference

  1. A shallow dive into router v4 animated transitions
  2. Dynamic transitions with react router and react transition group
  3. Issue#182 of react-transition-group
  4. StackOverflow: react-transition-group and react clone element do not send updated props

All code in this article is managed inHere, if you feel good, can give a.star.