[React / React Router] SPAを作成するために知っておくべき基本事項

## 概要
NMIXXというKPOPグループが好きで、React SPAでアプリケーションを作成したので、
備忘録として、ReactにおけるSPAアプリケーションの概要と、ルーティング（`React Router`）についてまとめました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842674/e7e205aa-2161-569a-4b89-bea129a8846d.png)

https://github.com/PenPeen/react_spa_nmixx_Introduction

## React Routerにおけるルーティング
シングルページアプリケーション (SPA) では、
初回のリクエストでサーバーから必要なリソースをすべて取得し、2回目以降の通信はDOMの更新とHistoryの置換で擬似的なページ遷移を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842674/a238098f-2914-a313-4421-44452c84ed41.png)

**メリット**
- ページ遷移が高速化
- フロントエンドとバックエンドの分業（独立したアプリケーション）

**デメリット**
- 初期ページの読み込みが遅い
- SEOにて不利になる可能性

Reactでは、ルーティングをコンポーネント単位で管理することが一般的です。

## History API
SPAのブラウザの履歴管理には **History API** を使用します。

History API の `pushState` や `replaceState` メソッドを使用すると、サーバーにリクエストを送らずに、ブラウザのアドレスバーの表示、履歴を更新することができます。
<br>
- `pushState()`:    新しい履歴エントリーを追加
- `replaceState()`: 現在の履歴エントリーを置換

以下のgifを見て分かるように、サーバーへのリクエストは発生していませんが、戻るボタンによってページのコントロールが可能となっています。

![タイトルなしaaaa.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842674/b4223c2e-6d3f-e092-da34-77a7f482daec.gif)

## SPAでのGA4（Google Analytics 4）
SPAでは、サーバー側からどのページが表示されているかを把握することができないため、GA4などのトラッキングの計測は、`useEffect`などでクライアントサイドから行う必要があります。

```tsx
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>`コンポーネントで、ルーティング対象とするコンポーネントをラップします。

```tsx
// 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>`コンポーネントを使用することで、ルーティングと対応するコンポーネントを定義できます。

```tsx
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>`コンポーネントを入れ子にした場合、ネストされたルーティングを実現できます。
親ルートがレンダリングされ、その中の <Outlet> コンポーネントが、子ルートを適切にレンダリングします。

#### ルーティングの注意点
- すべてのルーティングが評価された上で、最も具体的なパスがマッチする
- 末尾に `/` をつけても無視される
- ワイルドカードの `*` のみ使用可能だが、記述できるのは末尾のみ
- 大文字・小文字を区別したい場合は`caseSensitive`属性を指定する
- ネストされた`Route`コンポーネントの`path`プレフィックスには`/`をつけてはいけない

### 404エラーの表示
`*` キーワードを利用することで、
パスがマッチしない場合、404エラーページを表示することができます。

```tsx
const App = () => (
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  </BrowserRouter>
);

export default App;
```

このようにすることで、どのパスにもマッチしない場合に `NotFound`コンポーネントがレンダリングされます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842674/6b90463b-fd1b-1367-1881-ddf784cc30a3.png)


### Navigateコンポーネント
`<Navigate>`コンポーネントを使用すると、リダイレクトを発生することが可能です。
- `to`: リダイレクト先のパス
- `replace`: 履歴の置換（指定した場合はリダイレクト前のページが履歴から削除）

```tsx
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属性
リンク先のパスを文字列で指定することができます。
```tsx
<Link to="/">Home</Link>
```

オブジェクト指定した場合、パスに加えて、クエリパラメータ、ハッシュ、ユーザーには公開されない状態を指定できます。
```tsx
<Link
  to={{
    pathname: "/settings",
    search: "?mode=dark",
    hash: "#preferences",
    state: { fromSettings: true }
  }}
>
```

---

### NavLinkコンポーネント
`<NavLink>` は、 `<Link>` を拡張したコンポーネントで、現在アクセスしているページを装飾することが可能です。
`style` と `className`属性を動的に指定できます。


```tsx
<NavLink
  to="/"
  style={({ isActive }) => ({
    color: isActive ? 'red' : 'blue',
    textDecoration: isActive ? 'underline' : 'none',
  })}
>
```

```tsx
<NavLink
  to="/"
  exact
  className={({ isActive }) => (isActive ? 'active' : 'inactive')}
>
```

 `isActive`は、現ページがリンク先と一致する場合に `true` を取ります。

 ---

### Outletコンポーネント
`<Outlet>`コンポーネントは、親ルートがレンダリングされる際に、対応する子ルートのコンポーネントを表示するために使用されます。

親コンポーネント内に`<Outlet>`を配置することで、指定された子ルートのコンテンツがその位置に挿入されます。

#### ルーティング

```tsx
<Routes>
  <Route path="/" element={<ParentComponent />}>
    <Route path="child" element={<Child />} />
    <Route path="child_second" element={<ChildSecond />} />
  </Route>
</Routes>
```

#### 親コンポーネント
```tsx
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やクエリパラメータなどの情報にアクセスできます。

```tsx
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` へのページビューを送信できます。

```jsx
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`は、クエリパラメータを操作するために使用されます。
クエリパラメータの取得や更新を行うことが可能です。

```tsx
const [searchParams, setSearchParams] = useSearchParams();
```

- searchParams: クエリパラメータを格納したオブジェクト
- setSearchParams: クエリパラメータの更新関数

#### クエリパラメータの取得
```tsx
const MyComponent = () => {
  const [searchParams] = useSearchParams();
  const paramValue = searchParams.get('paramKey');

  return <div>Param Value: {paramValue}</div>;
};
```

#### クエリパラメータの更新
```tsx
const MyComponent = () => {
  const [searchParams, setSearchParams] = useSearchParams();

  const updateParams = () => {
    setSearchParams({ paramKey: 'newValue' });
  };

  return (
    <div>
      <button onClick={updateParams}>Update Param</button>
    </div>
  );
};
```

---

### useNavigate
`useNavigate`フックは、リダイレクトを行うために使用します。

```tsx
const navigate = useNavigate();
```

リダイレクト時は、遷移先のパスやオプションを引数に指定します。

```tsx
navigate('/path', { replace: true, state: { fromDashboard: true } });
```

- `path`: 遷移先のURL
- `replace`: `true`を指定すると、履歴スタックの現在のエントリを置換
- `state`: ナビゲーション時に渡す状態

#### 数値を指定した場合
`useNavigate`に数値を指定すると、その数値分だけ履歴スタックを移動します。
例えば、`navigate(-1)`は1つ前のページに戻ります。

```tsx
const MyComponent = () => {
  const navigate = useNavigate();

  const goBack = () => {
    navigate(-1);
  };

  return <button onClick={goBack}>Go Back</button>;
};
```

#### `<Navigate>`コンポーネントとの実行タイミングの違い
<Navigate>コンポーネントはレンダー時にナビゲーションを行いますが、
`useNavigate`フックは実行のタイミングでナビゲーションを行います。

---

### useParams
`useParams`フックは、現在のルートのパスパラメータを取得するために使用します。

- 例）　`members/:member_id`
GET: `members/1234` => `1234`

#### 戻り値
`useParams`は、パラメータ名をキーとするオブジェクトを返します。

```jsx
import { useParams } from 'react-router-dom';

const MyComponent = () => {
  const { member_id } = useParams();

  return <div>パラメータID: {member_id}</div>;
};
```

---

### useMatch
`useMatch`フックは、特定のパスが現在のURLと一致するかどうかを確認するために使用します。

#### useParamsとの使い分け
- `useParams`: 現在のルートに基づいたパラメータを取得する場合に使用
- `useMatch`: 特定のパスに対して一致するかどうかを確認する場合に使用

```tsx
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>`要素を動的に制御することが可能です。

### :memo: 使い方

```tsx
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 の基本はマスターできたかと思います！

気になる点や、ご意見あればコメントお願いします。