ささっと学ぶReact Router v4

記事としては旬な時期をとっくにすぎていますが、ブログ内にReactやReduxのチュートリアルがあるのにReact Rotuerの説明がないので、必要そうなものだけ書いときます。

React Routerとは

Reactでアプリケーションを作成する上で必須とも言えるのがReact Routerです。React Routerは名前の通り、Reactでのルーティングを担当するライブラリです。

大雑把に言えば、URLの変化に合わせて何か処理をするにはReact Routerが必要なのです。

インストール

React Routerは3つのパッケージがあります。

  • react-router
  • react-router-dom
  • react-router-native

ブラウザで使う場合は、react-router-domを使います。

yarn add react-router-dom

ルーティング導入

React RouterはBrowserRouterHashRouterというコンポーネントを使います。前者はHTML5のhistory APIを使っており、後者はwindow.locationを利用しています。古いブラウザもサポートしなければならない場合は、後者を利用する必要があります。

例えば、Appコンポーネントの中でルーティングを行う場合は以下になります。

// index.js

import React from "react";
import { BrowserRouter as Router } from "react-router-dom";
import App from "./App";

ReactDOM.render(
	<Router>
		<App />
	</Router>,
document.getElementById("root")
);

実際のルーティング内容はBrowserRouterで囲ったコンポーネント内に記述します。

ルーティング基礎

シンプルなルーティング例を書くと以下になります。

import React from "react";
import { Link, Route } from "react-router-dom"

const Home = () => <h2>Home</h2>;
const App = () => <h2>App</h2>

const App = () => {
  return (
    <div>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Route exact path="/" component={Home} />
      <Route path="/about" component={About} />
    </div>
  );
};

export default App;

Linkはhtmlのアンカータグのようにtoで指定したURL先への移動に利用します。実際に出力されるときもアンカータグで書き出されます。

Routeはそのルーティング先を描画するコンポーネントです。pathで一致したときに、componentで指定したコンポーネントが描画されます。

HomeリンクをクリックするとHomeコンポーネントがレンダリングされ、AboutをクリックするとAboutコンポーネントが描画されます。

Demo

exact

exactpathがURLと正確に一致したときを示します。先のコードからexactを取り除くと、/aboutでAboutコンポーネントだけでなく、Homeコンポーネントも描画されます。

これは/about/が含まれるため、両者ともルーティング先と見なされるためです。React Router 4では、このようにpathにマッチするすべてのRouteがルーティング対象となるため、注意が必要です。

render and chidren

コンポーネントの描画には、componentでコンポーネントを指定する以外にも、renderchildrenを使う方法もあります。renderはインナーレンダリングを行います。

<Route path="/" render={(props) => <h2>Home</h2>} />

childrenrenderと同じでインナーレンダリングを行うのですが、パスにマッチしない場合でもルーティングします。childrenの利用には他の解説を先に行う必要があるため、詳細は後述する「routeのカスタマイズ」で行います。

props

上記の例でも定義していますが、レンダリングする関数はReact Routerのpropsを受け取ります(クラスの場合はthis.props)。これには以下のオブジェクトが格納されています。

  • match(pathに一致したURL情報)
  • location(主に現在のロケーション情報)
  • history(ルーティングした過去の履歴情報)

尚、propsrenderに限らず、componentchildrenでも取得可能です。

matchとURLパラメーター

よく利用するのはmatchでしょう。このオブジェクトは、routepathとURLのロケーションが一致したときに作成されます。

例えばURLに末尾にパラメーターがある場合は、このオブジェクトで取得ができます。

const Drink = ({ match } ) => <p>Drink ID: {match.params.id}</p>;

const App = () => {
  return (
    <div>
      <Link to="/drink/1">coke</Link>
      <Link to="/drink/2">tea</Link>
      <Link to="/drink/3">coffee</Link>
      <Route path="/drink/:id" component={Drink} />
    </div>
  );
};

matchにはいくつかのプロパティが用意されています。ここで利用したparamsはURLパラメーターを含むオブジェクトです。他にもマッチしたURLを表すurlroutepathを示すpathなどがあります。

上記でmatchをコンソール出力すると、以下のオブジェクトが確認できます

{
	/* coffeeのリンクをクリックした場合 */
	path: "/drink/:id",
	url: "/drink/3",
	isExact: true,
	params: {
        id: "3"
	}
}

Demo

ネストされたルーティング

matchを使うと、ネストされたルーティングを実現しつつハードコーティングを減らせます。

const Food = ({ match }) => {
  return (
    <div>
      <Link to={`${match.url}/ramen`}>ラーメン</Link>
      <Link to={`${match.url}/rice`}>ご飯</Link>
      <Route
        path={`${match.path}/:name`}
        render={({ match }) => <h3>{match.params.name}</h3>}
      />
    </div>
  );
};

const App = () => {
  return (
    <div>
      <Link to="/food">Food</Link>
      <Route path="/food" component={Food} />
    </div>
  );
};

Foodコンポーネントのmatchには先に解説したようなURL情報を含むオブジェクトが格納されているので、それらを利用してネストするLinkRouteを構築しているだけです。

Demo

Switch

Switchは複数のrouteを囲い、グループ化するのに使います。Switchで囲まれたrouteは、現在のpath名に一致する最初のrouteのみをレンダリングします。

よく利用されるのは、404ページ(Not Found)です。

<Switch>
    <Route exact path="/" render={() => <h2>Home</h2>} />
    <Route path="/about" render={() => <h2>About</h2>} />
    <Route render={() => <h2>Not Found</h2>} />
</Switch>

pathを記述しないRouteを定義すると、それはすべてのパスでルーティング対象となります。結果、どのレンダリングの結果にもそのrouteの内容が出力されます。

Swtichで囲むことにより、内部のroutepathが1つずつ評価され、どれにも該当しなかったときにpathのないrouteが選ばれることになります。それが404ページ(Not Found)です。

Demo

routeのカスタマイズ

render and childrenで保留していたchildrenを使うと、ルーティングをカスタマイズできます。

childrenrenderと使い方は同じですが、path一致しない場合でも呼びだされます。このため、pathに一致している・していないときで、レンダリング内容を変えることができます。

const App = () => {
  return (
    <div>
      <Link to="/">Home</Link><br />
      <Link to="/about">About</Link><br />
      <Route
        path="/blog"
        children={({ match }) => {
          return (
            <div>
              {match ? ">" : ""}
              <Link to="/blog">Blog</Link>
            </div>
          );
        }}
      />

      <Route exact path="/" render={() => <h2>Home</h2>} />
      <Route path="/about" render={() => <h2>About</h2>} />
      <Route path="/blog" render={() => <h2>Blog</h2>} />
    </div>
  );
};

Routeの中でLinkを書き出すというものですが、前述したようにchildrenpathと一致していない場合でも呼びだされるということを頭に入れるとその挙動がわかると思います。

関数内で受け取るmatch に値があるときは、Routepathに一致したということです。この例ではpathが一致しているときは、描画するLinkの横に矢印が追加されるようになっています。

Demo


他のLinkでも同様の条件を設けたい場合、上記の方法では1つずつ記述するため冗長になってしまいます。通常はカスタムリンクを作成するのが便利です。

const CustomLink = ({ children, exact, to }) => (
  <Route
    path={to}
    exact={exact}
    children={({ match }) => (
      <div>
        {match ? ">>" : ""}
        <Link to={to}>{children}</Link>
      </div>
    )}
  />
);

const App = () => {
  return (
    <div>
      <CustomLink exact={true} to="/">Home</CustomLink>
      <CustomLink to="/about">About</CustomLink>

      <Route exact path="/" render={() => <h2>Home</h2>} />
      <Route path="/about" render={() => <h2>About</h2>} />
    </div>
  );
};

CustomLinkは親から受け取った各propsを使い、条件を加えた上でLinkを書き出しています。先の例とやっていることは同じですが、Linkの数が増えてもこの1つのカスタムリンクコンポーネントで処理できます。

尚、CustonLink関数の引数のchildrenは親要素から受け取る子要素を表すpropsで、Routechildrenと別物なので注意してください。

Demo

NavLinkLink同様の機能を持っていますが、アクティブなリンクを簡単にスタイリングする機能を持っています。

<NavLink activeClassName="active" to="/" exact>Home</NavLink>
<NavLink activeClassName="active" to="/about">About</NavLink>

<Route exact path="/" render={() => <h2>Home</h2>} />
<Route path="/about" render={() => <h2>About</h2>} />

例えば/にいるときは、1つ目のNavLinkactiveというclass名が付与されます。このため、別途CSSでスタイリングを定義する必要があります。

.active {
  color: cornflowerblue;
  font-weight: bold;
}

尚、exactをつけると、ロケーションが正確に一致するときのみ適用されます。

所謂プログラムチックにロケーションを移動させることもできます。historyオブジェクトを使うだけです。push()を使うと、historyに新しい履歴を追加することができます。結果、ロケーションが移動します。

class Form extends Component {
  onSubmit = () => this.props.history.push("/");
  render() {
    return (
      <form>
        <textarea rows="4" cols="40" placeholder="問い合わせ内容" /><br />
        <button onClick={this.onSubmit}>Submit</button>
      </form>
    );
  }
}

const App = () => {
  return (
    <div>
      <ul>
        <li><Link to="/">Home</Link></li>
        <li><Link to="/about">About</Link></li>
        <li><Link to="/form">Form</Link></li>
      </ul>
      <Route exact path="/" render={() => <h2>Home</h2>} />
      <Route path="/about" render={() => <h2>About</h2>} />
      <Route path="/form" component={Form} />
    </div>
  );
};

フォームの送信ボタンを押すと、ホームにリダイレクトされます。

Demo

Redirect

転送を行うRedirectもあります。これはメソッドではなくコンポーネントなので、先の例のようにイベントの処理関数に直接記述して移動…いう使い方ではできません。今回は長くなるので触れませんが、Redirectはユーザー認証などでよく利用されます。

toを指定した先にリダイレクトが行われるため、次の例は/formへのルーティングを/に転送し直します。

<Route path="/form" render={() => <Redirect to="/" />} />

サンプルとして先の例を無理矢理Redirectで書き直したものを置いておきます。

Demo

withRouter

Link以外でナビゲーションを行うには、前述したhistory.pushを呼び出すのが一般的です。Routeで直接レンダリングされるコンポーネントは、React Routerのpropsからhistoryにアクセスできます。しかし、直接レンダリングされないコンポーネントではどうでしょう?

アクセスできません!

wtihRouterは、React RouterのHOC(高階コンポーネント)であり、historyなどのReact Routerのpropsを指定コンポーネントにバインドします。使い方はwithRouterでコンポーネントをラップするだけです。

import { withRouter } from "react-router-dom";

// propsにhistory, location, matchが格納される
const MyButton = withRouter((props) => {
  return (
    <button onClick={() => props.history.push(props.path)}>
      {props.text}
    </button>
  );
});

<MyButton text="クリック" path="/about" />のように利用します。このコンポーネントはReact Routerが直接扱うコンポーネントではありませんが、withRouterによりルーティングのpropsが利用できるようになっています。

Demo


余談ですが、historyオブジェクトはhistory.listen()というメソッドがあります。これはルーティングが変更されるたびにコールバック関数を呼ぶメソッドです。つまりルーティングイベントを監視できます。

Routeと関係のないコンポーネントでもwithRouteを使うと、このルーティングイベントの変更を取り扱えます。下記はMyComponentがマウントされている限り、ルーティング変更のたびにそのpathnameをコンソール出力します。

import React, { Component } from "react";
import { withRouter } from "react-router-dom";

class MyComponent extends Component {
  componentWillMount() {
    this.props.history.listen(() => {
      console.log(this.props.history.location.pathname);
    });
  }

  render() {
    return <p>コンソール画面を確認してください</p>;
  }
}

export default withRouter(MyComponent);

解説が前後していますが、withRouterをステートフルなコンポーネント(クラス)で定義した場合は通常のprops同様、this.propsからReact Routerのpropsにアクセスできます。

Demo

Redux

withRouterはReduxでの接続でも使えます。単にconnect()を先程のようにwithRouterでラップするだけです。

withRouter(connect(mapStateToProps)(SomeComponent));

余力があれば、connected-react-routerのようなライブラリも調べてみるといいと思います。