第 22 章 React でページ移動を作る

20 章で Gravatar の画像を表示した際、 <a href="/users/1"> のようなリンクを作成しました。このままでは、この画像をクリックした時には一旦ページを離れて、 Rails が出力した /users/1 の HTML を取得し、その内容を表示してしまいます。しかし、共通部分を除いて JavaScript でページの内容だけを書き換えた方が、よりスムーズなレスポンスになるとは感じないでしょうか。このようなページ自体の遷移をなくして JavaScript によるページ書き換えと URL を対応させたのが、 Single Page Application (SPA) です。

JavaScript での URL 処理

Rails が生成するオールドスタイルな web アプリケーションでは、基本的に一つの URL が一つのページに対応していました。例えば以下の通りです。

  • / => トップページ
  • /users/:id => ID に該当するユーザプロフィールページ

SPA でも、現時点で表示している内容に対応する URL をブラウザに認識させたいという要望があります。例えば、トップページからユーザプロフィールページに JavaScript で書き換えたとして、ブラウザバックでトップページに戻れないとなるととても不便です。

History API には pushState によるブラウザ URL の書き換えや、 onpopstate というフックにより URL の変化からの処理実行があります。

しかし、 pushState での URL 書き換えを画面の変更に合わせて書くのは非常に煩雑です。React での URL 制御を便利に取り回すライブラリとして今回は React Router を使ってみましょう。

Hello, React Router

それでは、 React Router を始めましょう。 react-router-dom とその型定義をインストールします:

npm i -E react-router-dom@5.2.0
npm i -D -E @types/react-router-dom@5.1.7

まずは移動先としてメッセージを表示するだけのコンポーネントを作ります。そしてそれを React Router でルーティングします。

// app/javascript/components/HelloRouter.tsx

const HelloRouter = () => {
  return <>Hello, Router!</>;
};

export default HelloRouter;
// app/javascript/components/index.tsx

import { BrowserRouter, Link, Switch, Route } from "react-router-dom";
import HelloRouter from "./HelloRouter";
import { Home } from "./static-pages";

const App = () => {
  return (
    <BrowserRouter>
      <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/hello">Say Hello Router</Link>
          </li>
        </ul>
      </nav>

      <Switch>
        <Route exact path="/">
          <Home></Home>
        </Route>
        <Route path="/hello">
          <HelloRouter></HelloRouter>
        </Route>
      </Switch>
    </BrowserRouter>
  );
};

export default App;

画面を再読み込みすると、画面上部に新たにリンクが2つ表示されています。 <Link> コンポーネントは画面遷移が React Router によって制御される <a> タグ(本物の <a> タグ)をレンダーします。 <Route> コンポーネントは path 属性に URL のパスを指定し、そのパスにおいて描画したいコンポーネントを子コンポーネントとして書きます。

<Route> コンポーネントたちの親にさらに <Switch> コンポーネントがあると、パスに最初に合致したコンポーネントだけが描画されるようになります( <Switch> が無いとパスが合致したコンポーネントすべてが描画されるのです。詳しくは 公式APIドキュメント を読んでください)。

さて、画面に表示されたリンクを何回かいろいろクリックしてみて、画面が素早く遷移することを確認してください。 また開発者コンソールの Network タブを確認すると、 /hello へのリンクをクリックしても実際にサーバにリクエストは送られていないこともわかると思います。

しかしこのままでは想定しない動作を Rails サーバ側が起こしてしまうようになります。URL が http://localhost:3000/hello となった状態でブラウザをリロードすると、 Rails 側でそのような route は存在しないというエラーが発生してしまいます。そこで、 Rails で処理できない URL へのリクエストも StaticPagesController#home へと到達させるようにします。

# config/routes.rb

Rails.application.routes.draw do
  # 最終行に追加
  # ただし /rails 以下は特別な意味を持つので :any としてはルーティングさせない
  scope ':any', as: :any, constraints: { any: /(?!rails\/).*/ } do
    root to: 'static_pages#home'
  end
end

これで http://localhost:3000/hello にブラウザで直接アクセスしても、 Rails 側でエラーが出ることなく HelloRouter コンポーネントが表示できるようになると思います。

さらに React Router 側でも、定義されていないルートにアクセスされた際にページが存在しないことをユーザに伝えるようなコンポーネントを用意しておきます。

// app/javascript/components/PageNotFound.tsx

const PageNotFound = () => {
  return <>Page Not Found.</>;
};

export default PageNotFound;
// app/javascript/components/index.tsx

import { BrowserRouter, Link, Switch, Route } from "react-router-dom";
import PageNotFound from "./PageNotFound";
import { Home } from "./static-pages";

const App = () => {
  return (
    <BrowserRouter>
      <Switch>
        <Route exact path="/">
          <Home></Home>
        </Route>
        <Route path="*">
          <PageNotFound></PageNotFound>
        </Route>
      </Switch>
    </BrowserRouter>
  );
};

export default App;

useParams

Routepath にはパラメータを指定することもできます。

--- a/app/javascript/components/index.tsx
+++ b/app/javascript/components/index.tsx
@@ -1,6 +1,7 @@
 import { BrowserRouter, Link, Switch, Route } from "react-router-dom";
 import PageNotFound from "./PageNotFound";
 import { Home } from "./static-pages";
+import UserProfle from "./user-profiles/UserProfile";

 const App = () => {
   return (
@@ -9,6 +10,9 @@ const App = () => {
         <Route exact path="/">
           <Home></Home>
         </Route>
+        <Route path="/user_profiles/:id">
+          <UserProfle></UserProfle>
+        </Route>
         <Route path="*">
           <PageNotFound></PageNotFound>
         </Route>

パラメータの具体的な値は useParams フックを使って取得できます:

// app/javascript/components/user-profiles/UserProfile.tsx

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

interface Params {
  id: string;
}

const UserProfle = () => {
  const { id } = useParams<Params>();

  return <>User id: {id}</>;
};

export default UserProfle;

GravatarImage<a href={`/users/${props.user.id}`}> タグを <Link to={`/user_profiles/${props.user.id}`}> に変えて、 Gravatar の画像をクリックしたら画面遷移ができること、かつパスの :id 部分の値に応じた表示になっていることを確かめてみてください。

練習問題 2

できる範囲で構わないので、 Rails が /users/:id で表示していたのと同等の画面を UserProfile.tsx で作ってください。

次回予告

いよいよ次が最後の章になりました。これまで開発してきた SPA on Rails を本番環境へとデプロイし、実際に動くことを確認します。