[React Router] useRoutesフックでルーティングを実装

  • ReactJS

React.jsアドベントカレンダーの8日目の記事です。

React Routerのv6が今年(2021年)の11月リリースされました。そして、React Routerはv6へと進化を遂げたのと同時期にRemixのv1がリリースされ、その裏ではReact Routerが利用されています。また、React LocationのStable版もリリースされたことで、ルーティング実装の選択肢が1つ加わるなど、今年11月ごろから何かと話題の絶えないReact.js界隈です。

さて、React Routerのv5からv6のアップグレード作業は、公式のアップグレード手順を参考にして作業を進めてが特に大きな問題なく済みました。主にコンポーネントとフックの置き換えがほとんどだったのでそこまで大変なものではありませんでした。置き換えだけで終わったも良いのですが、せっかくなので新しい機能であるuseRoutesフックを使ったルーティングの実装をしてみようと思い、使ってみました。今回は例を示しつつ、useRoutesフックについて紹介したいと思います。

v6のベータ版からちらほら話題にはなっていましたが、正式リリースされたのでどんどん本番投入していきましょう!

例から学ぶuseRoutesフック

公式ドキュメントに説明はありますが、ここでも例を挙げて説明します。

Routes要素とRoute要素でルート定義する例

まず、React Routerでルーティングの実装をするときは、主に <Routes /><Route /> を利用します。v5では <Switch /> を使いますが、これは <Routes /> と読み替えていただければ良いです。

以下に <Routes /><Route /> を使ってJSXで定義した例を示します。

import React from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'
// ※ここではLayoutやFrontPageなどの自作コンポーネントのimportは省略して載せています
const AppRouter = () => (
  <Routes>
    <Route path='/' element={<Layout />}>
      <Route index element={<FrontPage />} />
      <Route path='*' element={<>Not Found</>} />
      <Route path='examples'>
        <Route index element={<Navigate to='/' replace />} />
        <Route path='child-a' element={<ChildA />} />
        <Route path='child-a' element={<ChildB />} />
        <Route path='child-a' element={<ChildC />} />
      </Route>
    </Route>
  </Routes>
)

useRoutesフックでルート定義する例

次に上記の例でuseRoutesフックを使ったケースを示します。

先にコードをざっくりと説明すると、オブジェクト(RouteObject)でルートの定義をして、useRoutesフックで構築したRoute要素(先ほどの例を参照)を表示しています。

import React from 'react'
import { Navigate, useRoutes } from 'react-router-dom'
// ※ここではLayoutやFrontPageなどの自作コンポーネントのimportは省略して載せています
const AppRouter = () => {
  const routes = useRoutes([
    {
      path: '/',
      element: <Layout />,
      children: [
        {
          index: true,
          element: <FrontPage />,
        },
        {
          path: '*',
          element: <>Not Found</>,
        },
        {
          path: 'examples',
          children: [
            {
              index: true,
              element: <Navigate to='/' replace />,
            },
            {
              path: 'child-a',
              element: <ChildA />,
            },
            {
              path: 'child-b',
              element: <ChildB />,
            },
            {
              path: 'child-c',
              element: <ChildC />,
            },
          ],
        },
      ],
    },
  ])
  return <>{routes}</>
}

useRoutesフックの引数には、RouteObjectの配列をとります。RouteObjectの定義を載せておきます。

基本的にpathプロパティとelementプロパティ、childreプロパティといったシンプルな構成のオブジェクトです。

// react-router-dom v6.0.2時点のでRouteObjectの定義
export interface RouteObject {
  caseSensitive?: boolean;
  children?: RouteObject[];
  element?: React.ReactNode;
  index?: boolean;
  path?: string;
}

useRouteフックはざっくり言えば、ルート定義のオブジェクトを渡してあげるだけで面倒なJSXでの定義をしてくれるフックです。

もちろんJSXで記述しても良いですが、ページ数が多い、またはネストが深い場合にはオブジェクトで記述できた方が見通しが良いかなと個人的に思います。あと、オブジェクトで定義してあった方が何かと扱いやすいです。

余談ですが、v6以前にもオブジェクトベースのルート定義はReact Router Configを別途導入することでできましたが、v6でコアに取り込まれたようです。オブジェクトの構成を見比べると名残のようなものが見られます。

応用例

このまま終わっても良いと思ったのですが、よりイメージしやすいように他に使用例のサンプルを載せておきます。GitHubにリポジトリを用意したので覗いてみてください。(本記事に載せているもの以外のコードがリポジトリ中にありますが、あまりお気になさらず)

🐙 GitHubリポジトリ: demo-react-router

次のようなケースを扱うサンプルを1つ紹介します。

  • React.lazyとSuspenseを用いてルートごとでCodeSplitting
  • ルートの保護(ログインしているか否かでアクセス制御)

React.jsでSPAを開発する時によく実装するような内容だと思います。

ルートの定義

次のルートを定義します。

  • /protections/child-a (child-b, child-cまで) ログイン済み状態(フラグが立っている場合)のみ閲覧可能なページです。未ログインの場合はアクセス禁止用のコンテンツを表示します。

ログイン状態の管理

あくまでサンプルなので、実際に認証はしていません。Context APIでフラグを管理し、trueの場合はログイン済み、falseの場合は未ログインとして扱います。ログイン状態はサイドメニューから切り替えられるようにしています。

詳細はリポジトリ中の src/context/MockAuthContext.tsx を参照してください。

今回は、これに関しては重要ではないので説明は割愛します。

protectionsルートの定義

リポジトリ中の src/routes/protections に内容がありますが、下記にもコードを載せておきます。

/**
  * src/routes/protections/index.tsx
  */

import React, { lazy, LazyExoticComponent, useContext } from 'react'
import { Navigate, Outlet, RouteObject } from 'react-router-dom'
import { MockAuthContext } from '~/context/MockAuthContext'

const ProtectedRoute = () => {
  const { isLogin } = useContext(MockAuthContext)
  // 未ログインであれば、アクセス禁止を表すコンテンツを表示する
  if (!isLogin) {
    return <>⛔</>
  }
  return <Outlet />
}

export const protectedRouteMap = new Map<string, LazyExoticComponent<() => JSX.Element>>([
  ['child-a', lazy(() => import('~/routes/protections/child-a'))],
  ['child-b', lazy(() => import('~/routes/protections/child-b'))],
  ['child-c', lazy(() => import('~/routes/protections/child-c'))],
])

export const protectedRouteObject: RouteObject = {
  path: 'protections/',
  // ログインしている状態の時のみアクセスできるようにする
  element: <ProtectedRoute />,
  children: [
    {
      index: true,
      element: <Navigate to='/' replace />,
    },
    ...Array.from(protectedRouteMap, ([path, ExoticComponent]) => ({
      path,
      element: <ExoticComponent />,
    })),
  ],
}

<ProtectedRoute /> はログインしていなければ、⛔を出力するというシンプルな作りです。もちろんリダイレクトをさせる処理を書いても良いです。

コード中にある <Outlet /> はReact Routerの要素で、ここには下位ルート(childrenプロパティに指定しているRouteObject群)で一致するパスのコンテンツが出力されます。したがって、変数 protectedRouteObject のルートのelementプロパティに <ProtectedRoute /> を設定しています。

上のコードのようにルーティング制御したいルートの上位で、今回作成した <ProtectedRoute /> のようなコンポーネントを設けることで簡単に実現できます。今回はルーティング制御で例を示しましたが、レイアウト(ヘッダーやフッター等)の共通化を目的とした用途でも使えます。

さて、あとはuseRoutesフックの引数に変数 protectedRouteObject を入れれば完成です。src/components/App.tsx に書いてあるので、見ていきましょう。

/**
  * src/components/App.tsx
  */

import React, { Suspense, lazy, useContext } from 'react'
import { useRoutes } from 'react-router-dom'
import Layout from '~/components/Layout'
import { protectedRouteObject } from '~/routes/protections'

const FrontPage = lazy(() => import('~/routes'))

const App = () => {
  const routes = useRoutes([
    {
      path: '/',
      element: <Layout />,
      children: [
        // protectionsルートのRouteObjectを追加
        protectedRouteObject,
        {
          index: true,
          element: <FrontPage />,
        },
        {
          path: '*',
          element: <>Not Found</>,
        },
      ],
    },
  ])
  return <Suspense fallback={<div>Loading</div>}>{routes}</Suspense>
}

export default App

しれっとReact.lazyを用いていましたが、これはコード分割をするために用いています。閲覧しているページのコンテンツ以外のコードは、基本的には不要なので分割して必要な時にロードするようにしておくのがベターかと思います。今回のサンプルは分割するまでもない規模ですが、大きなプロジェクトだと必然的にバンドルサイズが大きくなるため、コード分割は重要になってくるかと思います。

そして、ロード中にスケルトンを表示するために <Suspense /> でラップします。

詳しくはReact.js公式ドキュメントに載っていますので、目を通しておくと良いかと思います。

おわりに

useRoutesフックについてサンプルベースで紹介しましたが、いかがでしたでしょうか。なんとなく利用するイメージが掴んでいただけたら幸いです。React RouterはRemixで使われているので、今後はSSR関連の機能が増えたりしていくのでしょうか。Remix含め、ウォッチしていきたいことろですね。

この記事を共有

アバター

K.Utsunomiya
主にWeb技術について投稿していきます。
詳しいプロフィール