WebDesign Dackel

react-routerでページ遷移にちょっとしたアニメーションを付ける

Hatena0
Google+0
Pocket0
Feedly0

react-routerのリポジトリを見ると、アニメーション用のサンプルがありました。
以前、同じような事をやろうとして上手く出来なかったので、この機会に試してみました。

それぞれのライブラリのバージョンは以下のものを使用します。

  • react (0.14.2)
  • react-router (1.0.0-rc4)

成果物

react-routerのアニメーションサンプルスクリーンショット

動作サンプル

HomePage1Page2という3つのページを行き来出来るサンプルです。

また、見栄えを整えるために、Material-UIを使ってみましたが、簡単につかえてそれっぽく見えるのでいいですね…!

今回使用したファイルは、一通り以下のリポジトリへあげました。

tsuyoshiwada/react-samples/react-router-animate

サンプルのソースに便利そうなコンポーネントが…

react-router/examples/animations/app.jsの中に、そのまま使えそうなコンポーネントがあります。

ReactCSSTranstionGroupをラップしたRouteCSSTransitionGroupというコンポーネントです。
contextに渡ってくるパスの比較をして中身の更新をかけているようです。(間違っていたらご指摘頂けると嬉しいです…!)

import React, {Component, PropTypes} from "react"
import ReactCSSTransitionGroup from "react-addons-css-transition-group"
import StaticContainer from "react-static-container"


export default class RouteCSSTransitionGroup extends Component {
  static contextTypes = {
    location: PropTypes.object
  }

  constructor(props, context) {
    super(props, context);
    this.state = {previousPathname: null};
  }

  componentWillReceiveProps(nextProps, nextContext) {
    if ( nextContext.location.pathname !== this.context.location.pathname ) {
      this.setState({previousPathname: this.context.location.pathname});
    }
  }

  componentDidUpdate() {
    if ( this.state.previousPathname ) {
      this.setState({previousPathname: null});
    }
  }

  render() {
    const {children, ...props} = this.props;
    const {previousPathname} = this.state;

    return (
      <ReactCSSTransitionGroup {...props}>
        <StaticContainer
          key={previousPathname || this.context.location.pathname}
          shouldUpdate={!previousPathname}>
          {children}
        </StaticContainer>
      </ReactCSSTransitionGroup>
    );
  }
}

ReactCSSTransitionGroupの中に使用している、react-static-containerは静的コンテンツのレンダリングを最適化したコンポーネントみたいです。

React.Children.onlyを使っているだけの簡単な中身でした。

ソースコード

上記のRouteCSSTransitionGroupを実際に使ってみたソースです。

JSの構成は以下のようになっていて、app.jsbrowserifyでバンドルしています。

src/js
├── app.js
├── components
│   ├── App.js
│   ├── RouteCSSTransitionGroup.js
│   └── pages
│       ├── Home.js
│       ├── Page1.js
│       └── Page2.js
└── routes.js

エントリーファイル

Routerdiv#appをマウントしています。
あとは、material-uiを使うためにreact-tap-event-pluginを実行しておきます。

app.js

import React from "react"
import ReactDOM from "react-dom"
import {Router} from "react-router"
import createHashHistory from "history/lib/createHashHistory"
import injectTapEventPlugin from "react-tap-event-plugin"
import routes from "./routes"


injectTapEventPlugin();

const history = createHashHistory();

ReactDOM.render(
  <Router history={history}>
    {routes}
  </Router>,
  document.getElementById("app")
);

ルーティング

それぞれのコンポーネントを読み込んで、ルーティングを組み立ていきます。

routes.js

import React from "react"
import {Route, IndexRoute} from "react-router"
import App from "./components/App"
import Home from "./components/pages/Home"
import Page1 from "./components/pages/Page1"
import Page2 from "./components/pages/Page2"


export default (
  <Route path="/" component={App}>
    <Route path="page1" component={Page1} />
    <Route path="page2" component={Page2} />
    <IndexRoute component={Home} />
  </Route>
);

ルートコンポーネント

components/App.js

import React, {Component, PropTypes} from "react"
import RouteCSSTransitionGroup from "./RouteCSSTransitionGroup"
import mui from "material-ui"


const {
  AppBar,
  LeftNav,
  MenuItem,
  Mixins
} = mui;

const {
  StylePropable,
  StyleResizable
} = Mixins;

export default class App extends Component {
  mixins = [StylePropable, StyleResizable]

  render() {
    const menuItemsNav = [
      {route: "/", text: "Home"},
      {route: "/page1", text: "Page1"},
      {route: "/page2", text: "Page2"}
    ];

    return (
      <div>
        <AppBar
          title="React router animate"
          onLeftIconButtonTouchTap={::this.handleLeftIconButtonTouchTap} />

        <LeftNav
          ref="leftNav"
          docked={false}
          menuItems={menuItemsNav}
          onChange={::this.handleLeftItemChange} />

        // アニメーションを行うために、RouteCSSTransitionGroupを
        // 設定しておきます。
        <div className="container">
          <RouteCSSTransitionGroup
            component="div" transitionName="routing"
            transitionEnterTimeout={250} transitionLeaveTimeout={250} >
            {this.props.children}
          </RouteCSSTransitionGroup>
        </div>
      </div>
    );
  }

  handleLeftIconButtonTouchTap() {
    this.refs.leftNav.toggle();
  }

  handleLeftItemChange(e, selectedIndex, menuItem) {
    this.props.history.pushState(null, menuItem.route);
  }
}

いつのまにか、ReactCSSTransitionGroupに対してアニメーションの速度を指定しないといけないようになっていました。こちらで指定した速度をCSS側と一致させておく必要があるみたいです。

アニメーション用に、routingというクラスを割り当てて、以下のスタイルを設定しました。
Scssで書いています。

.routing {

  &-enter {
    opacity: .01;
    transform: translateY(30px);
    transition: all .25s ease-out;
    &-active {
      opacity: 1;
      transform: translateY(0);
    }
  }

  &-leave {
    opacity: 1;
    transform: translateY(0);
    transition: all .25s ease-out;
    &-active {
      opacity: 0;
      transform: translateY(-30px);
    }
  }
}

下から、「ふわっと」出るようにしてみました。

ポイントとして、以下のように動かす要素に対してposition: absoluteをしておくと、表示・非表示が綺麗にいきます。これが無いと、前の要素が消えるまで高さが残ったままで、完全に消えた時にガクガクした動きになってしまいます。

// .contentsのラッパー
.container {
  position:relative;
  width: 700px;
  max-width: 100%;
  margin: 30px auto;
}

// 実際に動かす要素
.contents {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  padding: 15px;
}

ページコンポーネント

components/pages/Home.js

import React, {Component, PropTypes} from "react"
import {Link} from "react-router"
import {
  Paper,
  FlatButton
} from "material-ui"


export default class Home extends Component {
  render() {
    return (
      <div className="contents">
        <Paper className="paper">
          <h2>Home</h2>
          <p>Homeのコンテンツが入ります。</p>
          <FlatButton label="Go Page1" containerElement={<Link to="/page1" />} linkButton={true} />
          <FlatButton label="Go Page2" containerElement={<Link to="/page2" />} linkButton={true} />
        </Paper>
      </div>
    );
  }
}

Page1Page2も同じ感じなので省略します。


これで以下のようにちょっとした動きのついたページ遷移が実現できました!

動作イメージ

気をつけたい点

ついついアニメーションにこだわり過ぎて、ページ遷移がしたいだけなのにユーザのストレスに繋がるようなケースをたまに見かけます。あくまで「ちょっとした」ものに留めておくのが良い気がします!