TL;DR;
-
react-routerのpathのマッチングで、URL末尾の"/"の扱いは…
-
<Route path="hoge">
→ hogeにもhoge/にもマッチする -
<Route path="hoge/">
→ hoge/にのみマッチする
-
- hogeへのアクセスをhoge/にリダイレクトしたくて
<Redirect from="hoge" to="hoge/" />
を記述すると、hoge/もRedirectのルーティングにマッチして無限リダイレクトが起きるが… - ルーティング定義の上から順に評価されるので、Redirect定義より上に、hoge/でマッチするルーティングを書くようにすると万事解決!
前提
react-router@2.4.1の話です。他のバージョンは調べてません。
背景
URL末尾の"/"(トレイリングスラッシュ)があったりなかったりは色々問題になります。(画像やCSSへの相対パスとか)
reactアプリケーションでもURLをRESTfulにしたいなと思うと、パスの途中に動的な要素が入ることがあります。
例: http://example.com/posts/12345/comments/
id=12345のpostのcomment一覧的な。
react-routerのLinkコンポーネント(aタグを生成)は相対パスをサポートしません。
Linkコンポーネントのみで画面遷移を作ろうとすると、動的部分も全て構築しなければなりません。
例えば
- http://example.com/posts/12345/comments/ を表示中
- ./new へのリンクを配置したい
場合
Linkコンポーネントは絶対パスが必須なので、post_idをparamsで受け取っておいて、下記のようにする必要があります。
<Link to={`/posts/${this.props.params.post_id}/comments/new`}>新規コメント</Link>
Aタグならシンプルに<a href="./new">新規コメント</a>
と書けますので、こちらを使いたくなります。
ただし、当然のことながら現在のURLが
- http://example.com/posts/12345/comments ではなく、
- http://example.com/posts/12345/comments/ でないと./newのリンクは正常に機能しません
アプリケーション内でパスを書き間違える(末尾"/"を忘れてしまう)こともありますし、利用しているフレームワーク等で末尾"/"の扱いが適当なこともあります。
※ Railsの認証ミドルウェアDeviseでは、仮に上記URLが要認証なパスだとして、
- 未認証状態で http://example.com/posts/12345/comments/ にアクセス
- http://example.com/users/sign_in にリダイレクトしてログイン画面を表示
- 認証後、元々アクセスしようとしていたURLを表示しようとしてくれますが、http://example.com/posts/12345/comments にアクセスに行ってしまいます。
末尾"/"を取り除いてしまっているのはブラウザか?Deviseか?
そんなわけで、末尾"/"なしURLを末尾"/"ありURLにリダイレクトしたいです。
ハマりポイント
<Router history={history}>
<Route path="/" component={App}>
<Router path="posts">
<IndexRoute component={PostIndex} />
...
<Redirect from="comments" to="comments/" /> ← これでリダイレクトさせるつもり
<Route path="comments/">
<IndexRoute component={CommentIndex} />
<Route path="new" component={NewComment} />
</Route>
</Route>
</Router>
この定義だと死にます。
pathのマッチングは、下記の法則があります。
-
<Route path="hoge">
→ hogeにもhoge/にもマッチする -
<Route path="hoge/">
→ hoge/にのみマッチする
したがって、<Redirect from="comments" to="comments/" />
という定義は、commentsをcomments/にリダイレクトしますが、comments/もcomments/にリダイレクトしようとして無限ループになります。
(Redirectコンポーネントのfrom属性はpath属性の別名なのでpathもfromも同じ挙動になります)
解決策
pathのマッチングは上から順に評価され、一番最初にマッチした物が適用される(正確にはもっと色々評価法則あるかもですが)ので、下記のように書くとOKです。
<Router history={history}>
<Route path="/" component={App}>
<Router path="posts">
<IndexRoute component={PostIndex} />
...
<Route path="comments/"> ← comments/にマッチする定義がRedirectより上にある
<IndexRoute component={CommentIndex} />
<Route path="new" component={NewComment} />
</Route>
<Redirect from="comments" to="comments/" />
</Route>
</Router>