React入門 ~React Router編~

React入門記事、第2弾。
前回は基礎的なことをまとめましたが、今回はReactアプリのルーティング設定を行ううえでよく使われているReact Routerについてまとめました。

※この記事はQiitaからの転載です。

React Routerとは? #

ReactでSPAを書くにあたって、DOMを書き換えて複数ページがあるように見せても、URLが変わらずブラウザからは1つのページとしてしか認識されません。
そこで、SPAの画面状態とURLとを紐づけるルーティングを行うことで、history APIを操作できるようにして、URLを指定して直接特定の画面にいけたり、ブラウザバックを利用できるようにすることができます。

このルーティングを行うデファクトのライブラリがReact Routerです。
React Routerを使うことでhistory APIを操作して、画面遷移も行ってくれます。

インストール #

$ yarn add react-router-dom

今回の使用バージョンは5.1.2です。

使い方 #

以下のコードは公式ドキュメントをコードを引用、もしくは元にしています。
React Router

※2020/2/22 追記
以下の記事を参考に、React Hooksを使ったやり方を記述するなど、全体的に記事を見直しました。

基本的な使い方 #

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

const App = () => {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to='/'>Home</Link>
            </li>
            <li>
              <Link to='/about'>About</Link>
            </li>
            <li>
              <Link to='/users'>Users</Link>
            </li>
          </ul>
        </nav>

        <Switch>
          <Route path='/about'>
            <About />
          </Route>
          <Route path='/users'>
            <Users />
          </Route>
          <Route path='/'>
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

const Home = () => {
  return <h2>Home</h2>;
}

const About = () => {
  return <h2>About</h2>;
}

const Users = () => {
  return <h2>Users</h2>;
}

export default App;

React Routerの基本動作GIF

ルーティングに必要なものをimport #

ルーティングに最低限必要なコンポーネントをimportします。

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

ルート階層でコンポーネント全体をBrowserRouterで囲う #

ここではRouterに命名しています。
このBrowserRouterコンポーネントは、画面遷移時にヒストリーAPIに履歴情報を追加してくれるものです。

const App = () => {
  return (
    <Router>
    .
    .
    .
    </Router>
  )
}

URLごとのレンダリング内容の定義 #

Switchコンポーネントで囲い、RouteコンポーネントでそれぞれのURLに応じたレンダリング内容を記述します。
上から順にURLとpathを比較し、一致するルートの内容を返します。

注意点として通常では完全一致で比較しません。
例としてpathが/aboutの場合、/about/aなどでも一致とみなされます。
完全一致にしたい場合は下記参照。

<Switch>
  <Route path='/about'>
    <About />
  </Route>
  <Route path='/users'>
    <Users />
  </Route>
  <Route path='/'>
    <Home />
  </Route>
</Switch>
.
.
.
const Home = () => {
  return <h2>Home</h2>;
}

const About = () => {
  return <h2>About</h2>;
}

const Users = () => {
  return <h2>Users</h2>;
}

なお、Routeのpathは一度に複数定義することもできます。
以下の場合は、/about/profile両方のパスでAboutコンポーネントをレンダリングします。

<Route path={['/about', '/profile']}>
  <About />
</Route>

リンクの作成 #

Linkコンポーネントで画面遷移するリンクを作成。toでリンクURLを指定。
後にaタグに変換されるようになっており、外部リンクも指定可能です。

なお、この例では一つのコンポーネント内に共存していますが、LinkコンポーネントはSwitch、Routeコンポーネントを使用しているコンポーネント内でしか使用できないということはありません。

<nav>
  <ul>
    <li>
      <Link to='/'>Home</Link>
    </li>
    <li>
      <Link to='/about'>About</Link>
    </li>
    <li>
      <Link to='/users'>Users</Link>
    </li>
  </ul>
</nav>

これで基本的なルーティングを作成することができます。

レンダリング内容で別ファイルに切り出したコンポーネントを使いたい #

コンポーネントをimportして、Routeのcomponentで渡してあげればOKです。

import About from './component/About';
import Users from './component/Users';
import Home from './component/Home';
.
.
.
<Switch>
  <Route path='/about' component={About} />
  <Route path='/users' component={Users} />
  <Route path='/' component={Home} />
</Switch>

より正確なルーティングにしたい #

URLとpathの完全一致にしたい場合は、Routeにexactをつけます。

<Switch>
  <Route path='/about' exact>
    <About />
  </Route>
  <Route path='/users' exact>
    <Users />
  </Route>
  <Route path='/' exact>
    <Home />
  </Route>
</Switch>

定義していないURLにアクセスされた場合のレンダリング内容を指定したい #

Switchの末尾にRouteを追加。
pathを指定しないRouteを最後に記述しておくことで対応できます。

<Switch>
  <Route path='/about'>
    <About />
  </Route>
  <Route path='/users'>
    <Users />
  </Route>
  <Route>
    <Error />
  </Route>
</Switch>

パスパラメータを使いたい #

URLのパスパラメータを受け付けるようにする場合は、Routeのpathで:aboutIdのように:をつけて指定します。

そして、レンダリング内容の方でuseParamsフックを使用して取り出すことができます。
以下の場合、useParams()の返り値は{aboutId: "1"}となり、分割代入 + ショートハンドを使用して変数に代入しています。

import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useParams
} from 'react-router-dom';
.
.
.
<Link to='/about/1'>About</Link>
.
.
.
<Switch>
  <Route path='/about/:aboutId'>
    <About />
  </Route>
</Switch>

const About = () => {
    const { aboutId } = useParams();
    return <h2>About:{aboutId}</h2>
  }

usePamramを使用した場合の動作GIF

クエリパラメータや任意のデータを使いたい #

Linkコンポーネントのtoには、オブジェクト形式でデータを渡すことができます。

  • pathname:URL
  • search:クエリパラメータ
  • hash:URLハッシュ
  • state:ユーザ定義のデータ

レンダリング内容の方でuseLocationフックを使用して、locationオブジェクトとして取り出しが可能です。
以下の場合、useLocation()の返り値は次のようになります。
コンソールに出力したlocationオブジェクトの内容

import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useLocation
} from 'react-router-dom';
.
.
.
const to = {
  pathname: '/users',
  search: '?class=A',
  hash: '#user-hash',
  state: { test: 'test-state' }
};
.
.
.
<Link to={to}>Users</Link>
.
.
.
<Switch>
  <Route path='/users'>
    <Users />
  </Route>
</Switch>

const Users = () => {
  const location = useLocation();
  return (
    <React.Fragment>
      <h2>Users</h2>
      <p>pathname:{location.pathname}</p>
      <p>search:{location.search}</p>
      <p>hash:{location.hash}</p>
      <p>state:{location.state.test}</p>
    </React.Fragment>
  );
}

useLocationを使用した場合の動作GIF

ちなみにクエリパラメータを取り出して扱いたい場合は、query-stringというライブラリを使うと便利です。
上記のコード例においてuseLocationで取り出した、locationオブジェクトのクエリパラメータに対して

import queryString from 'query-string';
.
.
.
queryString.parse(location.search)

とすると、{class: "A"}のようにオブジェクト形式に変換してくれるため、扱いやすくなります。

query-stringを頻繁に使う場合は、あらかじめuseLocationquery-stringを組み合わせたカスタムフックを作っておく手もありです。

Linkコンポーネントを使わずに遷移させたい #

ボタンのonClickアクションなどで画面遷移させたい時は、historyオブジェクトを使用します。

useHistoryフックを使用することで、historyオブジェクトの取り出しが可能です。
以下はナビゲーション用のコンポーネントを作成した例です。
history.pushを使用して、履歴を追加することでURLを変化させ画面遷移を実現しています。

※aタグとは違うので、リンクを右クリックして別タブで開くといったことはできません。

import React from 'react';
import { useHistory } from 'react-router-dom';

const Nav = props => {
  const history = useHistory();
  const handleLink = path => history.push(path);
  return (
    <nav>
      <button onClick={() => handleLink('/about')}>About</button>
      <button onClick={() => handleLink('/users')}>Users</button>
      <button onClick={() => handleLink('/')}>Home</button>
    </nav>
  );
}

export default Nav;

useHistoryを使用した場合の動作GIF

historyオブジェクトでできること #

historyオブジェクトの中身は以下のようになっており、locationオブジェクトも含まれています。
※history.push後の例です。
コンソールに出力したhistoryオブジェクトの内容

historyオブジェクトはHistory APIと完全一致ではありませんが、似たような処理ができます。

// 履歴の追加
history.push('/about')

// 履歴の追加 + ユーザ定義データの受け渡し
history.push('/about', { someState: 'foo' });

// 履歴の書き換え
history.replace('/about');

// 履歴の書き換え + ユーザ定義データの受け渡し
history.replace('/about', { someState: 'foo' });

// 履歴を2つ進める
history.go(2);

// 履歴を1つ戻る
history.goBack();

// 履歴を1つ進める
history.goForward();

// 上記の履歴変更の前に記述しておくと、遷移前にアラートを出す
history.block('このページを離れますか?');

ネストしたルーティングを実現したい #

useRouteMatchフックで取得できる、matchオブジェクトを使用することで実現できます。
なお、matchオブジェクトの中身は次のようになります。
※以下のコードでTopicsコンポーネントをレンダリング時の例
コンソールに出力したmatchオブジェクトの内容

import React from 'react';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useRouteMatch
} from 'react-router-dom';

const App = () => {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to='/'>Home</Link>
          </li>
          <li>
            <Link to='/topics'>Topics</Link>
          </li>
        </ul>

        <Switch>
          <Route path='/topics'>
            <Topics />
          </Route>
          <Route path='/'>
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

const Home = () => {
  return <h2>Home</h2>;
}

const Topics = () => {
  const match = useRouteMatch();

  return (
    <div>
      <h2>Topics</h2>

      <ul>
        <li>
          <Link to={`${match.url}/components`}>Components</Link>
        </li>
        <li>
          <Link to={`${match.url}/props-v-state`}>
            Props v. State
          </Link>
        </li>
      </ul>

      <Switch>
        <Route path={`${match.path}/components`}>
          <h3>Components</h3>
        </Route>
        <Route path={`${match.path}/props-v-state`}>
          <h3>props-v-state</h3>
        </Route>
      </Switch>
    </div>
  );
}

export default App;

ネストしたルーティングの動作GIF

リダイレクト #

Redirectコンポーネントを使用して、リダイレクトが実現できます。

以下はauthenticated変数によって遷移する先を変化させている例で、ログインしている場合はマイページ、していない場合は通常のホーム画面といった感じです。

import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  Redirect
} from 'react-router-dom'
.
.
.
<Switch>
  <Route path='/mypage'>
    <Mypage />
  </Route>
  <Route path='/'>
    {authenticated ? <Redirect to="/mypage" /> : <Home />}
  </Route>
</Switch>

また、以下のような書き方もできます。
こちらの場合はfromを使用していて、fromのURLにアクセスされたら、toのURLにリダイレクトします。

import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  Redirect
} from 'react-router-dom'
.
.
.
<Switch>
  <Redirect from='/test' to='/other' />
  <Route path='/other'>
    <Other />
  </Route>
  .
  .
  .
</Switch>

※旧式:HOCでのlocation、history、matchオブジェクトの取り扱い #

上記で書いていた、use〇〇フックはReact Hooksの一種です。
React Hooksが登場する前はHOCベースのやり方が普及しており、こちらでは主にwith〇〇というものでコンポーネントをラップしたりするやり方でした。

React RouterにおいてはwithRouterというものがあり、これでコンポーネントをラップすることで、そのコンポーネントのpropsにmatch、location、historyオブジェクトが渡され、操作できるようになります。

以下はhistoryオブジェクトを使用する例です。

import React from 'react';
import { withRouter } from 'react-router';

const HistoryNav = props => {
  const handleLink = path => props.history.push(path);
  return (
    <nav>
      <button onClick={() => handleLink('/about')}>About</button>
      <button onClick={() => handleLink('/users')}>Users</button>
      <button onClick={() => handleLink('/')}>Home</button>
    </nav>
  );
}

export default withRouter(HistoryNav);

参考リンクまとめ #

シリーズ記事 #