概要
NMIXXというKPOPグループが好きで、React SPAでアプリケーションを作成したので、
備忘録として、ReactにおけるSPAアプリケーションの概要と、ルーティング(React Router)についてまとめました。
React Routerにおけるルーティング
シングルページアプリケーション (SPA) では、
初回のリクエストでサーバーから必要なリソースをすべて取得し、2回目以降の通信はDOMの更新とHistoryの置換で擬似的なページ遷移を行います。
メリット
- ページ遷移が高速化
 - フロントエンドとバックエンドの分業(独立したアプリケーション)
 
デメリット
- 初期ページの読み込みが遅い
 - SEOにて不利になる可能性
 
Reactでは、ルーティングをコンポーネント単位で管理することが一般的です。
History API
SPAのブラウザの履歴管理には History API を使用します。
History API の pushState や replaceState メソッドを使用すると、サーバーにリクエストを送らずに、ブラウザのアドレスバーの表示、履歴を更新することができます。
- 
pushState(): 新しい履歴エントリーを追加 - 
replaceState(): 現在の履歴エントリーを置換 
以下のgifを見て分かるように、サーバーへのリクエストは発生していませんが、戻るボタンによってページのコントロールが可能となっています。
SPAでのGA4(Google Analytics 4)
SPAでは、サーバー側からどのページが表示されているかを把握することができないため、GA4などのトラッキングの計測は、useEffectなどでクライアントサイドから行う必要があります。
useEffect(() => {
    const pagePath = location.pathname + location.search;
    gtag('config', 'GA_MEASUREMENT_ID', {
      page_path: pagePath,
    });
}, [location.key]);
※ locationオブジェクトは、後述のReact Router Hooks APIで解説しています。
ページの管理
SPA では、サーバー側でページごとの状態を管理しないため、削除したページが検索エンジンにインデックスされたままになることがあります。
これを防ぐためには、サイトマップやGoogle Analytics 4(GA4)の管理機能を活用して、不要なページを効率的に削除し、管理することが重要です。
React Router
これまで説明したルーティングをどのように実現するかというと、ルーティングライブラリを使用します。
Reactアプリケーションにおけるルーティングライブラリとして、デファクトスタンダードとなっているのがReact Routerです。
以降はReact Routerの主要コンポーネントについて、まとめてご紹介します。
BrowserRouter コンポーネント
<BrowserRouter>コンポーネントを使用することで、Reactアプリケーションにルーティング機能を追加できます。
React Routerを使用する場合は、<BrowserRouter>コンポーネントで、ルーティング対象とするコンポーネントをラップします。
// App.tsx
function App() {
  return (
    <>
      <BrowserRouter>
        <Header />
        <Sidebar />
        <Routes>
          <Route path="/" element={<MainContent />} />
          <Route path="component1" element={<MainContent selectedComponent="component1" />} />
          <Route path="component2" element={<MainContent selectedComponent="component2" />} />
        </Routes>
      </BrowserRouter>
    </>
  );
}
RoutesおよびRoute コンポーネント
<Routes>コンポーネントと<Route>コンポーネントを使用することで、ルーティングと対応するコンポーネントを定義できます。
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const App = () => (
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/dashboard" element={<Dashboard />}>
        <Route path="settings" element={<Settings />} />
        <Route path="profile" element={<Profile />} />
      </Route>
    </Routes>
  </BrowserRouter>
);
ルーティングパスと対応するコンポーネント
| ルーティングパス | 対応するコンポーネント | 
|---|---|
/ | 
<Home /> | 
/about | 
<About /> | 
/dashboard | 
<Dashboard /> | 
/dashboard/settings | 
<Settings /> | 
/dashboard/profile | 
<Profile /> | 
ネストしたルーティング
<Route>コンポーネントを入れ子にした場合、ネストされたルーティングを実現できます。
親ルートがレンダリングされ、その中の  コンポーネントが、子ルートを適切にレンダリングします。
ルーティングの注意点
- すべてのルーティングが評価された上で、最も具体的なパスがマッチする
 - 末尾に 
/をつけても無視される - ワイルドカードの 
*のみ使用可能だが、記述できるのは末尾のみ - 大文字・小文字を区別したい場合は
caseSensitive属性を指定する - ネストされた
Routeコンポーネントのpathプレフィックスには/をつけてはいけない 
404エラーの表示
* キーワードを利用することで、
パスがマッチしない場合、404エラーページを表示することができます。
const App = () => (
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  </BrowserRouter>
);
export default App;
このようにすることで、どのパスにもマッチしない場合に NotFoundコンポーネントがレンダリングされます。
Navigateコンポーネント
<Navigate>コンポーネントを使用すると、リダイレクトを発生することが可能です。
- 
to: リダイレクト先のパス - 
replace: 履歴の置換(指定した場合はリダイレクト前のページが履歴から削除) 
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
const App = () => (
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/login" element={<Login />} />
      <Route path="*" element={<Navigate to="/login" replace />} />
    </Routes>
  </BrowserRouter>
);
export default App;
この例ではどのルーティングにもマッチしない場合、/loginにリダイレクトされます。
Linkコンポーネント
<Link>コンポーネントは、標準の<a>タグと異なり、ページをリロードせずにURLを変更します。
サーバーとの通信を発生させずに、ページ遷移とDOMの更新を行うことができます。
to属性
リンク先のパスを文字列で指定することができます。
<Link to="/">Home</Link>
オブジェクト指定した場合、パスに加えて、クエリパラメータ、ハッシュ、ユーザーには公開されない状態を指定できます。
<Link
  to={{
    pathname: "/settings",
    search: "?mode=dark",
    hash: "#preferences",
    state: { fromSettings: true }
  }}
>
NavLinkコンポーネント
<NavLink> は、 <Link> を拡張したコンポーネントで、現在アクセスしているページを装飾することが可能です。
style と className属性を動的に指定できます。
<NavLink
  to="/"
  style={({ isActive }) => ({
    color: isActive ? 'red' : 'blue',
    textDecoration: isActive ? 'underline' : 'none',
  })}
>
<NavLink
  to="/"
  exact
  className={({ isActive }) => (isActive ? 'active' : 'inactive')}
>
isActiveは、現ページがリンク先と一致する場合に true を取ります。
Outletコンポーネント
<Outlet>コンポーネントは、親ルートがレンダリングされる際に、対応する子ルートのコンポーネントを表示するために使用されます。
親コンポーネント内に<Outlet>を配置することで、指定された子ルートのコンテンツがその位置に挿入されます。
ルーティング
<Routes>
  <Route path="/" element={<ParentComponent />}>
    <Route path="child" element={<Child />} />
    <Route path="child_second" element={<ChildSecond />} />
  </Route>
</Routes>
親コンポーネント
const ParentComponent = () => {
  return (
    <div>
      <h1>Parent Component</h1>
      <Outlet />
    </div>
  );
};
export default ParentComponent;
パスとレンダリングされる対象のまとめ
| パス | レンダリングされる対象 | 
|---|---|
/ | 
ParentComponent( <Outlet>には何もレンダリングされない ) | 
/child | 
ParentComponent + Child
 | 
/child_second | 
ParentComponent + ChildSecond
 | 
React Router Hooks API
React Routerは、フックを提供しています。
フック処理を使用することで、ルーティング関連の操作を簡単に行うことができます。
React Router HooksであるuseLocation、useSearchParams、useNavigate、useParams、useMatchについて説明します。
useLocation
useLocationは、現在のページに関する情報を取得します。
URLやクエリパラメータなどの情報にアクセスできます。
const location = useLocation();
locationオブジェクトのデータ型
useLocationが返すlocationオブジェクトは、以下のプロパティを保持します。
- 
pathname: 現在のURLパス - 
search: クエリパラメータの文字列 - 
hash: ハッシュ文字列 - 
state:Linkやnavigate関数を使用して渡された状態オブジェクト(ユーザーには隠蔽) - 
key: React Routerが内部で使用する一意のキー 
location.key + useEffectによるGA4へのpage_view送信
location.keyとuseEffectを組み合わせて、GA4 へのページビューを送信できます。
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { logEvent } from 'firebase/analytics';
const MyComponent = ({ analytics }) => {
  const location = useLocation();
  useEffect(() => {
    logEvent(analytics, 'page_view', {
      page_path: location.pathname,
      page_location: window.location.href,
      page_title: document.title
    });
  }, [location.key]);
  return <div>現在のパス: {location.pathname}</div>;
};
useSearchParams
useSearchParamsは、クエリパラメータを操作するために使用されます。
クエリパラメータの取得や更新を行うことが可能です。
const [searchParams, setSearchParams] = useSearchParams();
- searchParams: クエリパラメータを格納したオブジェクト
 - setSearchParams: クエリパラメータの更新関数
 
クエリパラメータの取得
const MyComponent = () => {
  const [searchParams] = useSearchParams();
  const paramValue = searchParams.get('paramKey');
  return <div>Param Value: {paramValue}</div>;
};
クエリパラメータの更新
const MyComponent = () => {
  const [searchParams, setSearchParams] = useSearchParams();
  const updateParams = () => {
    setSearchParams({ paramKey: 'newValue' });
  };
  return (
    <div>
      <button onClick={updateParams}>Update Param</button>
    </div>
  );
};
useNavigate
useNavigateフックは、リダイレクトを行うために使用します。
const navigate = useNavigate();
リダイレクト時は、遷移先のパスやオプションを引数に指定します。
navigate('/path', { replace: true, state: { fromDashboard: true } });
- 
path: 遷移先のURL - 
replace:trueを指定すると、履歴スタックの現在のエントリを置換 - 
state: ナビゲーション時に渡す状態 
数値を指定した場合
useNavigateに数値を指定すると、その数値分だけ履歴スタックを移動します。
例えば、navigate(-1)は1つ前のページに戻ります。
const MyComponent = () => {
  const navigate = useNavigate();
  const goBack = () => {
    navigate(-1);
  };
  return <button onClick={goBack}>Go Back</button>;
};
<Navigate>コンポーネントとの実行タイミングの違い
コンポーネントはレンダー時にナビゲーションを行いますが、
useNavigateフックは実行のタイミングでナビゲーションを行います。
useParams
useParamsフックは、現在のルートのパスパラメータを取得するために使用します。
- 例) 
members/:member_id
GET:members/1234=>1234 
戻り値
useParamsは、パラメータ名をキーとするオブジェクトを返します。
import { useParams } from 'react-router-dom';
const MyComponent = () => {
  const { member_id } = useParams();
  return <div>パラメータID: {member_id}</div>;
};
useMatch
useMatchフックは、特定のパスが現在のURLと一致するかどうかを確認するために使用します。
useParamsとの使い分け
- 
useParams: 現在のルートに基づいたパラメータを取得する場合に使用 - 
useMatch: 特定のパスに対して一致するかどうかを確認する場合に使用 
import { useMatch } from 'react-router-dom';
const MyComponent = () => {
  const match = useMatch('/path/:id');
  if (match) {
    return <div>マッチングID: {match.params.id}</div>;
  }
  return <div>パスが一致しませんでした。</div>;
};
React Helmet
SPAでは、SEOの観点でページごとにタイトルやメタデータを管理する必要があります。
React Helmetライブラリを用いることで<head>要素を動的に制御することが可能です。
 使い方
import React from 'react';
import { Helmet } from 'react-helmet';
const SamplePage: React.FC = () => {
  return (
    <div>
      <Helmet>
        <title>マイページ</title>
      </Helmet>
    </div>
  );
};
export default MyPage;
まとめ
以上です!
React Router の基本はマスターできたかと思います!
気になる点や、ご意見あればコメントお願いします。



