React Router v7(フレームワーク利用) 実践ガイド:ブログサイトを作りながら学ぶ最新ルーティング

2025/01/23に公開

はじめに

本記事では、React Router v7のフレームワーク機能を使用してブログサイトを構築する際の基本的な使い方から、複雑なルーティング例までを幅広く解説します。この記事を読むことで、単純なページ遷移から動的ルーティング、さらにはネストされたルートの設定まで習得することができます。
初心者の方からステップアップを目指す方まで、ぜひ一緒にReact Router v7の世界を探ってみましょう。

React Router とは

React Routerは、Reactアプリケーション向けのルーティングライブラリです。ルーティングとは、ユーザーが特定のURLにアクセスした際に、どのコンポーネントを表示するかを制御する仕組みのことです。

https://reactrouter.com/home

React Routerはv7よりReactフレームワークとして最大限に使用することも、独自のアーキテクチャを持つライブラリとして最小限に使用することもできます。

フレームワーク利用とは?

React Routerはv7よりReactフレームワークとして活用することも可能となりました。

具体的には下記のような機能を提供しています。

  • Vite バンドラーと開発サーバーの統合
  • ホットモジュールの置換
  • コード分割
  • 型の安全性を確保したファイルシステムまたは設定ベースのルーティング
  • 型の安全性を確保したデータロード
  • 型の安全性を確保したアクション
  • アクション後のページデータの自動再バリデーション
  • SSR、SPA、静的レンダリング戦略
  • 保留状態と楽観的 UI
  • デプロイメントアダプター

今回ご紹介する機能はこちらが中心となります。

ブログサイトを構築する前提でこれらの機能をどう活用していくか詳しくご紹介していきます。

ライブラリ利用とは?

これまでのバージョン(v6)以前のバージョンはシンプルで宣言的なルーティング・ライブラリとして使用することができます。
URLとコンポーネントのセットをマッチングして、URLデータへのアクセスを提供し、アプリ内をナビゲートすることとなります。

これまでv6を利用してきたユーザーはv7にバージョンアップしたのち引き続きライブラリとしてReact Routerを使うことが多いかと思います。

React Router v7のライブラリ利用について詳しく知りたい方はこちらをご参考ください。

https://shinagawa-web.com/blogs/react-router-v7-library-blog-setup-guide

今回の記事のゴール

React Router v7のフレームワーク機能を使って下記のページにそれぞれ遷移できるようルーティングの設定を中心に実装します。

  • トップページ

  • 投稿

  • マイページ

    • アカウント

    • 設定画面

それぞれのページにアクセスしやすいようにナビゲーションを作成し遷移できるようにしています。

Image from Gyazo

セットアップ

React Routerが用意しているテンプレートを使ってプロジェクトを作成していきます。

mkdir react-router-v7-framework-tutorial
cd react-router-v7-framework-tutorial
npx create-react-router@latest .

Image from Gyazo

これでコードのセットアップから依存パッケージのダウンロードまで完了しました。

下記コマンドで起動します。

npm run dev

Image from Gyazo

プロジェクトにあるREADME.mdを見てみましょう。

- 🚀 Server-side rendering
- ⚡️ Hot Module Replacement (HMR)
- 📦 Asset bundling and optimization
- 🔄 Data loading and mutations
- 🔒 TypeScript by default
- 🎉 TailwindCSS for styling
- 📖 [React Router docs](https://reactrouter.com/)

これらの機能が既に使える状態で用意されています。

またpackage.jsonを確認するとReactのバージョンが最新の19になっており、19の機能を考慮したフレームワークとなっています。

package.json
  "dependencies": {
    "@react-router/node": "^7.1.3",
    "@react-router/serve": "^7.1.3",
    "isbot": "^5.1.17",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-router": "^7.1.3"
  },

ルーティング

プロジェクトを起動したときに画面が表示されたように既にトップページのルーティングがセットアップ段階で行われています。

app/routes.ts
import { type RouteConfig, index } from "@react-router/dev/routes";

export default [index("routes/home.tsx")] satisfies RouteConfig;

トップにアクセスが来た場合にroutes/home.tsxをレンダリングする設定となっています。
フレームワークとして利用する場合はこのapp/routes.tsがルーティングを担うメインのファイルとなります。

routes/home.tsx
import type { Route } from "./+types/home";
import { Welcome } from "../welcome/welcome";

export function meta({}: Route.MetaArgs) {
  return [
    { title: "New React Router App" },
    { name: "description", content: "Welcome to React Router!" },
  ];
}

export default function Home() {
  return <Welcome />;
}

2つエクスポートされていますがデフォルトエクスポートしている箇所がレンダリングしている部分となります。

export default function Home() {
  return <Welcome />;
}

実態としては../welcome/welcome.tsxとなります。

なんとなくappディレクトリの構造が見えてきたところでまずはトップを修正していきます。

routes/home.tsx
import type { Route } from "./+types/home";
- import { Welcome } from "../welcome/welcome";

export function meta({}: Route.MetaArgs) {
  return [
    { title: "New React Router App" },
    { name: "description", content: "Welcome to React Router!" },
  ];
}

export default function Home() {
- return <Welcome />;

+ return (
+   <h1>
+     トップ
+   </h1>
+ )
}

welcomeは削除してトップという表示を出してみます。

Image from Gyazo

トップだけだと少し寂しいのでブログ記事を表示してみます。

app/const/posts.ts
export const posts = [
  {
    id: 1,
    title: 'React Router 7のセットアップ',
    description: 'セットアップ以外にも様々な機能をご紹介',
  },
  {
    id: 2,
    title: 'Reactでの状態管理方法',
    description: 'useState、useContextなどをご紹介',
  },
  {
    id: 3,
    title: 'コンポーネント設計のベストプラクティス',
    description: 'ディレクトリ構成についてもご紹介',
  },
]

export const getPostById = (id: number) => {
  return posts.find((post) => post.id === id)
}

本来であればデータベースやCMSなどでブログ情報を管理して取得することとなりますが今回の記事からかなり脱線するので簡単なデータをファイルで用意しました。

3件のブログ情報とそのブログ情報をIDで取得できる関数を用意しました。

別のアーキテクチャになりますがデータベースやCMSなどでデータ取得するチュートリアルも過去に作成していますので興味ありましたらこちらもご参考ください。

https://shinagawa-web.com/blogs/nextjs-microcms-blog-tutorial

https://shinagawa-web.com/blogs/express-mongodb-rest-api-development-with-typescript

話を戻します。

ブログデータが用意できましたのでトップに表示します。

routes/home.tsx
+ import { posts } from '~/const/posts'
import type { Route } from './+types/home'

export function meta({}: Route.MetaArgs) {
  return [
    { title: 'New React Router App' },
    { name: 'description', content: 'Welcome to React Router!' },
  ]
}

export default function Home() {
  return (
+   <>
      <h1>トップ</h1>
+     <ul>
+       {posts.map((post) => (
+         <li key={post.id}>{post.title}</li>
+       ))}
+     </ul>
+   </>
  )
}

先ほど作成したブログデータのタイトルがトップページに表示されました。

Image from Gyazo

動的セグメント

次はブログの個別記事を参照するための設定をしていきたいと思います。

ホームでブログ記事のタイトルをクリックするとブログの個別記事のページに遷移するというものです。

まずはルーティングの設定から。

app/routes.ts
- import { type RouteConfig, index } from '@react-router/dev/routes'
+ import { type RouteConfig, index, route } from '@react-router/dev/routes'

export default [
  index('routes/home.tsx'),
+ route('post/:postId', './routes/post.tsx'),
] satisfies RouteConfig

post/:postIdでブログの個別記事を表示するよう設定しました。postIdはブログデータのIDを想定しています。こちらが動的セグメントという機能になります。

次に表示を行うコンポーネントを作成します。

app/routes/post.tsx
import { getPostById } from '~/const/posts'
import type { Route } from './+types/post'

export default function Post({ params }: Route.ComponentProps) {
  const post = getPostById(Number(params.postId))

  if (!post) {
    return <div>投稿が見つかりませんでした。</div>
  }

  return (
    <div>
      <h1>{post.title}</h1>
      {post.description}
    </div>
  )
}
import type { Route } from './+types/post'

ルーティングを設定すると自動で生成される型定義になります。

export default function Post ({
  params,
}: Route.ComponentProps) {

paramsが使えるようになります。

エディタ上でも確認できますが、params.postIdでURLで渡ってきたpostIdを取得できます。

Image from Gyazo

  const post = getPostById(Number(params.postId))

postIdを使ってブログデータを1件取得します。

  if (!post) {
    return <div>投稿が見つかりませんでした。</div>
  }

  return (
    <div>
      <h1>{post.title}</h1>
      {post.description}
    </div>
  )

取得結果によってレンダリングをおこなっています。

ブログデータに存在しないIDでリクエストが発生した場合を考慮して投稿が見つかりませんでした。という表示も出せるようにしています。

ブログデータが見つかった場合はtitle, descriptionを表示します。

それでは実際に動作確認を行ってみます。

ブログデータが存在する場合

Image from Gyazo

ブログデータが存在しない場合

Image from Gyazo

最後にブログ一覧からブログ個別記事に遷移できる設定を行います。

routes/home.tsx
import { posts } from '~/const/posts'
import type { Route } from './+types/home'
+ import { Link } from 'react-router'

export function meta({}: Route.MetaArgs) {
  return [
    { title: 'New React Router App' },
    { name: 'description', content: 'Welcome to React Router!' },
  ]
}

export default function Home() {
  return (
    <>
      <h1>トップ</h1>
      <ul>
        {posts.map((post) => (
-         <li key={post.id}>{post.title}</li>
+         <li key={post.id}>
+           <Link to={`/post/${post.id}`}>{post.title}</Link>
+         </li>
        ))}
      </ul>
    </>
  )
}

React RouterLinkを使用するとクリックした時にtoで設定したURLに遷移できます。

一通り設定が完了しましたので実際にホーム画面から遷移できるか確認してみます。

Image from Gyazo

少しだけブログサイトっぽくなってきました。

ネストしたルート

今度はブログサイトにマイページ画面を作成しようと思います。

マイページではアカウント情報を見る画面と設定変更をできる画面を作成します。

app/routes.ts
import { type RouteConfig, index, route } from '@react-router/dev/routes'

export default [
  index('routes/home.tsx'),
  route('post/:postId', './routes/post.tsx'),
+ route('mypage', './routes/mypage/index.tsx', [
+   route('account', './routes/mypage/account.tsx'),
+   route('settings', './routes/mypage/settings.tsx'),
+ ]),
] satisfies RouteConfig

ルートはネストが可能となっています。

/mypageでアクセスすると./routes/mypage/index.tsxを表示します。

app/routes/mypage/index.tsxファイルを作成してみましょう。

app/routes/mypage/index.tsx
import { Outlet } from 'react-router'

export default function MyPage() {
  return (
    <>
      <h1>マイページ</h1>
      <Outlet />
    </>
  )
}

Outlet は、React Router のコンポーネントで、ネストされたルートのコンテンツを表示するためのプレースホルダー です。

具体的には、親ルート に共通のレイアウトやコンポーネント (例: ヘッダーやサイドバー) を定義しつつ、その中に 子ルート のコンテンツを埋め込むために使用されます。

では子ルートにあたる、account.tsxsettings.tsxを作成します。

app/routes/mypage/account.tsx
export default function Account() {
  return <h2>アカウント</h2>
}
app/routes/mypage/settings.tsx
export default function Settings() {
  return <h2>設定画面</h2>
}

このように設定することでマイページという一つのグループにアカウントや設定画面があるという構成ができます。

それでは実際にネストされたルートにアクセスできるか確認します。

まずは/mypage

「マイページ」という文言が表示されています。

Image from Gyazo

次に/mypage/account

すると親ルートにあたる「マイページ」という文言と子ルートにあたる「アカウント」という文字が表示されることが確認できます。

Outletが正常に動いて子ルートを表示できていることがわかります。

Image from Gyazo

このようなルーティングの設定でも動くのですが幾つか便利な機能がありますので合わせて紹介します。

app/routes.ts
- import { type RouteConfig, index, route } from '@react-router/dev/routes'
+ import { type RouteConfig, index, layout, prefix, route } from '@react-router/dev/routes'

export default [
  index('routes/home.tsx'),
  route('post/:postId', './routes/post.tsx'),
- route('mypage', './routes/mypage/index.tsx', [
-   route('account', './routes/mypage/account.tsx'),
-   route('settings', './routes/mypage/settings.tsx'),
- ]),
+ ...prefix('mypage', [
+   index('./routes/mypage/index.tsx'),
+   layout('./routes/mypage/layout.tsx', [
+     route('account', './routes/mypage/account.tsx'),
+     route('settings', './routes/mypage/settings.tsx'),
+   ])
+ ])
] satisfies RouteConfig

prefix を使用すると、親ルートファイルを作成する必要なく、ルートのセットにパスプレフィックスを追加できます。

ルートはネストしたいけどそれ専用のコンポーネントはいらないよというときに使えます。

...prefix('mypage', [

インデックスルートは、親の URL で親の Outlet にレンダリングされます(デフォルトの子ルートのように)。

  index('./routes/mypage/index.tsx'),

ただ親ではプレフィックスを使っているためOutletではなく./routes/mypage/index.tsxこちらのレンダリング結果がそのまま表示されます。

つまりこの設定で/mypageにアクセスすると./routes/mypage/index.tsxを表示するようになりました。

  layout('./routes/mypage/layout.tsx', [

こちらはレイアウト専用のコンポーネントを定義できるlayoutとなります。

URLのネストをさせたくないけど、子ルートのaccount.tsxsettings.tsxに対して共通のレイアウトを適用したい場合に使用します。

このように設定することで親と子ルートの関係を細かく分離できるようになりました。

少し動作確認をしたいのでコンポーネントを作成、編集します。

まずはレイアウト用のコンポーネント

app/routes/mypage/layout.tsx
import { Outlet } from 'react-router'

export default function MyPage() {
  return (
    <>
      <h1>マイページ</h1>
      <Outlet />
    </>
  )
}

このように設定することでレイアウトが適用される子ルートに対して「マイページ」という表示がされます。

次は/mypageにアクセスされたときに表示するコンポーネントです。

app/routes/mypage/index.tsx
- import { Outlet } from 'react-router'

export default function MyPage() {
  return (
    <>
      <h1>マイページトップ</h1>
-    <Outlet />
    </>
  )
}

/mypageにアクセスされた場合に「マイページトップ」と表示されるようになります。

動作確認

まずは/mypage

「マイページトップ」という文言が表示されています。

Image from Gyazo

次に/mypage/account

するとレイアウトで設定した「マイページ」という文言と子ルートにあたる「アカウント」という文字が表示されることが確認できます。

Image from Gyazo

最初のネストしたルートだと親ルートの表示内容が子ルートに直接影響を与えていましたが、今回の設定でそれを分割し独立して動かすことが可能となりました。

ナビゲーション

これまで幾つかのURLを定義してきました。

都度、ブラウザでURLを直接入力していくには厳しいボリュームになってきましたのでそれぞれのページにアクセスしやすくナビゲーションを作っていきます。

その前に現状の画面遷移を一旦整理してみます。

  • トップページ
    /routes/home.tsx

  • 投稿
    /post/:postIdroutes/post.tsx:postId は投稿の ID)

  • マイページ
    /mypageroutes/mypage/index.tsx

    • アカウント
      /mypage/accountroutes/mypage/account.tsx

    • 設定画面
      /mypage/settingsroutes/mypage/settings.tsx

このようになっていますので2種類のナビゲーションを用意します。

  • トップからマイページに遷移
  • マイページ内でアカウントや設定画面に遷移

トップからマイページに遷移

ヘッダー部分に表示するナビゲーション用のコンポーネントを作成します。

app/components/home-navigation.tsx
import { NavLink } from 'react-router'

export function HomeNavigation() {
  return (
    <nav className='pb-5 mb-5 border-b flex gap-4'>
      <NavLink to="/" end>
        トップ
      </NavLink>
      <NavLink to="/mypage" end>
        マイページ
      </NavLink>
    </nav>
  )
}

React RouterのNavLinkを使います。

NavLinkはアクティブ状態をレンダリングする必要があるナビゲーションリンク用です。

具体的には該当のURLにアクセスがしているときにactiveというクラスが設定されます。

そのためCSSでactiveに対して文字色を変えたり背景色を付けたりできるようになります。

app.css
@tailwind base;
@tailwind components;
@tailwind utilities;

html,
body {
  padding: 20px;
}

h1 {
  @apply font-bold text-xl
}

h2 {
  @apply font-semibold text-lg
}

.active {
  @apply text-sky-600;
}

今回はactiveクラスがついている要素に対して文字色を青にしました。

ナビゲーション用のコンポーネントが作成できましたのでトップページに適用します。

routes/home.tsx
import { posts } from '~/const/posts'
import type { Route } from './+types/home'
import { Link } from 'react-router'
+ import { HomeNavigation } from '~/components/home-navigation'

export function meta({}: Route.MetaArgs) {
  return [
    { title: 'New React Router App' },
    { name: 'description', content: 'Welcome to React Router!' },
  ]
}

export default function Home() {
  return (
    <>
-     <h1>トップ</h1>
+     <HomeNavigation />
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <Link to={`/post/${post.id}`}>{post.title}</Link>
          </li>
        ))}
      </ul>
    </>
  )
}

ついでにマイページのトップにもナビゲーションを適用します。

app/routes/mypage/index.tsx
+ import { HomeNavigation } from "~/components/home-navigation";

export default function MyPage() {
  return (
    <>
+     <HomeNavigation />
      <h1>マイページトップ</h1>
    </>
  )
}

それではナビゲーションを使ってトップとマイページのトップにアクセスできるか確認します。

Image from Gyazo

想定通りページの切り替えができ、且つ現在自分のいる場所がどこかわかるように文字色が青になりました。

マイページ内でアカウントや設定画面に遷移

次はマイページ内でアカウントや設定画面に遷移しやすくするためのナビゲーションを作成します。

app/components/mypage-navigation.tsx
import { NavLink } from 'react-router'

export function MyPageNavigation() {
  return (
    <nav className="mb-5 flex gap-4 border-b pb-5">
      <NavLink to="/mypage/account" end>
        アカウント
      </NavLink>
      <NavLink to="/mypage/settings" end>
        設定画面
      </NavLink>
    </nav>
  )
}

基本的には先ほど作成したトップで使用していたナビゲーションと同じような構造になります。
リンク先だけアカウントや設定画面用を設定しています。

各画面にナビゲーションを適用していきます。

まずはマイページの子ルートのレイアウト。

app/routes/mypage/layout.tsx
import { Outlet } from 'react-router'
+ import { HomeNavigation } from '~/components/home-navigation'
+ import { MyPageNavigation } from '~/components/mypage-navigation'

export default function MyPage() {
  return (
    <>
+     <HomeNavigation />
+     <MyPageNavigation />
      <Outlet />
    </>
  )
}

今回作成したマイページ用のナビゲーションにプラスしてトップへ遷移できるナビゲーションも設定しました。
これでアカウントや設定画面からトップへの遷移もできるようになります。

次はマイページのトップです。

app/routes/mypage/index.tsx
import { HomeNavigation } from '~/components/home-navigation'
+ import { MyPageNavigation } from '~/components/mypage-navigation'

export default function MyPage() {
  return (
    <>
      <HomeNavigation />
+     <MyPageNavigation />
      <h1>マイページトップ</h1>
    </>
  )
}

マイページのトップからアカウントや設定画面に遷移するためにナビゲーションを設定しました。

最後にトップへ遷移するナビゲーションを修正します。

app/components/home-navigation.tsx
import { NavLink } from 'react-router'

export function HomeNavigation() {
  return (
    <nav className="mb-5 flex gap-4 border-b pb-5">
      <NavLink to="/" end>
        トップ
      </NavLink>
-     <NavLink to="/mypage" end>
+     <NavLink to="/mypage">
        マイページ
      </NavLink>
    </nav>
  )
}

endを削除しました。
endがついているとtoで設定したURLにアクセスした時のみactiveクラスが設定されます。
逆に外すとtoで設定したURLを含むもの全てのURLでactiveクラスが設定されます。

それでは動作確認を行います。

/へのアクセスはこれまで通りとなります。

Image from Gyazo

マイページをクリックするとマイページのトップに遷移しマイページ用のナビゲーションが表示されました。

Image from Gyazo

設定画面をクリックすると設定画面に遷移し、「マイページ」「遷移画面」2つの文字色が青になりました。

これで現在自分のいる場所がマイページの設定画面だということがわかります。(実際のUIはアイコンや表示サイズを変えるなどしてもう少しわかりやすくした方がいいかと思いますが)

ここから「トップ」をクリックするとトップページに遷移もできます。

Image from Gyazo

忘れていましたがブログの個別記事を表示するコンポーネントにもナビゲーションを入れておきます。

import { getPostById } from '~/const/posts'
import type { Route } from './+types/post'
+ import { HomeNavigation } from '~/components/home-navigation'

export default function Post({ params }: Route.ComponentProps) {
  const post = getPostById(Number(params.postId))

  if (!post) {
    return (
+     <>
+       <HomeNavigation />
+       <div>投稿が見つかりませんでした。</div>
+     </>
    )
  }

  return (
    <div>
+     <HomeNavigation />
      <h1>{post.title}</h1>
      {post.description}
    </div>
  )
}

さいごに

ここまで、React Router v7のフレームワーク機能を活用したブログサイト構築のさまざまなルーティング方法をご紹介しました。今回解説した内容を押さえることで、React Router v7を用いた柔軟なルーティング設計が可能になり、実践的なスキルを身につけることができたのではないでしょうか。
アプリケーション開発は、ユーザー体験を向上させる工夫の積み重ねです。今回の内容が、あなたのプロジェクトに役立つ一歩となれば幸いです。

次のブログ記事

JSON Placeholderというサンプルのjsonを返すサーバーを用いて投稿データをサーバサイド、クライアントサイドで取得する方法をご紹介しています。

レスポンスが遅い場合は考慮してスピナーなどを処理中に表示するなども併せて実装し解説しています。

https://shinagawa-web.com/blogs/react-router-v7-framework-blog-site-rendering-example

記事に関するお問い合わせ📝

記事の内容に関するご質問、ご意見などは、下記よりお気軽にお問い合わせください。
ご質問フォームへ

技術支援などお仕事に関するお問い合わせ📄

技術支援やお仕事のご依頼に関するお問い合わせは、下記よりお気軽にお問い合わせください。
お問い合わせフォームへ