トップページへ

S.A.G.A佐賀!!出身者のプログラミングブログ

React Routerを使用したルーティングの基本

React Routerを使用したルーティングの基本

Reactのメジャーなルーティングライブラリで、フレームワークRemixの元にもなっているReact Routerの使い方を見ていきます。 機能が豊富なため、すべての機能を取り扱うことができません。 本記事では基本的な機能のみ取り扱うこととします。

ルーティングとは

まずはReact Routerの使用方法の前にアプリケーションのルーティングについておさらいしたいと思います。 サーバーサイドアプリケーションとReactなどのSPA(シングルページアプリケーション)ではルーティングの定義が違います。 PHPなどを用いた従来のサーバーサイドアプリケーションの場合、リクエストされたURLに対して紐づくページ内容をサーバーがクライアントに返却します。 しかしSPAの場合は、初回リクエスト時にアプリケーション全体のJavaScriptコードが返却され、その後は基本的にサーバーへリクエストを飛ばさず動的にDOMを書き換えます。 これはURL(ブラウザのアドレスバー)が変わってもリクエストを飛ばさずクライアントで完結するということです。 動的にDOMを書き換えページ遷移を疑似的に行い、ブラウザのセッション履歴を同期させることがSPAにおけるルーティングとなります。

React Routerのインストールとバージョン

ルーティングのおさらいをしたところで実際にReact Routerをインストールしてみましょう。 React Routerを使用するにはreact-router-domをインストールする必要があります。

npm i react-router-dom

また本記事ではViteで開発環境を作成し、ReactやReact Routerのバージョンは下記を使用します。

  • react 18.2.0
  • react-dom 18.2.0
  • react-router-dom 6.21.2

まずはルーティングをさせてみる

react-router-domをインストールしたところで実際に使用してみましょう。

プロバイダコンポーネント

React Routerではルーティング機能を提供するプロバイダコンポーネントをトップレベルで記述します。 Viteで作成したReact環境の場合はmain.tsxに記述します。 これでReact RouterのAPIが下の階層のコンポーネントで使用することができるようになります。

src/main.tsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'

import App from './App.tsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
)

ルーティングの設定

プロバイダコンポーネントの記述が済みましたので実際にルーティングの設定をしてみましょう。 インデックスページの場合、Topというコンポーネントを表示、/aboutでAboutというコンポーネントを表示させるとし、2つのコンポーネントを作成します。 propsを渡すこともできるので、Aboutコンポーネントはpropsを渡してみることにします。

src/components/Top.tsx

import type { FC } from 'react';

export const Top: FC = () => {
  return <p>Topページ</p>;
};

src/components/About.tsx

import type { FC } from 'react';

type Props = {
  title: string;
};

export const About: FC<Props> = ({ title }) => {
  return <p>{title}ページ</p>;
};

ルーティング設定を記述するファイルを新たに用意しreact-router-domからRoutes, Routeコンポーネントを読み込みます。 これらは任意のパスとレンダリングされるコンポーネントの結び付けるReact Routerのもっとも基本的な機能になります。 pathに任意のパスを記述しelementに表示させるコンポーネントを記述します。 このとき注意点としてインデックスの場合は/を使いますが、それ以外では/をつけません。

src/routes/AppRouter.tsx

import type { FC } from 'react';
import { Route, Routes } from 'react-router-dom';

import { About } from '../components/About';
import { Top } from '../components/Top';

export const AppRouter: FC = () => {
  return (
    <Routes>
      <Route path="/" element={<Top />} />
      <Route path="about" element={<About title="About" />} />
    </Routes>
  );
};

リンクさせる

リンクを設定するにはLinkコンポーネントを使用します。 HTMLのaタグを使用してしまうとReact Routerの管轄外となり、リクエストが送信されてしまいます。 そのためアプリケーション内でリンクを設定するにはLinkコンポーネントを使用します。 Linkを設定するファイルを新たに用意しリンクの設定をします。

src/components/Nav.tsx

import type { FC } from 'react';
import { Link } from 'react-router-dom';

export const Nav: FC = () => {
  return (
    <nav>
      <ul>
        <li>
          <Link to="/">TOP</Link>
        </li>
        <li>
          <Link to="/about">ABOUT</Link>
        </li>
      </ul>
    </nav>
  );
};

Routerを読み込む

これで基本的なルーティングの準備が整いましたので読み込んで動作確認してみましょう。

src/App.tsx

import './App.css';
import { Nav } from './components/Nav';
import { AppRouter } from './routes/AppRouter';

function App() {
  return (
    <>
      <Nav />
      <AppRouter />
    </>
  );
}

export default App;

実際に表示されているナビゲーションをクリックし、ページ遷移すれば問題ありません。

プロバイダの種類

実際にルーティングをさせた際はBrowserRouterを使用しましたが、他にも以下のようなプロバイダの種類があります。

  • BrowserRouter ... History APIを使用しUIとURLを動的に同期する
  • HashRouter ... 何らかの理由でURLをサーバーに送信できない場合に使用
  • MemoryRouter ... メモリの中でだけで管理され、テスト等で使用
  • NativeRouter ... React Nativeで使用
  • StaticRouter ... Node.jsで環境でレンダリング時に使用

pathについて

pathは任意のパスとレンダリングされるコンポーネントの結び付けるものでした。 pathにはいくつか注意点もありますのでpathについて詳しく見ていきます。

pathの注意点

pathには注意点がいくつかあります。ルーティング設定時にも書いていますが、インデックス以外は/をつけません。 これは2階層目になるページでも同様です。 下記の例では、/blogではBlogコンポーネントが返却され/blog/postでPostコンポーネントが返却されます。

import type { FC } from 'react';
import { Route, Routes } from 'react-router-dom';

import { About } from '../components/About';
import { Blog } from '../components/Blog';
import { Post } from '../components/Post';
import { Top } from '../components/Top';

export const AppRouter: FC = () => {
  return (
    <Routes>
      <Route path="/" element={<Top />} />
      <Route path="about" element={<About />} />
      <Route path="blog">
        <Route path="" element={<Blog />} />
        <Route path="post" element={<Post />} />
      </Route>
    </Routes>
  );
};

他にも以下のような注意点があります。

  • 末尾の/は無視される
  • 大文字・小文字を区別するにはcaseSensitiveを指定
  • パスのマッチングはベストマッチのものが選択される
  • 正規表現が使えない
  • *は末尾のみ使用できる

indexについて

インデックスページはpath="/"と書いていましたが、index属性へ置き換えることが可能です。

import type { FC } from 'react';
import { Route, Routes } from 'react-router-dom';

import { About } from '../components/About';
import { Top } from '../components/Top';

export const AppRouter: FC = () => {
  return (
    <Routes>
      <Route index element={<Top />} />
      <Route path="about" element={<About />} />
    </Routes>
  );
};

Not Foundの設定

全てのパスにマッチせず404ページを見せたい場合は、その階層のパスエントリーの最後に*を置くことで実現できます。 下記の場合、post/abcなど存在しないパスを叩くとPostNotFoundコンポーネントが返却されます。

import type { FC } from 'react';
import { Route, Routes } from 'react-router-dom';

import { About } from '../components/About';
import { Blog } from '../components/Blog';
import { Post } from '../components/Post';
import { PostNotFound } from '../components/PostNotFound';
import { Top } from '../components/Top';

export const AppRouter: FC = () => {
  return (
    <Routes>
      <Route index element={<Top />} />
      <Route path="about" element={<About />} />
      <Route path="blog">
        <Route path="" element={<Blog />} />
        <Route path="post/*" element={<Post />} />
        <Route path="*" element={<PostNotFound />} />
      </Route>
    </Routes>
  );
};

コンポーネント

React Routerには他にもいくつかコンポーネントが存在します。 その中でも使用頻度の高そうなものをいくつか見ていきます。

Navigateコンポーネントはレンダリングされるとtoで指定したパスへ現在のlocationが変更されます。 またreplace属性を付与することで変更前のlocationが残りません。 例えば/abcというページにアクセスすると、マッチするものがないためインデックスページへリダイレクトされますが、このときに/abcというページにアクセスした履歴が残りません。 /abcにアクセスしブラウザバックすると/abcにアクセスする前のページに戻ります。

import type { FC } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';

import { About } from '../components/About';
import { Top } from '../components/Top';

export const AppRouter: FC = () => {
  return (
    <Routes>
      <Route index element={<Top />} />
      <Route path="about" element={<About />} />
      <Route path="*" element={<Navigate to="/" replace />} />
    </Routes>
  );
};

Linkコンポーネント

基本的なルーティングの設定をした際は、Linkコンポーネントのto属性にパスの文字列を渡しましたが、オブジェクトを渡すこともできます。 オブジェクトの場合はパスだけでなくパラメーターやハッシュの設定ができます。

import type { FC } from 'react';
import { Link } from 'react-router-dom';

export const Nav: FC = () => {
  return (
    <nav>
      <ul>
        <li>
          <Link to="/">TOP</Link>
        </li>
        <li>
          <Link
            to={{
              pathname: '/contact',
              search: '?from=here',
              hash: '#abc',
            }}
          >
            CONTACT
          </Link>
        </li>
      </ul>
    </nav>
  );
};

またLinkコンポーネントのstate属性にはユーザーに見せたくない情報などをリンク先に受け渡すこともできます。

import type { FC } from 'react';
import { Link } from 'react-router-dom';

export const Nav: FC = () => {
  return (
    <nav>
      <ul>
        <li>
          <Link to="/">TOP</Link>
        </li>
        <li>
          <Link
            to={{
              pathname: '/contact',
              search: '?from=here',
              hash: '#abc',
            }}
            state={{ secret: 'xxxxxxx' }}
            replace
          >
            CONTACT
          </Link>
        </li>
      </ul>
    </nav>
  );
};

NavLinkコンポーネントは、そのパスにマッチした場所にいるかどうかを属性に渡すことのできるLinkコンポーネントです。 真っ先に思いつく使用方法はカレントページのナビゲーションにスタイルを当てたりaria-currentを付与したりかと思います。 以下の例ではstyleで直接スタイルを当てるパターンとclassNameを付与するパターンです。

import type { FC } from 'react';
import { NavLink } from 'react-router-dom';

export const Nav: FC = () => {
  return (
    <nav>
      <ul>
        <li>
          <NavLink
            to="/"
            style={({ isActive }) => (isActive ? { color: '#f00' } : undefined)}
            aria-current="page"
          >
            TOP
          </NavLink>
        </li>
        <li>
          <NavLink
            to="/about"
            className={({ isActive }) => (isActive ? 'is-current' : undefined)}
            aria-current="page"
          >
            ABOUT
          </NavLink>
        </li>
      </ul>
    </nav>
  );
};

コールバックを渡し、その引数isActiveがパスにマッチするときにtrueを返します。 上記の例では三項演算子を使用しtrueならスタイルを当てfalseなら適用しないようにundefinedを渡しています。 aria-currentはパスにマッチしているときに付与してくれます。

Outletコンポーネント

Outletコンポーネントはネストされた子コンポーネントを親コンポーネントで表示するものです。 挙動を確認するために3つのコンポーネントを用意します。

src/components/Blog.tsx

import type { FC } from 'react';
import { Outlet } from 'react-router-dom';

export const Blog: FC = () => {
  return (
    <>
      <p>Blogページ</p>
      <Outlet />
    </>
  );
};

src/components/Post1.tsx

import type { FC } from 'react';

export const Post1: FC = () => {
  return <p>記事1</p>;
};

src/components/Post2.tsx

import type { FC } from 'react';

export const Post2: FC = () => {
  return <p>記事2</p>;
};

Routeコンポーネントは以下のようにネストされていたとします。

src/routes/AppRouter.tsx

import type { FC } from 'react';
import { Route, Routes } from 'react-router-dom';

import { Blog } from '../components/Blog';
import { Post1 } from '../components/Post1';
import { Post2 } from '../components/Post2';
import { Top } from '../components/Top';

export const AppRouter: FC = () => {
  return (
    <Routes>
      <Route index element={<Top />} />
      <Route path="blog" element={<Blog />}>
        <Route path="post1" element={<Post1 />} />
        <Route path="post2" element={<Post2 />} />
      </Route>
    </Routes>
  );
};

この場合、/blog/post1にアクセスするとOutletにはpost1が表示され、/blog/post2にアクセスするとOutletにはpost2が表示されます。 /blogにアクセスした場合はOutletには何も表示されません。

Hooks API

React RouterにはいくつかHooksが用意されています。 全て記載はできませんので、コンポーネント同様に使用頻度の高そうなものを見ていきましょう。

useNavigate

これはnavigate関数を返すAPIでNavigateコンポーネントに似ています。 navigate関数の第一引数にNavigateコンポーネントのto属性と同じものを渡すと同じ働きをします。 第二引数にはオブジェクトを渡すことができ、下記の場合はNavigateコンポーネントのreplace属性と同じ働きをします。 また第一引数に1や-1などの数値を渡すことで「進む」「戻る」の操作ができます。

import type { FC } from 'react';
import { useNavigate } from 'react-router-dom';

export const Nav: FC = () => {
  const navigate = useNavigate();

  return (
    <nav>
      <ul>
        <li>
          <button onClick={() => navigate('/', { replace: true })}>TOPへ</button>
        </li>
        <li>
          <button onClick={() => navigate(1)}>進む</button>
        </li>
        <li>
          <button onClick={() => navigate(-1)}>戻る</button>
        </li>
      </ul>
    </nav>
  );
};

useLocation

useLocationはコンポーネントの中でlocationオブジェクトを取得するためのものです。 locationが変わるたびに副作用を実行するときに利用します。 なお公式ドキュメントでは、locationが変わるたびにGoogle Analyticsのページビュー情報を送信するサンプルコードが記載されています。

import { useEffect } from 'react';
import ReactGa from 'react-ga4';
import { useLocation } from 'react-router-dom';

ReactGa.initialize('G-XXXXXXXX');

export const usePageTracking = () => {
  const location = useLocation();

  useEffect(() => {
    ReactGa.send({
      hitType: 'pageview',
      page: location.pathname + location.search,
    });
  }, [location.key]);
};

useParams

useParamsは動的にルーティングパラメータを取得するHooksです。 { パラメータ名: パラメータ }形式のオブジェクトで使用することができます。 以下のコンポーネントとルーティング設定があったとします。

src/components/Post.tsx

import type { FC } from 'react';
import { useParams } from 'react-router-dom';

export const Post: FC = () => {
  const post = useParams();
  console.log(post);

  return <p>post</p>;
};

src/routes/AppRouter.tsx

import type { FC } from 'react';
import { Route, Routes } from 'react-router-dom';

import { Blog } from '../components/Blog';
import { Post } from '../components/Post';
import { Top } from '../components/Top';

export const AppRouter: FC = () => {
  return (
    <Routes>
      <Route index element={<Top />} />
      <Route path="blog" element={<Blog />}>
        <Route path=":post" element={<Post />} />
      </Route>
    </Routes>
  );
};

この場合/blog/post1と/blog/abcにアクセスするとぞれぞれ以下のコンソールが返ってきます。

// /blog/post1へアクセス

> { post: 'post1' }


// /blog/abcへアクセス
> { post: 'abc' }

また以下のように分割代入をすることで画面上へ表示することもできます。

src/components/Post.tsx

import type { FC } from 'react';
import { useParams } from 'react-router-dom';

export const Post: FC = () => {
  const { post } = useParams();

  return <p>{post}</p>;
};

useParamsの効果的な使用方法は、取得したパラメータを用いてデータのフェッチを行うことです。 JSONPlaceholderのAPIを使用しておこなってみます。

src/routes/AppRouter.tsx

import type { FC } from 'react';
import { Route, Routes } from 'react-router-dom';

import { Post } from '../components/Post';
import { Posts } from '../components/Posts';
import { Top } from '../components/Top';

export const AppRouter: FC = () => {
  return (
    <Routes>
      <Route index element={<Top />} />
      <Route path="posts" element={<Posts />}>
        <Route path=":postId" element={<Post />} />
      </Route>
    </Routes>
  );
};

src/components/Post.tsx

import type { FC } from 'react';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';

interface PostData {
  id: number;
  title: string;
  body: string;
}

export const Post: FC = () => {
  const { postId } = useParams();
  const [post, setPost] = useState<PostData | null>(null);

  useEffect(() => {
    const fetchPost = async (): Promise<void> => {
      try {
        const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
        const responseData = await response.json();
        setPost(responseData);
      } catch (error) {
        console.log(error);
      }
    };

    fetchPost();
  }, [postId]);

  if (!post) return <p>Not Found</p>;

  return (
    <>
      <p>id:{post.id}</p>
      <p>title:{post.title}</p>
      <p>body:{post.body}</p>
    </>
  );
};

このようにすると/posts/1や/posts/2のように取得するパラメータによってデータ取得することができます。

useSearchParams

useSearchParamsはクエリの取得・変更をすることができ、ReactのHooksであるuseStateの書き方に似ています。

const [searchParams, setSearchParams] = useSearchParams();

setSearchParams()を使うことでクエリ文字列を変更でき、searchParamsにはURLSearchParamsオブジェクトが入ります。 URLSearchParamsはReact Routerの一部ではなくWeb APIの一部になります。 クエリ文字列を取得するにはget()を使用します。

src/components/Top.tsx

import type { ChangeEvent, FC } from 'react';
import { Link, useSearchParams } from 'react-router-dom';

export const Top: FC = () => {
  const [searchParams, setSearchParams] = useSearchParams();

  const handleQueryParamChange = (e: ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setSearchParams((params) => ({ ...params, [name]: value }));
  };

  const queryParam = 'queryParam';

  return (
    <>
      <p>TOPページ</p>
      <label>
        パラメータ:
        <input
          type="text"
          name={queryParam}
          value={searchParams.get(queryParam) || ''}
          onChange={handleQueryParamChange}
        />
      </label>
      <Link to={`/param?${queryParam}=${searchParams.get(queryParam)}`}>Paramページへ</Link>
    </>
  );
};

src/components/Param.tsx

import type { FC } from 'react';
import { useSearchParams } from 'react-router-dom';

export const Param: FC = () => {
  const [searchParams] = useSearchParams();

  return <p>パラメータ: {searchParams.get('queryParam')}</p>;
};

いかがだったでしょうか。React Routerの基本的な機能は一通り知ることができたのではないでしょうか。 今回は基本的な機能に絞ったのでReact Router v6.4から追加されたData APIについては解説しませんでした。 この記事を更新するか新たに作成するかしてどこかで解説できればと思います。 Hooksも他に種類があるので要件に応じて必要な機能をドキュメントから確認いただければと思います。