react-routerでページ遷移にちょっとしたアニメーションを付ける
react-routerのリポジトリを見ると、アニメーション用のサンプルがありました。
以前、同じような事をやろうとして上手く出来なかったので、この機会に試してみました。
それぞれのライブラリのバージョンは以下のものを使用します。
react
(0.14.2
)react-router
(1.0.0-rc4
)
成果物
Home
、Page1
、Page2
という3つのページを行き来出来るサンプルです。
また、見栄えを整えるために、Material-UIを使ってみましたが、簡単につかえてそれっぽく見えるのでいいですね…!
今回使用したファイルは、一通り以下のリポジトリへあげました。
サンプルのソースに便利そうなコンポーネントが…
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.js
をbrowserify
でバンドルしています。
src/js
├── app.js
├── components
│ ├── App.js
│ ├── RouteCSSTransitionGroup.js
│ └── pages
│ ├── Home.js
│ ├── Page1.js
│ └── Page2.js
└── routes.js
エントリーファイル
Router
とdiv#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>
);
}
}
Page1
、Page2
も同じ感じなので省略します。
これで以下のようにちょっとした動きのついたページ遷移が実現できました!
気をつけたい点
ついついアニメーションにこだわり過ぎて、ページ遷移がしたいだけなのにユーザのストレスに繋がるようなケースをたまに見かけます。あくまで「ちょっとした」ものに留めておくのが良い気がします!