はじめに
前回の記事ではReact + ViteでコンポーネントベースのUI開発を始める方法として、JSXの書き方・props・useState・useEffect・カスタムフックを解説しました。今回はTanStack Routerを使ってReactアプリにページ遷移を実装する方法を解説します。TanStack Routerは型安全なルーティングライブラリで、TypeScriptとの相性が抜群です。ポートフォリオサイトのトップページ・ブログ一覧・個別記事ページといった複数ページの構成を実装しながら解説します。
1. Reactのルーティングとは
1.1 SPAのルーティングの仕組み
ReactはデフォルトではSPA(Single Page Application)として動作します。
従来のWebサイト(MPA):
/ → index.html を読み込む
/about → about.html を読み込む
/blog → blog.html を読み込む
→ ページ移動のたびにサーバーへリクエストが発生する
ReactのSPA:
/ → index.html(1ファイル)+ JSがURLを見てUIを切り替える
/about → 同じindex.html + Reactがaboutコンポーネントを表示する
/blog → 同じindex.html + Reactがblogコンポーネントを表示する
→ サーバーへのリクエストなしに画面が切り替わる(高速)
1.2 TanStack RouterとReact Routerの比較
| 比較 | TanStack Router | React Router v6 |
|---|---|---|
| TypeScript対応 | ✅ 型安全なルーティング | △ 型サポートあるが弱め |
| ファイルベースルーティング | ✅ 対応 | ❌ 設定が必要 |
| データローディング | ✅ 組み込み | ✅ loaderで対応 |
| シェアード状態 | ✅ 充実 | △ Context頼み |
| 学習コスト | 中〜高 | 低〜中 |
| 採用実績 | 増加中 | 多い |
💡 今回はTanStack Routerを選びましたが、React Routerとの考え方は共通する部分が多いです: ルーティングの基本概念(パスの定義・動的ルート・ネストされたレイアウト)は同じです。どちらのライブラリを使っても、今回学ぶ概念は活かせます。
2. TanStack Routerのセットアップ
2.1 インストール
bash
# TanStack Routerをインストールする
npm install @tanstack/react-router
# Viteプラグインをインストールする(ファイルベースルーティング用)
npm install -D @tanstack/router-plugin
# ルート定義を生成するCLIツール
npm install -D @tanstack/router-cli
2.2 vite.config.tsにプラグインを追加する
typescript
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
export default defineConfig( {
plugins: [
TanStackRouterVite(), // ← TanStack Routerプラグイン(reactの前に書く)
react(),
],
resolve: {
alias: { '@': '/src' },
},
} );
2.3 ファイルベースルーティングのフォルダ構成
src/
├── routes/ ← ルートファイルを置くディレクトリ
│ ├── __root.tsx ← ルートレイアウト(全ページ共通)
│ ├── index.tsx ← / (トップページ)
│ ├── about.tsx ← /about
│ ├── blog/
│ │ ├── index.tsx ← /blog(ブログ一覧)
│ │ └── $postId.tsx ← /blog/[postId](動的ルート)
│ └── works/
│ ├── index.tsx ← /works(制作実績一覧)
│ └── $workId.tsx ← /works/[workId]
├── components/
├── main.tsx
└── routeTree.gen.ts ← 自動生成(触らない)
💡 $ で始まるファイル名が動的ルートになります: $postId.tsx は /blog/1・/blog/css-tips など、パスの一部が変動するルートです。$ の後の名前(postId)がパラメータ名になります。
3. ルーターの基本設定
3.1 main.tsxでルーターを設定する
tsx
// src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider, createRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen'; // 自動生成される
// ルーターのインスタンスを作成する
const router = createRouter( {
routeTree,
defaultPreload: 'intent', // ホバー時にプリロードする
defaultPreloadStaleTime: 0,
} );
// TypeScriptの型サポートのためにルーターの型を登録する
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}
createRoot( document.getElementById( 'root' )! ).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
);
3.2 ルートレイアウトを作る(__root.tsx)
// src/routes/__root.tsx
import { createRootRoute, Link, Outlet } from '@tanstack/react-router';
// ルートレイアウト(全ページに共通で表示される)
export const Route = createRootRoute( {
component: RootLayout,
} );
function RootLayout() {
return (
<>
{/* 全ページ共通のヘッダー */}
<header className="site-header">
<div className="header-inner">
<Link to="/" className="site-logo">
My Portfolio
</Link>
<nav className="global-nav">
<ul>
<li>
<Link
to="/"
activeProps={{ className: 'is-active' }}
activeOptions={{ exact: true }}
>
Home
</Link>
</li>
<li>
<Link
to="/about"
activeProps={{ className: 'is-active' }}
>
About
</Link>
</li>
<li>
<Link
to="/blog"
activeProps={{ className: 'is-active' }}
>
Blog
</Link>
</li>
<li>
<Link
to="/works"
activeProps={{ className: 'is-active' }}
>
Works
</Link>
</li>
</ul>
</nav>
</div>
</header>
{/* 各ページのコンテンツが表示される場所 */}
<main className="site-main">
<Outlet />
</main>
{/* 全ページ共通のフッター */}
<footer className="site-footer">
<p>© {new Date().getFullYear()} My Portfolio</p>
</footer>
</>
);
}
💡 <Outlet /> は子ルートのコンポーネントが描画される場所です: レイアウトコンポーネントの中で <Outlet /> を書いた場所に、現在のURLに対応するページコンポーネントが表示されます。
4. 各ページのルートを作る
4.1 トップページ(index.tsx)
// src/routes/index.tsx
import { createFileRoute, Link } from '@tanstack/react-router';
export const Route = createFileRoute( '/' )( {
component: HomePage,
} );
function HomePage() {
return (
<div className="home-page">
{/* ヒーローセクション */}
<section className="hero">
<h1 className="hero__title">
Web制作・React開発
</h1>
<p className="hero__subtitle">
HTML・CSS・JavaScript・WordPressから
React・TypeScriptまで幅広く対応します。
</p>
<div className="hero__actions">
<Link to="/works" className="btn btn--primary">
制作実績を見る
</Link>
<Link to="/about" className="btn btn--outline">
プロフィール
</Link>
</div>
</section>
{/* 最新ブログ */}
<section className="section">
<h2 className="section__title">最新記事</h2>
{/* ブログカードコンポーネントなどを配置 */}
<div className="posts-grid">
{/* 後ほどloader機能で取得したデータを使う */}
</div>
<Link to="/blog" className="btn btn--secondary">
記事をもっと見る
</Link>
</section>
</div>
);
}
4.2 ブログ一覧ページ(blog/index.tsx)
// src/routes/blog/index.tsx
import { createFileRoute, Link } from '@tanstack/react-router';
// loaderで非同期データを取得する
export const Route = createFileRoute( '/blog/' )( {
loader: async () => {
const res = await fetch( 'https://jsonplaceholder.typicode.com/posts?_limit=12' );
const data = await res.json() as Post[];
return { posts: data };
},
component: BlogIndexPage,
} );
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
function BlogIndexPage() {
// loaderの戻り値を型安全に取得する
const { posts } = Route.useLoaderData();
return (
<div className="blog-page">
<header className="page-header">
<h1 className="page-header__title">Blog</h1>
<p className="page-header__desc">
Web制作の学習記録を発信しています。
</p>
</header>
<div className="posts-grid">
{posts.map( post => (
<article key={post.id} className="post-card">
<Link
to="/blog/$postId"
params={{ postId: String( post.id ) }}
className="post-card__link"
>
<h2 className="post-card__title">{post.title}</h2>
<p className="post-card__excerpt">
{post.body.slice( 0, 80 )}...
</p>
</Link>
</article>
) )}
</div>
</div>
);
}
💡 TanStack Routerの loader はページ表示前にデータを取得する機能です: useEffect でデータ取得するのと違い、loader を使うとコンポーネントが表示されるときにはすでにデータが準備されています。ローディング状態の管理が不要になり、コードがシンプルになります。
4.3 ブログ個別ページ(blog/$postId.tsx)
// src/routes/blog/$postId.tsx
import { createFileRoute, Link, notFound } from '@tanstack/react-router';
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
export const Route = createFileRoute( '/blog/$postId' )( {
// loaderでURLパラメータを使ってデータを取得する
loader: async ( { params } ) => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${params.postId}`
);
if ( !res.ok ) {
// 404の場合はnotFound()を投げる
throw notFound();
}
const post = await res.json() as Post;
return { post };
},
component: BlogPostPage,
notFoundComponent: PostNotFound, // 404時に表示するコンポーネント
} );
function BlogPostPage() {
const { post } = Route.useLoaderData();
const { postId } = Route.useParams(); // URLパラメータを取得する
return (
<article className="single-post">
<header className="single-post__header">
<p className="breadcrumb">
<Link to="/">Home</Link>
{' / '}
<Link to="/blog">Blog</Link>
{' / '}
<span>記事{postId}</span>
</p>
<h1 className="single-post__title">{post.title}</h1>
</header>
<div className="single-post__content">
<p>{post.body}</p>
</div>
<nav className="post-nav">
<Link to="/blog" className="btn btn--outline">
← 記事一覧に戻る
</Link>
</nav>
</article>
);
}
function PostNotFound() {
return (
<div className="not-found">
<h2>記事が見つかりませんでした</h2>
<Link to="/blog">記事一覧に戻る</Link>
</div>
);
}
4.4 Aboutページ(about.tsx)
// src/routes/about.tsx
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute( '/about' )( {
component: AboutPage,
} );
function AboutPage() {
return (
<div className="about-page">
<h1>About</h1>
<section className="profile">
<img
src="/images/profile.jpg"
alt="プロフィール写真"
width={200}
height={200}
/>
<div className="profile__info">
<h2>山田太郎</h2>
<p>Web制作を学習中のエンジニアです。</p>
</div>
</section>
<section className="skills">
<h2>スキル</h2>
<ul className="skills__list">
{[
'HTML / CSS',
'JavaScript / TypeScript',
'React / Vite',
'WordPress',
'PHP',
].map( skill => (
<li key={skill} className="skills__item">{skill}</li>
) )}
</ul>
</section>
</div>
);
}
5. ナビゲーションと便利な機能
5.1 Linkコンポーネントの使い方
import { Link } from '@tanstack/react-router';
// 基本的なリンク
<Link to="/about">About</Link>
// 動的ルートへのリンク
<Link to="/blog/$postId" params={{ postId: String( post.id ) }}>
{post.title}
</Link>
// アクティブ時のスタイルを適用する
<Link
to="/blog"
activeProps={{ className: 'nav-link is-active' }}
inactiveProps={{ className: 'nav-link' }}
>
Blog
</Link>
// 外部リンクは通常のaタグを使う
<a href="https://github.com/username" target="_blank" rel="noopener noreferrer">
GitHub
</a>
5.2 プログラムによるナビゲーション
import { useNavigate, useRouter } from '@tanstack/react-router';
function ContactForm() {
const navigate = useNavigate();
const handleSubmit = async ( e: React.FormEvent ) => {
e.preventDefault();
// フォーム送信処理...
await submitForm( formData );
// 送信成功後にサンクスページに遷移する
navigate( { to: '/thanks' } );
};
// ...
}
// 動的ルートへの遷移
navigate( {
to: '/blog/$postId',
params: { postId: '42' },
} );
// クエリパラメータ付きで遷移する
navigate( {
to: '/blog',
search: { page: 2, category: 'css' },
} );
5.3 クエリパラメータを型安全に管理する
// src/routes/blog/index.tsx
import { createFileRoute, Link } from '@tanstack/react-router';
import { z } from 'zod'; // zodで検証する
// クエリパラメータのスキーマを定義する
const blogSearchSchema = z.object( {
page: z.number().catch( 1 ),
category: z.string().optional(),
} );
export const Route = createFileRoute( '/blog/' )( {
validateSearch: blogSearchSchema, // ← クエリパラメータを検証・型付けする
loader: async ( { context } ) => {
// searchパラメータを取得する
const { page, category } = context;
// ...
},
component: BlogIndexPage,
} );
function BlogIndexPage() {
const { page, category } = Route.useSearch(); // 型安全なsearchパラメータ
return (
<div>
<p>現在のページ:{page}</p>
{/* ページネーション */}
<Link to="/blog" search={{ page: page + 1 }}>
次のページ →
</Link>
</div>
);
}
6. ローディングとエラーの状態表示
6.1 ローディング中のスケルトン表示
// src/routes/blog/index.tsx
export const Route = createFileRoute( '/blog/' )( {
loader: async () => { /* ... */ },
pendingComponent: BlogSkeleton, // ローディング中に表示する
errorComponent: BlogError, // エラー時に表示する
component: BlogIndexPage,
} );
// ローディング中のスケルトンUI
function BlogSkeleton() {
return (
<div className="posts-grid">
{Array.from( { length: 6 } ).map( ( _, i ) => (
<div key={i} className="post-card post-card--skeleton">
<div className="skeleton skeleton--title" />
<div className="skeleton skeleton--text" />
<div className="skeleton skeleton--text" />
</div>
) )}
</div>
);
}
// エラー時のUI
function BlogError( { error }: { error: Error } ) {
return (
<div className="error-state" role="alert">
<h2>記事の読み込みに失敗しました</h2>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>
再読み込みする
</button>
</div>
);
}
css
/* スケルトンのCSS */
.post-card--skeleton {
pointer-events: none;
}
.skeleton {
background: linear-gradient(
90deg,
#1A2030 25%,
#252D40 50%,
#1A2030 75%
);
background-size: 200% 100%;
border-radius: 4px;
animation: shimmer 1.5s infinite;
}
.skeleton--title { height: 24px; width: 70%; margin-bottom: 12px; }
.skeleton--text { height: 16px; width: 90%; margin-bottom: 8px; }
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
7. 404ページを設定する
tsx
// src/routes/__root.tsx に追記する
import { createRootRoute, Outlet, Link, NotFoundRoute } from '@tanstack/react-router';
export const Route = createRootRoute( {
component: RootLayout,
notFoundComponent: NotFoundPage, // ← 404ページを設定する
} );
function NotFoundPage() {
return (
<div className="not-found-page">
<h1>404</h1>
<p>お探しのページは見つかりませんでした。</p>
<Link to="/" className="btn btn--primary">
トップページに戻る
</Link>
</div>
);
}
8. 実践:ポートフォリオのルート構成例
src/routes/
├── __root.tsx ← 共通ヘッダー・フッター・グローバルナビ
├── index.tsx ← / トップページ(ヒーロー・最新記事・制作実績)
├── about.tsx ← /about プロフィール・スキル
├── blog/
│ ├── index.tsx ← /blog ブログ記事一覧(ページネーション付き)
│ └── $postId.tsx ← /blog/1 個別記事
├── works/
│ ├── index.tsx ← /works 制作実績一覧(フィルタ付き)
│ └── $workId.tsx ← /works/portfolio-site 制作実績詳細
└── contact.tsx ← /contact お問い合わせフォーム
9. 実践チェックリスト
✓ @tanstack/react-router と @tanstack/router-plugin がインストールできたか
✓ vite.config.ts にTanStackRouterViteプラグインを追加したか
✓ src/routes/ ディレクトリが作成されて routeTree.gen.ts が自動生成されているか
✓ __root.tsx でRootLayoutと <Outlet /> を設定したか
✓ <Link to="..."> でページ遷移が正しく動作するか確認したか
✓ アクティブなリンクにスタイルが適用されているか
✓ 動的ルート($postId.tsx)で Route.useParams() からパラメータを取得できるか
✓ loader でデータを取得して Route.useLoaderData() で受け取れるか
✓ pendingComponent でローディング状態が表示されるか
✓ 存在しないURLにアクセスすると404ページが表示されるか
まとめ
今回はTanStack Routerを使ってReactアプリにページ遷移を実装する方法を解説しました。ポイントをまとめると:
- ReactのSPAではJavaScriptがURLを見てUIを切り替えるためページ移動時のサーバーリクエストが発生しない
- TanStack Routerはファイルベースのルーティングで
src/routes/のファイル構成がそのままURLになる __root.tsxでヘッダー・フッターなどの共通レイアウトを定義して<Outlet />に子ページを表示する<Link to="...">でページ遷移・activePropsでアクティブなリンクのスタイルを設定する$で始まるファイル名が動的ルートでRoute.useParams()でパラメータを取得するloaderでページ表示前にデータを取得してRoute.useLoaderData()で受け取るとuseEffectが不要になるpendingComponentでローディングUI・errorComponentでエラーUIを設定できる- クエリパラメータはZodのスキーマで検証して型安全に管理できる
次の記事では、ReactアプリのグローバルなState管理ライブラリとしてJotai(または Zustand)を使う方法を解説します。お楽しみに!
コメント