React Routerの基本
React Router(公式サイト)は、コンポーネントの入れ替えによる見た目上の画面遷移と、それに伴うURLの変更を管理・同期するためのライブラリです。Web向けとReact Native向けの実装が用意されていますが、今回はWeb向けの実装であるreact-router-domを使用します。
導入するには、次のコマンドを実行してください。
$ npm install react-router-dom
まずはURLとコンポーネントのひも付けの方法について見ていきましょう。次のように定義します。
import {
BrowserRouter,
Route
} from "react-router-dom";
// 自作の画面
import HomePage from "./home";
import MyPage1 from "./mypage1";
import MyPage2 from "./mypage2";
const App = () => (
<BrowserRouter>
{/* BrowserRouter直下に置けるコンポーネントは1つだけ */}
<div>
{/* RouteはBrowserRouter以下ならばどこの階層に置いてもよい */}
<Route exact path="/" component={HomePage} /> {/*(1)*/}
<Route path="/mypage/1" component={MyPage1} />
<Route path="/mypage/2" component={MyPage2} />
</div>
</BrowserRouter>
);
BrowserRouterコンポーネントの中にRouteコンポーネントを置いていきます。(1)ではdivを挟んでBrowserRouterのほぼ直下にRouteを置いていますが、もっと複雑な階層の中に置いても構いません。なお、React RouterにはBrowserRouter以外にもいくつかのRouterコンポーネントが存在していますが、本記事では扱いません。
Routeは現在のアドレスバーのURLに応じて、自身の表示・非表示を切り替えるコンポーネントです。URLのパスがpath属性に定義したものを含む場合に、component属性に定義したコンポーネントを表示状態にします。それ以外の場合は非表示となります。上記のサンプルの場合は、URLごとにJSXが次のようになるとイメージしてください。
// http://example.com/ のとき <div> <HomePage /> </div> // http://example.com/mypage/1 のとき <div> <MyPage1 /> </div> // http://example.com/mypage/2 のとき <div> <MyPage2 /> </div>
実際にはRouteがコンポーネントを描画する際にいくつかのpropsを渡すので、上の例は正確ではありませんが、大枠としてはこのようなものだと理解してください。
RouteはURLを前方一致で評価します。どういうことかというと、ルート(/)にHomePageコンポーネントをひも付けておいた場合、設定がない/hogeや/fugaといったURLに対してもHomePageコンポーネントを描画してしまうのです。これを防ぐための手だてとして、前項の(1)のようにexact属性を設定する、というものがあります。exact属性をtrueに設定しておくことで、pathに完全一致するURLで表示する場合のみ、そのコンポーネントを描画するようになります。
Routeを使うことで、URLに応じたコンポーネントを表示されることが分かりました。では、次にURLを変更する方法を紹介します。通常のWebでは画面遷移にアンカータグを利用しますが、React RouterではLinkコンポーネントというものが用意されています。次のように使用します。
<Link to="/mypage/1">MyPage1を開く</Link>
これは内部的にアンカータグを描画しますが、クリックイベントを改変しています。クリックすると、HTML5のHistory APIに近い挙動を引き起こします。
少し踏み込んだReact Routerの使い方
React Routerには魅力的な機能がたくさんあります。実践的なノウハウの一部をご紹介します。
パラメータを受け取る
React RouterではURLからパラメータをもらってくる方法が提供されているため、パラメータに応じて内容を切り替えたいタイプのコンポーネントで便利に使うことができます。この機能を使うためには、Routeを以下の通り設定します。
<Route
path="/articles/:articleId" {/* 記事のIDを持つURLに反応させる */}
component={Article} /> {/* 記事を表示するコンポーネント */}
すると、コンポーネント側では次の通りパラメータを取り出すことができます。
const articleId = this.props.match.params.articleId; // (1)
fetch(`https://example.com/api/articles/${articleId}`)
.then(article => {
this.setState({ article }); // 画面に記事を描画する
});
Routeのpathでコロンを付けたパラメータがprops.match.paramsに入っており、(1)のように取り出すことができます。この仕組みを作っておくと、LinkコンポーネントでURLにパラメータを指定するだけでリンク先のコンテンツを差し替えられるようになるため、とても便利です。
<Link to="/articles/001">記事001</Link> <Link to="/articles/002">記事002</Link> <Link to="/articles/003">記事003</Link>
IDやカテゴリ名などをパラメータとして渡すことで、コンポーネントの共通化が捗るケースがあります。設計時に考慮してみるとよいでしょう。
デフォルトで表示するコンポーネントを設定する
パラメータ付きのRouteを作成した際には、パラメータがなかった場合のことも考慮します。例えば次の例です。
<div>
<Route
path="/articles/:articleId"
component={Article} />
</div>
articleIdを渡さなかった場合、つまり/articlesとしてアクセスした場合には、マッチするpathがないと見なされて、何も表示されません。ユーザビリティ上あまり好ましい状況ではないので、デフォルトで何らかの記事を表示したいところですね。Redirectコンポーネントを使って、次の通りに書き換えてみましょう。
<div>
<Route
path="/articles/:articleId"
component={Article} />
<Redirect to="/articles/001" />
</div>
マッチするRouteが1つもなかった場合に、Redirectのto属性に設定されたURLにリダイレクトされ、少なくとも何かのコンテンツが表示されている状況を作ることができます。
動的にリダイレクトを行う
JSXに定義した静的な設定でリダイレクトを仕込んでおく方法については、前項で紹介した通りでした。では、動的にリダイレクトを発生させるにはどうしたらよいのでしょうか。Routeを使って描画したコンポーネントに渡されるpropsのひとつに、historyがあります。これはHTML5のHistory APIを薄くラップしたライブラリです(BrowserRouterと併用した場合の挙動です)。これを使って、動的にリダイレクトを実現できます。
このhistoryを使ってリダイレクトを行う場合は、history.replace("遷移先のパス")を利用します。前項で定義したRouteで例を挙げます。articleIdに該当する記事が存在する場合には記事データの取得を行い、存在しない場合には"/articles/001"にリダイレクトする処理は、以下のように書きます。
const articleId = this.props.match.params.articleId;
if (articleExists(articleId)) { // そのIDに該当する記事の存在を確認する
fetch(`https://example.com/api/articles/${articleId}`)
.then(article => {
this.setState({ article }); // 画面に記事を描画する
});
} else {
this.props.history.replace("/articles/001"); // (1)
}
(1)で指定したパスにBrowserRouterやRouteが反応することで、画面が切り替わります。
動的に画面遷移を行う
前項と同様に、Linkを使わずに画面遷移を行う方法があります。Linkのto要素に指定していた遷移先のパスを、history.push("遷移先のパス")という形で実行すると、画面遷移が発生します。
Routeが1つしか表示されないことを保証する
前述の通り、Routeは現在のURLがpathにマッチした場合だけ表示され、マッチしなかった場合には表示されません。これは大原則です。しかし、これは裏を返せば「マッチしたRouteはすべて表示される」ということでもあります。リスト1を元にして、exact要素を書かなかった場合の例を考えてみましょう。
<div>
<Route path="/mypage/1" component={MyPage1} />
<Route path="/mypage/2" component={MyPage2} />
<Route path="/" component={HomePage} />
</div>
この場合、”/mypage/1”ではMyPage1とHomePageが表示され、”/mypage/2”ではMyPage2とHomePageが表示されてしまいます。これは意図せぬ動作です。
このケースであればexactを使う形でも解決可能ですが、パスの構造が複雑になってくると、exactだけでは対処できないケースも出てくるかもしれません。こうした「ある階層に表示するコンポーネントを排他的に必ず1つにしたい」という要件を満たすアプローチとして、Switchコンポーネントを使う方法を紹介します。
Switchは、childrenとなったRouteやRedirectのうち、マッチしたはじめの1つしか表示しない特性を持ったコンポーネントです。次の通り、Routeの上位に置くことで動作します。
<Switch>
<Route path="/mypage/1" component={MyPage1} />
<Route path="/mypage/2" component={MyPage2} />
<Route path="/" component={HomePage} />
</Switch>
この場合、Switchの特性により、次のように動作します。
// http://example.com/ のとき <div> <HomePage /> </div> // http://example.com/mypage/1 のとき <div> <MyPage1 /> </div> // http://example.com/mypage/2 のとき <div> <MyPage2 /> </div>
意図した動作になりましたね。画面全体の差し替えや、固定のレイアウトの中で左ペインや右ペインの中身だけを切り替えていくような挙動をさせたい場合には、Switchを積極的にするとよさそうです。
注意:URLを直接打ち込んだときの対応
React Routerを使うことで、index.htmlを表示したまま、あたかも画面遷移をしたかのようにURLを変えることができるようになりました。
しかし、一点だけ気を付けたほうがいいことがあります。ユーザーが「https://example.com/articles/001」といったURLをブラウザに直接入力した場合、ApacheなどのWebサーバーは「https://example.com/articles/001/index.html」を返却しようとするケースが多いかと思います。しかし、実際には「https://example.com/index.html」しか用意されていないため、404 NotFoundになってしまいます。
これを防ぐためには、サーバーサイドのルーティングを設定して、「https://example.com/」以下のすべての階層へのアクセスに対し、「https://example.com/index.html」と同じファイルを返すようにするなどの対策を行う必要があります。
create-react-appの開発サーバーは元々こうした設定になっているため見落としがちですが、本番運用の段階でしばしば落とし穴になる部分なので、気を付けましょう。
まとめ
戻る・進むやブックマークが使えるのは、WebアプリやWebサイトにとって大切なことです。JavaScriptでURL設計をするのは不慣れで戸惑う方も多いかもしれませんが、身近なサーバーサイドエンジニアに話を聞いたりしながら、ユーザビリティの高い画面遷移を構築していきましょう。
