2019.10.20

Hookにも対応!Vue.jsエンジニアのためのReact Router v5入門


この記事では、React コミュニティにおけるルーティングライブラリのデファクトスタンダード React Router の基本的な機能を紹介します。

以前の記事『Vue.jsエンジニアのためのReact入門』と同様に、主に Vue Router をひととおり学習した方向けに、React Router ならどうやるのか?という視点でまとめます。

とはいえ前記事よりはゆるい比較なので、Vue Router 知らない方でも React Router の入門として読めると思います。

ルーティングライブラリとは

Vue Router 知っている皆さんには説明不要かもしれませんが、おさらいです。

Vue Router も React Router も、「ルーティングライブラリ」と呼ばれます。

ルーティングライブラリは、SPA(Single Page Application)には欠かせない存在で、紙芝居のように URL に応じて表示するコンポーネントを切り替えることで、画面遷移をエミュレートします。

そのためには、最低限以下の機能を持っている必要があります。

  • URL パターンとコンポーネントの紐づけ
  • URL 中に含まれる動的パラメータの定義と取得(たとえば /users/12341234
  • プログラムによるナビゲーション

Vue Router と React Router はいずれもこのような機能、およびそれを実現するための独自コンポーネントを提供しています。

その意味ではどちらも同じようなライブラリですが、決定的に異なるのは、Vue Router が豊富な「ナビゲーションガード」を持っている点でしょう。ナビゲーションガードとは、ナビゲーション発生から完了までの色々なタイミングに処理をフックできる機能です(次のコンポーネントをレンダリングする直前に何かするなど)。

React Router はナビゲーションガードの機能を明示的には持っていません。ただ、似たようなことを実現する方法は紹介します。

インストール

Web アプリで使用する場合は、react-router-dom ライブラリをインストールします。

$ npm install react-router-dom

モバイルアプリ用の react-router-native やそれぞれのコアである react-router も存在しますので、注意しましょう。

ルートマッチング

URL パターンとコンポーネントの紐づけは以下のように実装します。

import { BrowserRouter, Switch, Route } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route path="/" exact children={<Index />} />
        <Route path="/about" children={<About />} />
      </Switch>
    </BrowserRouter>
  );
}

react-router-dom から3つのコンポーネント BrowserRouter, Switch, Route をインポートしています。この3つはセットで覚えてしまうとよいでしょう。

BrowserRouter

BrowserRouter の内部でルーター機能が使えるようになります。

BrowserRouter は HTML5 History API を用いるタイプのルーターで、他には HashRouter コンポーネントもあります。ただ、あえて HashRouter を選ぶケースはほとんどないでしょう。

basename プロパティで URL パターンをグルーピングできます。以下の Route/settings/profile にマッチします。

<BrowserRouter basename="/settings">
  <Switch>
    <Route path="/profile" children={<MyComponent />} />
  </Switch>
</BrowserRouter>

Switch

Switch コンポーネントは、排他的なルートの描画にために使います。

そもそも Route は、マッチさえすれば登録されたコンポーネントを描画します。以下のように書いた場合(2つ目はパスパラメータ、3つ目はワイルドカード、どちらも詳しくは後述します)、/cat はすべてにマッチするので、3つのコンポーネントが描画されます。

<Route path="/cat" children={<Cat />} />
<Route path="/:userId" children={<User />} />
<Route path="*" children={<NotFound />} />

Switch で囲むと、上から検索していって、マッチした Route のみを描画します。

<Switch>
  <Route path="/cat" children={<Cat />} />
  <Route path="/:userId" children={<User />} />
  <Route path="*" children={<NotFound />} />
</Switch>

たいていのケースでは Switch を利用することになるでしょう。

Route

Route は、URL パターンごとに描画されるコンポーネントを定義します。

path に URL パターン、children に対応するコンポーネントを指定します。つまり、URL が path の値にマッチしたとき、children に指定されたコンポーネントを描画します。

以下、プロパティについて簡単に紹介しますが、これ以外にもあるので詳細はドキュメントを参照してください。

path

URL パターンの書き方は、Vue Router と同じです。どちらも path-to-regexp というライブラリを使用しているためです。

path には配列で複数のパターンを登録できます。

<Route path={['/users', '/profiles']} children={<UserList />} />

children

React 的には、以下の二つの書き方は同じ意味です。

<Route path="/abc" children={<MyComponent />} />

<Route path="/abc">
  <MyComponent />
</Route>

子要素は children というプロパティ名で渡されるからです。

exact

exact プロパティは、URL パターンを完全一致でマッチング判定させたい場合に付与します。

たとえば、exact を付けずに以下のように書いた場合、/about は一個目の / にマッチしてしまい、<About /> ではなく <Index /> が表示されます。デフォルトの判定は前方一致だからです。

NG
<Switch>
  <Route path="/" children={<Index />} />
  <Route path="/about" children={<About />} />
</Switch>

exact を付ければ、その path に完全一致しない限りはマッチしません。以下のように書けば、/about/ にマッチしなくなります。

OK
<Switch>
  <Route path="/" exact children={<Index />} />
  <Route path="/about" children={<About />} />
</Switch>

マッチングの定義をしたパスへのリンクには Link コンポーネントを利用します。

import { BrowserRouter, Switch, Route, Link } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">index</Link>
        <Link to="/about">about</Link>
      </nav>
      <Switch>
        <Route path="/" exact children={<Index />} />
        <Route path="/about" children={<About />} />
      </Switch>
    </BrowserRouter>
  );
}

Link は最終的には <a> に描画されるので、titleclassName などアンカー要素に付与できる属性は Link にも付与できます。

<Link to="/" title="..." className="...">index</Link>

to プロパティの指定方法は、文字列、オブジェクト、関数の3種類あります。それぞれ簡単に紹介します。それ以外のプロパティも含めて、詳細はマニュアルを参照してください。

String

文字列での指定はシンプルですね。パスをそのまま指定します。

<Link to="/cat"></Link>

Object

オブジェクト形式では、クエリパラメータや追加データ(state)などを指定できます。

<Link to={{
  pathname: '/cat', // パス文字列
  search: '?breed=persian', // クエリパラメータ
  hash: '#the-hash', // URLハッシュ
  state: { fromDogPage: true } // 任意のデータ
}}>ペルシャ猫</Link>

ちなみに state データは遷移先のコンポーネントで、location オブジェクトから参照できます。location オブジェクトは、useLocation フックを使って取得します。

import { useLocation } from 'react-router-dom';

function Cat() {
  const location = useLocation();
  if (location.state) {
    console.log(location.state.fromDogPage);
  }

  // 以下略...
}

Function

関数で渡すパターンは少し見た目がややこしいですが、「現在の location」を引数に取って、上述の文字列またはオブジェクトを返却します。

<Link to={location => `${location.pathname}?breed=persian`}>ペルシャ猫</Link>

現在の location を加工したリンクを生成する場合に使用するのでしょう。

設定ファイル化

Vue Router は、ルーティングの設定を配列にまとめて定義します。React Router 自体にはそのような書き方は存在しませんが、単純に JavaScript コードとして、たとえば以下のようにまとめることはできます。

const routes = [
  {
    path: '/',
    exact: true,
    children: <Index />
  },
  {
    path: '/about',
    children: <About />
  }
];

export default routes;
import routes from './routes.js';

function App() {
  return (
    <BrowserRouter>
      <Switch>
        {routes.map((config, i) => (
          <Route key={i} {...config} />
        ))}
      </Switch>
    </BrowserRouter>
  );
}

他にもユースケースに応じて色々な書き方ができるでしょう。ライブラリ固有のシンタックスではないので、むしろ自由度が高いとも言えます。

無理に上記のようにまとめる必要はないと思いますが、場合によっては一元管理できて良いかもしれません。

URL パラメータ

パスパラメータの取得

パスパラメータの定義方法は、上でも触れましたが Vue Router と同じで、path-to-regexp の仕様に準じます。

<Switch>
  <Route path="/users/:id" children={<User />} />
</Switch>

パラメータの取得には、useParams フックを用います。

import { useParams } from 'react-router-dom';

function User() {
  const { id } = useParams();

  // ...
}

パラメータ変更の検知

パラメータの変更を検知して、変更があったときのみ何か処理を行いたい場合は、React の組み込みフック useEffect を利用すればよいでしょう。

import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';

function User() {
  const { id } = useParams();

  useEffect(() => {
    console.log(`URL param changed: ${id}`);
  }, [id]);

  // 以下略...
}

クエリパラメータ

次にクエリパラメータです。React Router には、クエリパラメータを取得するための固有のシンタックスは存在しません。location オブジェクトの search プロパティにクエリ文字列が入っているので、各自でパースする必要があります。

const location = useLocation();
const search = location.search; // -> "?foo=bar&baz=piyo"

// URLSearchParamsはIE11非対応
const query = new URLSearchParams(search);

query-string などのライブラリを使ってもいいかもしれません。

マニュアルでは、カスタムフックにまとめるアイディアが紹介されています。

function useQuery() {
  const location = useLocation();
  return new URLSearchParams(location.search);
}

function MyComponent() {
  const query = useQuery();

  // 以下略...
}

location オブジェクト

すでに登場していますが、location オブジェクトには、現在のルートに関する情報が詰まっています。

location オブジェクトを取得するには、useLocation フックを用います。

const location = useLocation();

location オブジェクトは以下のプロパティを持っています。

{
  key: 'abc123',
  pathname: '/path/to/current/page',
  search: '?foo=bar&baz=piyo',
  hash: '#some-hash',
  state: {
    one: true,
    two: 'string',
    three: { four: 5 }
  }
}

stateLinkRedirect、またプログラムによるナビゲーション時に付与することのできるユーザー定義のデータです。

リダイレクト

リダイレクトには、Redirect コンポーネントを使用します。

以下の例では、authenticatedtrue の場合には /mypage に紐づくコンポーネント(Route で定義されている前提)が描画されます。

<Route exact path="/">
  {authenticated ? <Redirect to="/mypage" /> : <Home />}
</Route>

Route コンポーネントと同様に、to プロパティにはオブジェクトも指定できます。

Switch の中で使用する際は、from プロパティを用います。

<Switch>
  <Redirect from="/from-path" to="/to-path" />
  <Route path="/to-path" children={<MyComponent />} />
</Switch>

ナビゲーションガード

前述の通り、React Router では Vue Router のようなナビゲーションガードの機能は提供されていません。ここでは、よくありそうな、ナビゲーション前後に処理を加えたいユースケースについて、React Router での解決策を紹介します。

遷移先コンポーネント描画前

まずは遷移先コンポーネント描画前に処理を加えるパターンです。

ログイン済みのユーザーしか閲覧できないコンテンツを描画する前に認証チェックする、などがあるあるなユースケースでしょう。

そういった場合、Route コンポーネントをラップしたコンポーネントを作成して対応します。

import React from 'react';
import { Route, Redirect } from 'react-router-dom';

// 特定のルートで前処理を加えるラッパーコンポーネント
function GuardedRoute(props) {
  // ここで何らかの前処理を行う

  // たとえば条件によるリダイレクトなど
  if (notAuthenticated) {
    return <Redirect to="/login" />
  }

  // 前処理が終わったあと、普通にRouteをレンダリングする
  // {...props} はプロパティをそのまま展開するための記述
  return <Route {...props} />;
}

export default GuardedRoute;

前処理を加えたいルートに対しては、ラッパーコンポーネントを使用します。

<Switch>
  <Route path="/cat" children={<Cat />} />
  <GuardedRoute path="/dog" children={<Dog />} />
  <GuardedRoute path="/bird" children={<Bird />} />
</Switch>

上の例だと、/cat に対しては前処理は行われず、/dog/bird のルートでは前処理が行われます。React ではこういったケースもコンポーネントで対処するのですね。

別の前処理が必要な場合は、別のラッパーコンポーネントを作成すればOKです。

遷移直前

遷移直前に処理を行う場合に関しては、何かの条件で「ページを離れますがよろしいですか?」 などの確認ダイアログを出す機能が、React Router から提供されています。

あるページコンポーネントに Prompt コンポーネントを加えると、そのコンポーネントを離れる直前にブラウザの確認ダイアログが表示されます。

return (
  <>
    <Prompt
      when={condition}
      message="入力した内容が保存されていません。このページから離れますか?"
    />
    <form>...</form>
  </>
);

ダイアログで「OK」をクリックするとナビゲーションが走り、「キャンセル」をクリックするとナビゲーションが中止されます。

when プロパティにダイアログが表示される条件(Boolean)を指定します。when を置かない場合は必ずダイアログが表示されます。

プログラムによるナビゲーション

Link からではなく、動的にナビゲーションを発生させるには、history オブジェクトを使用します。

history オブジェクトは、useHistory フックから取得します。

import { useHistory } from 'react-router-dom';
const history = useHistory();
history.push('/about'); // /aboutへのナビゲーションが発生

history オブジェクトは History API と似たインターフェースを持っています。

history.push('/about', { someState: 'foo' });
history.replace('/about');
history.replace('/about', { someState: 'foo' });
history.go(2);
history.goBack(); // = go(-1)
history.goForward(); // = go(1)

さらに、前述の Prompt の機能を動的に呼び出すことも可能です。

const unblock = history.block('このページを離れますか?');
unblock(); // ブロック解除 = ナビゲーション発生

404ページ

どこにもマッチしなかった場合に Not Found 的な画面を見せたい場合は、以下のように path にワイルドカード(*)を使った指定で実現できます。

<Switch>
  <Route path="/cat" children={<Cat />} />
  <Route path="/dog" children={<Dog />} />
  <Route path="*" children={<NotFound />} />
</Switch>

ワイルドカードルートは、Switch 内で一番下に定義されていなければいけません。上から順番に見ていって、マッチするルートがなかった場合は、最後に必ずワイルドカードルートにマッチするという仕組みです。

スクロールの振る舞い

最後に、ページ遷移時にスクロール位置を上部に戻る方法を紹介します。

マニュアルで紹介されているままですが、パスの変更を検知してスクロール位置を変更するコンポーネントを作成します。

import { useEffect } from "react";
import { useLocation } from "react-router-dom";

function ScrollTop() {
  const { pathname } = useLocation();

  // パス変更時に画面トップへスクロール
  useEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);

  return null; // 何もレンダリングしない
}

export default ScrollTop;

特に何もレンダリングしないコンポーネントは、null を返すようです。

Router のすぐ下に配置します。

<BrowserRouter>
  <ScrollTop />
  <Switch>
    <Route path="..." children={...} />
    <Route path="..." children={...} />
    <Route path="..." children={...} />
  </Switch>
</BrowserRouter>

React ではなんでもコンポーネントですね。


以上、React Router の基本機能について紹介しました。ここで紹介した機能だけでも使い始められると思いますが、より細かいコンポーネントのプロパティや仕様について知る必要が出た際は、ぜひマニュアルを参照してください。

Vue.jsエンジニアのためのReact入門』でも思いましたが、やはり React 勢はシンプル、というか Vue 関連のほうが親切な機能が最初からいっぱい付いてるって感じですね。

この記事が React 学習の一助となれば幸いです 😇