react-router

2015-04-01T00:00:00+09:00 JavaScript React.js

react-routerを使ってみた。

※React.jsな構成の考え方とかとりあえず反映せずにざらーって書いてるので(ry

※やたらと長い上に書いた本人以外わかりづらいと思うので公式ドキュメント読んだ方が良い

環境構築

とりあえずnpmでreact及びreact-routerの依存性とbrowserifyを使ったビルドの定義をしておく

{
  "name": "sample",
  "version": "0.0.1",
  "main": "js/app.js",
  "dependencies": {
    "react": "^0.13.1",
    "react-router": "^0.13.2"
  },
  "devDependencies": {
    "browserify": "^8.0.3",
    "envify": "^3.0.0",
    "reactify": "^0.15.2",
    "watchify": "^2.6.2"
  },
  "scripts": {
    "start": "watchify -o bundle.js -v -d js/app.js",
    "build": "browserify . -t [envify --NODE_ENV production] -o bundle.js",
    "postinstall": "npm run build"
  },
  "browserify": {
    "transform": ["reactify", "envify"]
  }
}

でコード中にはReact.jsで使うJSX構文が含まれているのでreactifyのトランスフォームを使ってビルドする事で可能。envifyはnode.jsのprocess.env.NODE_ENVを静的な文字列に置き換えるトランスフォーマーかと(それ以外に何かあるのかは未調査)

でプロジェクト構成を整えたらnpm installで必要な依存性の取得をした後にpostinstallでビルドするようにしているのでjs/app.jsをビルドしたbundle.jsが生成される。それを以下のHTMLで利用する(以降にて共通で使用するHTML)

<html>
  <head>
  </head>
  <body>
    <div id="hello-app"></div>
    <script src="bundle.js"></script>
  </body>
</html>

あとはガシガシとコード書いてドキュメント読みつつ進める

js/app.js

"use strict";

var React  = require("react"),
    Router = require("react-router"),
    routes = require("./routes");

/*
HTML5 Hisotry Locationを使う場合
Router.run(routes, Router.HistoryLocation, function(Handler) {
*/

// hashbang方式を使う場合には第2引数にRouter.HistoryLocation等を指定しない
Router.run(routes, function(Handler) {
  React.render(
    <Handler />,
    document.querySelector('#hello-app')
  );
});

Router.runしてマッチしたルーティングを指定したセレクターでハンドラー(ReactClass)で描画するっていう感じかと

js/routes.js

"use strict";

var React        = require("react"),
    Router       = require("react-router"),
    Route        = Router.Route,
    Link         = Router.Link,
    RouteHandler = Router.RouteHandler,
    DefaultRoute = Router.DefaultRoute;

var MainNav = React.createClass({
  render: function() {
    /*
    Linkを使えば現在見ているページに対してclass="active"がつく
    <a>で記述するとHTML5モード使ったりhashbang方式使ったりの切り替えしないといけないはずので<Link>を使った方が良いかも
    */

    return (
      <div>
        <ul>
          <li><Link to="hello">hello</Link></li>
        </ul>
      </div>
    );
  }
});

var App = React.createClass({
  render: function() {
    return (
      <div>
        <MainNav />
        {/* 要はルーティングマッチしたコンテンツがレンダリングされる領域? */}
        <RouteHandler />
      </div>
    );
  }
});

var Root = React.createClass({
  render: function() {
    return (
      <h2>Root</h2>
    );
  }
});

var Hello = React.createClass({
  render: function() {
    return (
      <h2>Hello</h2>
    );
  }
});

var routes = (
  <Route name="app" path="/" handler={App}>
    <Route name="hello" path="/hello" handler={Hello} />
    <DefaultRoute handler={Root} />
  </Route>
);

module.exports = routes;

先ほど書いた上記のHTMLにアクセスにするとまず<DefaultRoute>で定義しているRootハンドラーが作用してそこが<RouteHandler>な領域に描画されるような形かと。で<MainNav>なので定義している<Link>が変換された<a>をクリックするとHelloハンドラーが作用して同様な描画処理が行われる感じ

<NotFoundRoute>と<Redirect>

js/route.js

"use strict";

var React         = require("react"),
    Router        = require("react-router"),
    Route         = Router.Route,
    Link          = Router.Link,
    RouteHandler  = Router.RouteHandler,
    DefaultRoute  = Router.DefaultRoute,
    Redirect      = Router.Redirect,
    NotFoundRoute = Router.NotFoundRoute;

var MainNav = React.createClass({
  render: function() {
    return (
      <div>
        <ul>
          <li><Link to="hello">hello</Link></li>
        </ul>
      </div>
    );
  }
});

var App = React.createClass({
  render: function() {
    return (
      <div>
        <MainNav />
        <RouteHandler />
      </div>
    );
  }
});

var Root = React.createClass({
  render: function() {
    return (
      <h2>Root</h2>
    );
  }
});

var Hello = React.createClass({
  render: function() {
    return (
      <h2>Hello</h2>
    );
  }
});

var NotFound = React.createClass({
  render: function() {
    return (
      <h2>Not Found</h2>
    );
  }
});


var routes = (
  <Route name="app" path="/" handler={App}>
    <Route name="hello" path="/hello" handler={Hello} />
    <Redirect from="hoge" to="hello" />
    <DefaultRoute handler={Root} />
    <NotFoundRoute handler={NotFound} />
  </Route>
);

module.exports = routes;

ルーティングの定義とNotFoundハンドラーの定義だけ修正しただけだけど。存在しないルーティングにアクセスしたような場合には<NotFoundRoute>で定義したハンドラーが作用するようになる。で<Redirect>で定義するとfromで指定したルーティングにアクセスした場合にはtoで指定したルーティング名にリダイレクトされるようになる

ルーティングパラメーターを利用する

ルーティングパラメーターも利用する事も出来るし、ルーティングライブラリとかによくあるアスタリスクを使ったsplatルーティングパラメーターも利用する事が可能 (詳しくは https://github.com/rackt/react-router/blob/master/docs/guides/path-matching.md#splats )

js/routes.js

"use strict";

var React        = require("react"),
    Router       = require("react-router"),
    Route        = Router.Route,
    Link         = Router.Link,
    RouteHandler = Router.RouteHandler,
    DefaultRoute = Router.DefaultRoute;

var MainNav = React.createClass({
  render: function() {
    return (
      <div>
        <ul>
          {/* <Link>でパラメーターを利用する場合にはparams=で指定 */}
          <li><Link to="user" params={% raw %}{{ id: 1}}{% endraw %}>user1</Link></li>
          <li><Link to="user" params={% raw %}{{ id: 2}}{% endraw %}>user2</Link></li>
        </ul>
      </div>
    );
  }
});

var App = React.createClass({
  render: function() {
    return (
      <div>
        <MainNav />
        <RouteHandler />
      </div>
    );
  }
});

var Root = React.createClass({
  render: function() {
    return (
      <h2>Root</h2>
    );
  }
});

var User = React.createClass({
  contextTypes: {
    router: React.PropTypes.func
  },
  render: function() {
    var params = this.context.router.getCurrentParams();

    return (
      <h2>User{params.id}</h2>
    );
  }
});

var routes = (
  <Route name="app" path="/" handler={App}>
    <Route name="user" path="/user/:id" handler={User} />
    <DefaultRoute handler={Root} />
  </Route>
);

module.exports = routes;

<Route>のpathで:idのようなパラメーターを設定して、それをRouter APIのgetCurrentParamsを介して利用する事が出来る

又、Router APIを介さなくてもルーティングパラメーターをハンドラーのプロパティ(React.createClass内のprops)で参照する事も可能。それを利用する場合は以下のように修正する

js/app.js

"use strict";

var React  = require("react"),
    Router = require("react-router"),
    routes = require("./routes");

Router.run(routes, function(Handler, state) {
  React.render(
    // 修正
    <Handler params={% raw %}{state.params}{% endraw %} />,
    document.querySelector('#hello-app')
  );
});

js/routes.js

"use strict";

var React        = require("react"),
    Router       = require("react-router"),
    Route        = Router.Route,
    Link         = Router.Link,
    RouteHandler = Router.RouteHandler,
    DefaultRoute = Router.DefaultRoute;

var MainNav = React.createClass({
  render: function() {
    return (
      <div>
        <ul>
          <li><Link to="user" params={% raw %}{{ id: 1}}{% endraw %}>user1</Link></li>
          <li><Link to="user" params={% raw %}{{ id: 2}}{% endraw %}>user2</Link></li>
        </ul>
      </div>
    );
  }
});

var App = React.createClass({
  render: function() {
    return (
      <div>
        <MainNav />
        {/* 修正 */}
        <RouteHandler {...this.props} />
      </div>
    );
  }
});

var Root = React.createClass({
  render: function() {
    return (
      <h2>Root</h2>
    );
  }
});

var User = React.createClass({
  render: function() {
    // paramsをthis.context.router.getCurrentParamsじゃなくてthis.propsから参照するように
    var params = this.props.params;
    return (
      <h2>User{params.id}</h2>
    );
  }
});

var routes = (
  <Route name="app" path="/" handler={App}>
    <Route name="user" path="/user/:id" handler={User} />
    <DefaultRoute handler={Root} />
  </Route>
);

module.exports = routes;

っていう感じでハンドラーにパラメーターをプロパティにマージ?するような形として利用する事も出来る

Dynamic Segmentを使う場合の注意

Dynamic Segment(ルーティングパラメーター)を使ってる場合において、<Link>等で切り替えを行うような際(公式的にはトランジションを行う場合)にはそのハンドラーのgetInitialStateが発生しなくなる。getInitialStateで返した値をrenderでstateを使うような場合にはcomponentWillReceivePropsでsetStateするような形を利用するようにとの事。つまり以下みたいな感じでやれって事らしい

js/routes.js

"use strict";

var React        = require("react"),
    Router       = require("react-router"),
    Route        = Router.Route,
    Link         = Router.Link,
    RouteHandler = Router.RouteHandler,
    DefaultRoute = Router.DefaultRoute;

var MainNav = React.createClass({
  render: function() {
    return (
      <div>
        <ul>
          <li><Link to="user" params={% raw %}{{ id: 1}}{% endraw %}>user1</Link></li>
          <li><Link to="user" params={% raw %}{{ id: 2}}{% endraw %}>user2</Link></li>
        </ul>
      </div>
    );
  }
});

var App = React.createClass({
  render: function() {
    return (
      <div>
        <MainNav />
        <RouteHandler />
      </div>
    );
  }
});

var Root = React.createClass({
  render: function() {
    return (
      <h2>Root</h2>
    );
  }
});

var User = React.createClass({
  contextTypes: {
    router: React.PropTypes.func
  },
  getInitialState: function() {
    return this.getStateFromStore();
  },
  componentWillReceiveProps: function() {
    this.setState(this.getStateFromStore());
  },
  render: function() {
    return (
      <h2>User{this.state.id}</h2>
    );
  },
  getStateFromStore: function() {
    return this.context.router.getCurrentParams();
  }
});

var routes = (
  <Route name="app" path="/" handler={App}>
    <Route name="user" path="/user/:id" handler={User} />
    <DefaultRoute handler={Root} />
  </Route>
);

module.exports = routes;

getInitialStateは実装しないっていう訳じゃなくて、<Link>でトランジションされるような際においてはgetInitialStateが発生しないので、そこで返すと同等なstateを保持するような形になる模様

公式的には https://github.com/rackt/react-router/blob/master/examples/master-detail/app.js#L91 を参考にしろとの事

ルーティングをネストする場合

js/routes.js

"use strict";

var React        = require("react"),
    Router       = require("react-router"),
    Route        = Router.Route,
    Link         = Router.Link,
    RouteHandler = Router.RouteHandler,
    DefaultRoute = Router.DefaultRoute;

var MainNav = React.createClass({
  render: function() {
    return (
      <div>
        <ul>
          <li><Link to="users">users</Link></li>
          <li><Link to="user" params={% raw %}{{ id: 1}}{% endraw %}>user1</Link></li>
          <li><Link to="user" params={% raw %}{{ id: 2}}{% endraw %}>user2</Link></li>
        </ul>
      </div>
    );
  }
});

var App = React.createClass({
  render: function() {
    return (
      <div>
        <MainNav />
        <RouteHandler />
      </div>
    );
  }
});

var Root = React.createClass({
  render: function() {
    return (
      <h2>Root</h2>
    );
  }
});

var Users = React.createClass({
  render: function() {
    return (
      <div>
        <h2>Users</h2>
        <div>
          {/* ネストされたルーティングがあるような場合であれば<RouteHandler />しないとネストされた部分まで作用しない模様 */}
          <RouteHandler />
        </div>
      </div>
    );
  }
});

var User = React.createClass({
  contextTypes: {
    router: React.PropTypes.func
  },
  render: function() {
    var params = this.context.router.getCurrentParams();

    return (
      <h2>User{params.id}</h2>
    );
  }
});

var routes = (
  <Route name="app" path="/" handler={App}>
    <Route name="users" handler={Users}>
      <Route name="user" path=":id" handler={User} />
    </Route>
    <DefaultRoute handler={Root} />
  </Route>
);

module.exports = routes;

書いてる通り、ルーティングがネストされている場合にネストされた側のルーティングにマッチしても親側で<RouteHandler>してない場合にはネストされた部分まで処理が作用しない模様(んまぁ、今までを考えればルートなappルーティングで<RouteHandler>してるから当たり前な事だと)

んまぁ難癖ありそうだなって思ったら出来る限りそういう定義にならないようにルーティング設計を考えるべきかと

renderする際にリダイレクトしたい場合

そういう事するような要件になるようにならないようにするべきなんですが、あくまで疑問として描画される際にあたって問題があればリダイレクトしたいような要件の場合は以下のようにAPI使って出来なくもない模様

"use strict";

var React        = require("react"),
    Router       = require("react-router"),
    Route        = Router.Route,
    RouteHandler = Router.RouteHandler;

var App = React.createClass({
  render: function() {
    return (
      <div>
        <RouteHandler />
      </div>
    );
  }
});

var Root = React.createClass({
  contextTypes: {
    router: React.PropTypes.func
  },
  render: function(event) {
    this.context.router.replaceWith("user", { "id": "123" });
    //this.context.router.transitionTo('user', { id: 123 });

    return null;
  }
});

var User = React.createClass({
  contextTypes: {
    router: React.PropTypes.func
  },
  render: function() {
    var params = this.context.router.getCurrentParams();
    return (
      <h2>User{params.id}</h2>
    );
  }
});

var routes = (
  <Route name="app" path="/" handler={App}>
    <Route name="root" path="/" handler={Root} />
    <Route name="user" path="/user/:id" handler={User} />
  </Route>
);

module.exports = routes;

これを使う事によりどういう問題が出てくるかって所までは把握してないので、先も言ったけどこういう事案にならないように避けるべきかと

以上。長いけどまだ見落としてる部分もありそうなので色々分かったら追記なり新しくネタ書いたりするかも知れない

追記

Navigationっていうmixinを使うと

var React = require("react")
    Router = require("react-router"),
    Navigation = Router.Navigation;

var Top = React.createClass({
  mixins: [Navigation],
  onClick: function(e) {
    e.preventDefault();
    this.replaceWith("user", { id: 1 });
  },
  render: function() {
    return (
      <div>
        <a href="#" onClick={this.onClick}>user</a>
      </div>
    );
  }
});

module.exports = Top;

なんて感じに使えますが、Navigationを使うルーティングの操作は現在deprecatedな模様。なので使わないのが良さそう

jestの__mocks__ディレクトリに関して doma2を使ってみる (4) - Domain -