Reactで複数のページを作る方法 – Router v6.0準拠

  • 2022年12月20日
  • React

Reactは、SPA(Single Page Application)に最適化されていますが、Reactでも “React Router”ライブラリーを使うことで、下記のようなApp.jsの内部に複数のページ(path)を持つWebサイトを作ることができます。Routerは2021年の10月頃にver 6.0.0の正式版がリリースされ、それ以前のver 5とは大きく変わりました。本稿の記載内容は全てver6.0に準拠しています。

Router

react-router-domのインストール

npmコマンドを使ってreact-router-domをインストールします。バージョンを指定する場合は下段のようにします。

npm install react-router-dom
npm install raect-router-dom@6

React Routerを使用する場所は、通常は最上位のコンポーネントのApp.jsですので、続いてApp.jsにてReact Routerライブラリーをインポートします。Reactそのものとcssのインストールの記載は省略しています。v6からは<switch>の代わりに<Routes>を使います。App.jsで<Link>を使うことはあまりないと思いますので、Linkのインポートは多くの場合App.jsでは不要です。

App.js

import {
BrowserRouter as Router,
Routes,
Route,
} from "react-router-dom";

BrowserRouterとは

BrowserRouterではHTMLのhistory APIを用いて、ユーザー側の表示とURLを一致させます。他方HashRouterはHTMLのハッシュ#を用いてルーティング処理をし、URLのパスにも#が混じります(両者の詳細についてはstackoverflowで議論を参照)。下記のようにBrowserRouterをas RouterとしてインポートするのはHashRouterへの切り替えを容易にするためです。ただ多くの場合はhistory APIを用いますので、そのままインポートしても問題ありません(公式ドキュメント及び本稿のトップ画像の例)。

React Routerのコード本体(Router)

App.jsの関数部分は、次のように記載します。<Router>と内側の<Routes>で全体を包んだ後、個別のページを記載したコンポーネントをelement={ }の波括弧の内側に記載します。この例では、<Home/>、<About/>、<Products/>の3つのコンポーネントがあります。<Route>のpath属性にはそれぞれのページのpath(URL)を記載します。

ここでは<Home/>コンポーネントにrootの”/”を割り当て、<About/>コンポーネントに”/about”を、<Products/>コンポーネントに”/products”のパスを割り当てています。3つのコンポーネントは別ファイルにて、Home.js、About.js、Products.jsとして設け、ここでは記載を省略していますが、それぞれをApp.jsの上段でインポートします(通常のReactの構成)。

App.js

function App() {
return (
< div className="app">
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About/>} />
<Route path="/products" element={<Products/>} />
<Route path="/*" element={<NotFound/>} />
</Routes>
</Router>
</ div>
);
}

カスタム404エラーページの追加方法

上記に記載の4つ目にコンポーネントは、カスタムの404エラーページです。オールキャッチのパス、”/*”を割り当てることで、存在しないパスにアクセスがあった場合は<NotFound/>コンポーネントが表示されます。

Routeに複数のコンポーネントを持たせる方法

始めから複数ページを持たせる予定であれば、上記のような構成にすると思いますが、場合によってはSPAとしてApp.js上に複数のコンポーネントを直接設置していたところに、後からページを追加したいということがあると思います。その場合は、新たにルートパス”/”を割り当てる中間の<Home/>等のコンポーネントを作らずとも、下記のように、Routeコンポーネントに直接複数のコンポーネント(ここでは<Header/>と<Main/>)を持たせることができます。複数のコンポーネントを空のタグで括っている点がポイントです。

App.js

function App() {
return (
< div className="app">
<Router>
<Routes>
<Route path="/" element={<
<>
<Header/>
<Main/>
</>
}/>
<Route path="/about" element={<About/>} />
<Route path="/products" element={<Products/>} />
<Route path="/*" element={<NotFound/>} />
</Routes>
</Router>
</ div>
);
}

ページ毎に共通のNavbarやFooterを設定する方法

これまでの方法に従うと、NavbarやFooterはどのページにもインポートが必要になります。<Router>と<Routes>の間には、<div>タグや他のコンポーネントが入っても問題なく動きますので、上部の<Router>と<Routes>の間にNavbarコンポーネントを、下段の<Router>と<Routes>の間にFooterコンポーネントを挟むことで、どのページでも上段と下段にそれぞれNavbarとFooterを表示させることもできます。この場合、これまで説明した例では<Router>の外側にあったdivタグを<Router>のすぐ内側に配置し、<Routes>のすぐ外側を<div>タグあるいは<main>タグで括ることで、Router内側の<div>タグの直下の子要素に、<Navbar/><main></main><Footer/>を3つ並べることができます(スタイリングがしやすい)。なお、<Routes>と<Route>の間には<div>タグ他を配置できません。下記は、以上を適用した事例です。クラス名は全てTailwindです(本稿とは関係ないのであまり気にしないでください)。



exact属性の役割

こちらのexact記載はv5の記載ですので、v6を使われている方は飛ばし、次のLinkに飛んでください。v6では前方部分一致によるレンダリングがなくなったのでexact不要になりました。

<Route>の内部で、exactという属性(props)を渡しています。exactを指定しないとURLとpathの前方の部分一致でレンダリングしますが、exactを渡すことで、URLと完全に一致するpathの内容をレンダリングをします。

具体的にこの例でexactを指定しない場合、”/about”と”/products”のURLをリクエストした場合でも、それぞれのURLがrootのpathである”/”と前方が部分一致していることから<Home/>コンポーネントが表示され、<About/>コンポーネントと<Products/>コンポーネントは返ってきません。

ただし正確には、<Switch>は指定されたURLを上から順にソートするので、Homeコンポーネントの部分を3番目に配置することで、この例ではexactなしでも動きます(下記)。公式ドキュメントの一番初めに記載された事例はこの方法です。また、exactを全てに記述せずともrootのpathにだけを渡せば、この例では動きます。

リンクを張る方法(Link)

<Routes>の内部で固有のpathを持つ3つのコンポーネントを設置しましたので、続いて互いのコンポーネント間で内部リンクを張ります。内部リンクはもちろん<a>タグでも張れるのですが、React Routerに備わる<Link>を使用する方がずっと高速です。<a>タグがリフレッシュを伴って、サーバから別のページを読み込むのに対して、<Link>はクライアント側でページ遷移の処理をします。また<a>タグでは、UseStateやRecoilで保持される状態もリフレッシュに伴い失われますが、<Link>では失われません。

公式ドキュメント及び多くのReact Routerを紹介するブログでは、<Link>を含む<nav>タグを、App.jsで<Routes>の上部に並列に記載していてわかりにくいのですが、<Link>は実質的にはただの<a>タグですので、どこにでも設置できます。

下記コードはサブコンポーネントのHome.jsにリンクを設置する例です。2行目でReract-router-domからLinkをインポートし、Home関数の内部で<Link>タグを使用して内部リンクを張っています。


Home.js

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

function Home() {
return (
< div className="home">
<Link to="/products">Link to Products</Link>
<Link to="/about">Link to About</Link>
</ div>
);
}

export default Home;

本事例のようにテキストではなく、画像(imgタグ)、ボタン(buttonタグ)、その他アイコンにリンクを持たせたいときは、Linkの開始タグと終了タグの間に対象のタグを配置します。

以降は、React Routerに関する応用のトピックです。初めてReact Routerを使う方、単に複数のページを設定したい方にはやや難しい内容ですので、読み飛ばして頂ければと思います。

応用編1 – パラメーター

クエリパラメーターの設定方法

React RouterのLinkを使うと、リンクにクエリパラメーターを持たせることも容易です。下記の例では、dataのページに対しするリクエストで、変数名queryに変数paramというクエリパラメーターを持たせた例です。pathnameにはパスを、searchにはクエリパラメーターを指定して、全体をオブジェクトとしてtoに渡します。ULRにすると、/data?query=paramとなります。

<Link
to={{
pathname: "/data",
search: "?query=param",
}}
>

パスパラメーターの設定方法

パスパラメーターの設定は<Route>にします。具体的にはRouteのpathの部分にコロンを付けてパスパラメーターの変数名を付します。下記のはpostというパスにidという変数名でパスパラメーターを付して、Postコンポーネントを読み込む例です。実際のURLの例は例えば/post/10(idが10の場合)や/post/james(idがjamesの場合)のようになり、コロン(:)は付きません。よって前述のLinkを通じて<Route>で設定したパスにパスパラメターを渡すときは、<Link to=”/post/10″>(idが10の場合)、または、<Link to=”/post/james”>(idがjamesの場合)のようにします。

<Route path="/post/:id" element={<Post/>} />

パスパラメーターを読む方法 – UseParams

パスパラメーターは通常バックエンドで処理するので、フロントエンドのReactで読むことはあまりないかもしれませんが、react-router-domのuseParams関数を使うと処理が可能です。useParamsで取得した内容を変数に格納し(下記の例ではparams)、具体的なパスパラメーターはparams.変数名(下記の例ではparams.id)で得ることが可能です。本事例では、パスパラメーターの内容を表示させているだけですが、実際はここでパスパラメーターを使って、バックエンドや外部APIにリクエストをすることが多いと思います。

Post.js

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

function Post() {
const params = useParams();
return (
< div>
Post {params.id}
</ div>
);
}

export default Post;

応用編2 – リダイレクトの方法

Navigate, useNavigate

React Routerではリダイレクトの設定も容易です。リダイレクトは、例えばバックエンドあるいは外部APIへのリクエストに対して404エラーを得たら404ページにリダイレクトさせる場合や、Postリクエスト等なんらかの処理を終えた後に、他のページにユーザーを戻すときなどで使えます。下記はHTTPステータスコードを処理する例です。実際はcatchされるerrorの内容を読むことになりますが、ここでは単に変数statusにコードを格納しています。if文でステータスコードが404のときに、/notfoundページへリダイレクトしています。HTTPステータスコードが404でないときは、JSXの内容(本件ではPostという文字が表示されるだけ)が表示されます。

Post.js

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

function Post() {
const status = 404;

if (status === 404) {
return <Navigate to="/notfound" />;
}

return (
< div>
Post
</ div>
);
}

export default Post;

次の例はなんらかの処理の後にリダイレクトする例です。ボタンに対するonClickで処理(本件ではconsole.log)とリダイレクトが生じます。リダイレクトには、useNavigate関数からの戻り値を格納した変数(ここではnavigate)を使用します。ただuseNavigateはコンポーネントの内部でしか使えないので、Context等コンポーネントの外でリダイレクトを定める場合はwindow.location(“/about”)を使うとリダイレクトできます(リフレッシュは生じる)。

Post.js

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

function Post() {
const navigate = useNavigate();
const onClick = () => {
console.log("Executed");
navigate("/about");
};

return (
< div>
<h1>Post</h1>
<button onClick={onClick}>CLick</button>
</ div>
);
}

export default Post;

本稿ではReactで複数のページを作る方法をご紹介しました。Router ver6.0には、他にも入れ子構造に対応しているなどの特徴があります。ここまで読んで頂いた方は入れ子構造も容易に理解できると思います。詳細は公式ドキュメントをご覧ください。

応用編3 – useLocation

useLocationを使用すると、現在表示されているページのパスを取得できます。前述の例では、”/about”のページにいるときは”/about”が取得でき、”/products”のページにいるときは”/products”が取得できます。この機能を使うと、ヘッダー、フッター、Navbarなど各ページに共通するコンポーネント内の要素(例:色)を、ページ毎に動的に変化させることができます。Javascriptには現在のルートパスを取得するlocationというクラスが元々備わっていますが、Reactのコンポーネント内ではlocationをそのまま使えないので、react-router-domのuseLocationを使用します。例えばNavbarコンポーネントがあるとして、次のような使い方ができます。

Navbar.js

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

function Navbar() {
const location = useLocation();

const isRoute = (route) => {
if (route == location.pathname) {
return true;
};

//中略
}

export default Navbar;

isRoute関数は、引数として渡されたパスが、現在のページのパスと等しければ、trueを返します。よって、例えばページ毎にクラス名を変更したい場合は、次のような三項演算子をHTMLタグの内部で組みます。

//"/about"のページに対して動的に変化させたいタグのクラス
className={
isRoute("/about")
? "navbarActive"
: "navbarDefault"
}

//"/products"のページに対して動的にさせたいタグのクラス
className={
isRoute("/products")
? "navbarActive"
: "navbarDefault"
}

他にも、共通の1つのコンポーネントを複数の別のページで使用するときに、ページ(のパス)に応じて共通するコンポーネントの表示内容を変える目的でuselocationを使えます。下記はjsxを返すreturnの内部で三項演算子を用いて、pathnameが”about”なら<p>タグでaboutと表示し、それ以外の場合は<p>タグで”products”と表示する例です。

<p>{location.pathname === "/about" ? "About" : "Products"}</p&gt

応用編4 – Protected (Private) Route

Protected Routeは特定のページを非公開とする機能です。例えば、ログイン済みのユーザーのみが見ることのできるページを設定するときに使われます。Private Routeとも呼ばれます。例えば本稿事例のProducts Route(コンポーネント)をProtected Routeとする場合、Protected RouteでProduts Routeを包みます。内側のもとのProductsは何も変わらずそのままです。


Private/Protected Route

App.js

具体的なApp.jsの組み方は次の通りです。

App.js

function App() {
return (
< div className="app">
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About/>} />
<Route path="/products" element={<ProtectedRoute/>} >
<Route path="/products" element={<Products/>} />
</Route>
<Route path="/*" element={<NotFound/>} />
</Routes>
</Router>
</ div>
);
}

Protected Routeの部分を下記に抜粋します。内側のProdutsはこれまで本稿で紹介したものと同一です。外側のProtected Routeのポイントは3点です。(1)Routeタグは通常のRouteのようにセルフクロージングとせず、一般のタグと同様に開始タグと終了タグに分けます。(2)pathは保護する対象のRoute(本件の場合はProducts)と同じpathにします。(3)参照するコンポーネント(element)は、別途作成するProtectedRouteコンポーネントとします。ProtectedRouteはjsxコンポーネントとして作成の上、記載は省略していますがApp.jsの上部で他のコンポーネント同様にインポートします。

<Route path="/products" element={<ProtectedRoute/>} >
<Route path="/products" element={<Products/>} />
</Route>

ProtectedRouteコンポーネント

ProtectedRouteコンポーネントは、最小の構成では次のようになります。

ProtectedRoute.jsx

import { Navigate, Outlet } from "react-router-dom";

function ProtectedRoute() {
const user = True;
return user ? <Outlet/> : <Navigate to="/" />;
}

export default ProtectedRoute;

import

1行目でProtected (Private) Routeに関連するライブラリーをインポートします。Navigateはリダイレクトに使うraect-router-domのコンポーネントです。Ouletはapp.jsで内側に包んだ子コンポーネントを呼び出すために使用します。本事例の場合は、Product コンポーネントが返されます。

ProtectedRoute関数

本事例ではuserにTrueを格納していますが、実際は後述の例のように、ここでなんらかの認証状態を保持・記憶する関数を使用します。returnの部分は三項演算子(if文)を使用し、userが存在する場合(Trueの場合)は小コンポーネントの<Outlet/>、すなわち<Product/>を返します。他方でuserが存在しない場合(Falkseの場合)は、ホーム”/”へリダイレクトします。本事例ではuserがtrueで固定されているで、常に<Product/>が表示されます。

Firebase Authでの実装例

下記はfirebaseのauthenticationを使用し、ユーザーのログイン状態に応じて表示内容を変える例です。

先ほどの最小構成事例でuser = Trueとした部分では、useAuthState()関数を呼び出し、ユーザーの状態を保持するuserと、ロードの状態を保持するloading、ログイン時のエラーの状態を保持するerrorの3つの変数を設定します。引数にはfirebaseの認証オブジェクトauthを渡します。

firebaseサーバーからの応答を待つ間はloadingがTrueになるので、単にh3タグでLoading…と表示します。loadingがFalseの場合は、先ほども説明した通り、userの存在・不存在に応じたそれぞれの内容を返します(三項演算子)。

ProtectedRoute.jsx

import { Navigate, Outlet } from "react-router-dom";
import { useAuthState } from "react-firebase-hooks/auth";
import { getAuth } from "firebase/auth";

function ProtectedRoute() {
const auth = getAuth();
const [user, loading, error] = useAuthState(auth);
if (loading) {
return <h3>Loading...</h3>;
}
return user ? <Outlet/> : <Navigate to="/" />;
}

export default ProtectedRoute;

Firebaseのauthentication(認証)について詳しく知りたい方は、私のこちらの記事「メールとパスワードによるユーザー認証の実装方法 – Firebase + React」をご覧ください。

React Router ver6.0が学べる講座

本稿を読んで頂ければ、React Router ver6.0は問題なくお使い頂けると思いますが、動画の講座で学びたい方はUdemyのこちらの講座「React Front To Back 2022」をお勧めいたします(2022年5月現在)。この講座は本稿と同様に、元々はReact Router ver5.0を用いて解説していましたが、ver6.0へのアップグレード後すぐに動画を更新・追加していました。Router以外にも、Reactの基本からfetchを使ったリクエスト、JSON server、Tailwind、React Context、Redux、Firebase、Heroku、MERNスタックなど、基本から応用、フロントエンドからバックエンドまで2022年現在では最新で魅力的なReactのテクノロジーを学ぶことができます(※但し、私はReact ContextとReduxについてはRecoilを選ぶ点については私のこちらの記事を参照)。講義は英語(米国)ですが、講師のBradさん非常にゆっくりと話しますので聞きとりやすいと思います。価格は12,000-13,800円で受講期間の制限なく、何十万円もするようなプログラミングスクールよりずっと安い上、Udemyは時々セールをします。先ほどのリンクをクリックし定価13,800円が表示されても、お急ぎでない場合は、1-3週間置いて改めてクリックしてみてください。


React Routerを日本語で学べる講座

英語で学ぶのが辛い方は、Udemyのこちらの講座「Reactに入門した人のためのもっとReactが楽しくなるステップアップコース完全版」をお勧めいたします。動画でのRouterのバージョンを5.0から6.0に更新していない点は非常に残念なのですが(2022年5月現在)、講師の岡田さんの説明はわかりやすいです。Reactの応用編という位置付けで、useMemo、useCallbackなどのレンダリング最適化、グローバル状態管理ではContextとRecoil、カスタムフック、ChakuraUI、それにTypeScriptの入門まで大変分かりやすく説明されております。


今日も最後まで読んで頂きありがとうございました。