Remix の仕組みを図解で理解:ルーティング/データロード/アクションの全体像
Remix は、モダンな Web アプリケーション開発を革新するフルスタックフレームワークとして注目を集めていますね。 従来の React 開発では、ルーティング、データフェッチ、状態管理を別々のライブラリで組み合わせる必要がありましたが、Remix はこれらを統合的に提供します。
本記事では、Remix の核となる「ルーティング」「データロード」「アクション」の 3 つの仕組みを、図解を交えながら詳しく解説していきます。 初心者の方でも理解できるよう、段階的に説明していきますので、ぜひ最後までお付き合いください。
背景
Remix が生まれた理由
React エコシステムにおいて、長年にわたり開発者は複数のライブラリを組み合わせてアプリケーションを構築してきました。 ルーティングには React Router、データフェッチには axios や fetch、状態管理には Redux や Context API など、それぞれ異なるツールを使用する必要がありました。
この状況には以下のような課題がありましたね。
- ライブラリ間の統合に手間がかかる
- データフェッチとルーティングの連携が複雑になる
- サーバーサイドレンダリング(SSR)の実装が困難
- パフォーマンス最適化に高度な知識が必要
Remix は、これらの課題を解決するために、React Router の開発者たちによって設計されました。
Remix のアーキテクチャ哲学
Remix は「Web 標準」を重視したフレームワークです。 ブラウザの標準機能である HTTP メソッド、Form、URL などを活用することで、シンプルで堅牢なアプリケーション開発を実現しています。
以下の図は、Remix のアーキテクチャ全体像を示しています。
mermaidflowchart TB
browser["ブラウザ<br/>(ユーザー)"]
subgraph remix["Remix アプリケーション"]
router["ルーター<br/>(ファイルベース)"]
loader["loader関数<br/>(データ取得)"]
action["action関数<br/>(データ変更)"]
component["Reactコンポーネント<br/>(UI表示)"]
end
subgraph backend["バックエンド"]
api["API"]
db[("データベース")]
end
browser -->|"GET リクエスト"| router
browser -->|"POST リクエスト<br/>(Form送信)"| router
router -->|"URL解析"| loader
router -->|"Form処理"| action
loader -->|"データ取得"| api
action -->|"データ更新"| api
api <-->|"CRUD操作"| db
loader -->|"データ提供"| component
action -->|"処理結果"| component
component -->|"HTML/JSON"| browser
この図から、Remix がリクエストを受け取ってからレスポンスを返すまでの基本的な流れが理解できますね。
図で理解できる要点:
- ブラウザからのリクエストは必ずルーターを経由する
- GET リクエストは loader 関数で処理され、POST リクエストは action 関数で処理される
- 各関数はバックエンド API と連携してデータベースとやり取りする
課題
従来の React 開発における課題
Remix が解決しようとしている具体的な課題を見ていきましょう。
データフェッチのタイミング問題
従来の React 開発では、コンポーネントがマウントされた後にuseEffectでデータをフェッチするのが一般的でした。
これには以下のような問題がありましたね。
| # | 課題 | 影響 |
|---|---|---|
| 1 | 初回レンダリング時にローディング状態が表示される | ユーザー体験の低下 |
| 2 | データ取得が遅延する(ウォーターフォール問題) | パフォーマンスの悪化 |
| 3 | SEO 対策が困難 | 検索エンジンでの評価低下 |
| 4 | エラーハンドリングが複雑化 | 開発コストの増加 |
状態管理の複雑さ
クライアントサイドで状態管理を行う場合、以下のような課題に直面します。
typescript// 従来の React でのデータフェッチ例
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// データフェッチ処理
fetchUser(userId)
.then((data) => setUser(data))
.catch((err) => setError(err))
.finally(() => setLoading(false));
}, [userId]);
// ローディング、エラー、成功状態の管理が必要
if (loading) return <div>読み込み中...</div>;
if (error) return <div>エラーが発生しました</div>;
if (!user) return <div>ユーザーが見つかりません</div>;
return <div>{user.name}</div>;
}
このコードでは、loading、error、userという 3 つの状態を手動で管理する必要があります。
アプリケーションが大きくなるにつれて、この状態管理は複雑になっていきますね。
フォーム処理の煩雑さ
従来の React 開発では、フォームの送信処理も複雑でした。
typescript// 従来の React でのフォーム処理例
function CreatePost() {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setSubmitting(true);
try {
// API呼び出し
await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
});
// 成功後の処理
setTitle('');
setContent('');
} catch (error) {
console.error(error);
} finally {
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
{/* フォーム要素 */}
</form>
);
}
各入力フィールドの状態管理、送信中の状態管理、エラーハンドリングなど、多くのボイラープレートコードが必要になります。
以下の図は、従来の React 開発におけるデータフローの課題を示しています。
mermaidsequenceDiagram
participant User as ユーザー
participant Browser as ブラウザ
participant React as React<br/>コンポーネント
participant API as API
User->>Browser: ページアクセス
Browser->>React: 初回レンダリング
React->>Browser: ローディング表示
Note over React,API: コンポーネントマウント後に<br/>データフェッチ開始
React->>API: データリクエスト
API-->>React: データレスポンス
React->>Browser: データ表示
Note over User,Browser: 初回表示まで時間がかかる
この図から、従来の方法では初回レンダリングとデータフェッチが分離されているため、ユーザーが実際のコンテンツを見るまでに時間がかかることがわかりますね。
図で理解できる要点:
- コンポーネントのマウント後にデータフェッチが開始される
- ローディング状態の表示期間が長くなる
- データ取得の遅延によりユーザー体験が低下する
解決策
Remix の統合的アプローチ
Remix は、ルーティング、データロード、アクションを統合することで、これらの課題を解決します。 それぞれの仕組みを詳しく見ていきましょう。
ファイルベースルーティング
Remix では、ファイルシステムがそのままルーティング構造になります。 これにより、直感的でメンテナンスしやすいルート管理が可能になりますね。
ルーティングの基本構造
bashapp/
routes/
_index.tsx → /
about.tsx → /about
blog._index.tsx → /blog
blog.$slug.tsx → /blog/:slug
users.$userId.tsx → /users/:userId
このファイル構造から、URL パスが自動的に生成されます。
$で始まる部分は動的パラメータとして扱われます。
ネストされたルーティング
Remix の強力な機能の 1 つが、ネストされたルーティングです。
bashapp/
routes/
dashboard.tsx → /dashboard (親レイアウト)
dashboard._index.tsx → /dashboard (トップページ)
dashboard.settings.tsx → /dashboard/settings
dashboard.profile.tsx → /dashboard/profile
以下の図は、ネストされたルーティングの構造を示しています。
mermaidflowchart TD
root["ルート<br/>(root.tsx)"]
dashboard["ダッシュボード<br/>(dashboard.tsx)"]
index["インデックス<br/>(dashboard._index.tsx)"]
settings["設定<br/>(dashboard.settings.tsx)"]
profile["プロフィール<br/>(dashboard.profile.tsx)"]
root --> dashboard
dashboard --> index
dashboard --> settings
dashboard --> profile
style root fill:#e1f5ff
style dashboard fill:#fff4e1
style index fill:#f0f0f0
style settings fill:#f0f0f0
style profile fill:#f0f0f0
親ルート(dashboard.tsx)は子ルート全体で共有されるレイアウトとして機能します。
図で理解できる要点:
- ルートは階層構造を持つ
- 親ルートは子ルート全体のレイアウトを提供する
- 各子ルートは独立したコンテンツを持つ
ルートコンポーネントの実装
typescript// app/routes/dashboard.tsx
import { Outlet } from '@remix-run/react';
// 親ルート: ダッシュボード全体のレイアウト
export default function Dashboard() {
return (
<div className='dashboard-layout'>
<header>
<h1>ダッシュボード</h1>
<nav>{/* ナビゲーションメニュー */}</nav>
</header>
{/* 子ルートがここに表示される */}
<Outlet />
</div>
);
}
Outletコンポーネントは、子ルートのコンテンツが表示される場所を指定します。
この仕組みにより、レイアウトの再利用が簡単になりますね。
loader 関数によるデータロード
Remix のloader関数は、ルートコンポーネントがレンダリングされる前にサーバーサイドでデータを取得します。
loader 関数の基本
typescript// app/routes/users.$userId.tsx
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import type { LoaderFunctionArgs } from '@remix-run/node';
// サーバーサイドで実行されるデータ取得関数
export async function loader({
params,
}: LoaderFunctionArgs) {
const userId = params.userId;
// データベースまたはAPIからユーザー情報を取得
const user = await db.user.findUnique({
where: { id: userId },
});
if (!user) {
// ユーザーが見つからない場合は404エラー
throw new Response('Not Found', { status: 404 });
}
// JSON形式でデータを返す
return json({ user });
}
このloader関数は、ルートにアクセスされたときに自動的に実行されます。
パラメータやクエリ文字列はparamsやrequestオブジェクトから取得できますね。
loader データの使用
typescript// 同じファイル内のコンポーネント
export default function UserProfile() {
// loaderから返されたデータを取得
const { user } = useLoaderData<typeof loader>();
return (
<div className='user-profile'>
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>登録日: {user.createdAt}</p>
</div>
);
}
useLoaderDataフックを使用することで、loader関数から返されたデータに型安全にアクセスできます。
従来のuseStateやuseEffectによるデータ管理が不要になりますね。
複数の loader の並列実行
ネストされたルートでは、各ルートのloaderが並列に実行されます。
mermaidsequenceDiagram
participant Browser as ブラウザ
participant Remix as Remix<br/>サーバー
participant Loader1 as loader<br/>(親ルート)
participant Loader2 as loader<br/>(子ルート)
participant DB as データベース
Browser->>Remix: /dashboard/profile<br/>アクセス
par 並列実行
Remix->>Loader1: 親ルートのloader実行
Loader1->>DB: ダッシュボード情報取得
DB-->>Loader1: データ返却
Remix->>Loader2: 子ルートのloader実行
Loader2->>DB: プロフィール情報取得
DB-->>Loader2: データ返却
end
Loader1-->>Remix: ダッシュボードデータ
Loader2-->>Remix: プロフィールデータ
Remix->>Browser: 完全なHTMLレスポンス
この並列実行により、ウォーターフォール問題が解消され、パフォーマンスが大幅に向上します。
図で理解できる要点:
- 親ルートと子ルートの loader は並列に実行される
- データ取得の待ち時間が最小化される
- 完全なデータを含む HTML がレンダリングされる
エラーハンドリング
typescript// app/routes/users.$userId.tsx
import {
isRouteErrorResponse,
useRouteError,
} from '@remix-run/react';
// エラーバウンダリーコンポーネント
export function ErrorBoundary() {
const error = useRouteError();
// HTTPエラーレスポンスの場合
if (isRouteErrorResponse(error)) {
return (
<div className='error-container'>
<h1>
{error.status} {error.statusText}
</h1>
<p>{error.data}</p>
</div>
);
}
// その他のエラーの場合
return (
<div className='error-container'>
<h1>予期しないエラーが発生しました</h1>
<p>申し訳ございませんが、問題が発生しました。</p>
</div>
);
}
ErrorBoundaryをエクスポートすることで、ルートレベルでエラーハンドリングを一元管理できます。
action 関数によるデータ変更
action関数は、フォーム送信やデータ変更を処理するためのサーバーサイド関数です。
action 関数の基本
typescript// app/routes/posts.new.tsx
import { json, redirect } from '@remix-run/node';
import type { ActionFunctionArgs } from '@remix-run/node';
// POSTリクエストを処理する関数
export async function action({
request,
}: ActionFunctionArgs) {
// フォームデータを取得
const formData = await request.formData();
const title = formData.get('title');
const content = formData.get('content');
// バリデーション
const errors: { title?: string; content?: string } = {};
if (!title || typeof title !== 'string') {
errors.title = 'タイトルは必須です';
}
if (!content || typeof content !== 'string') {
errors.content = '内容は必須です';
}
// エラーがある場合は戻す
if (Object.keys(errors).length > 0) {
return json({ errors }, { status: 400 });
}
// データベースに保存
const post = await db.post.create({
data: { title, content },
});
// 作成した投稿ページにリダイレクト
return redirect(`/posts/${post.id}`);
}
action関数は、POST、PUT、PATCH、DELETEなどの HTTP メソッドで呼び出されます。
フォームから送信されたデータはrequest.formData()で取得できますね。
Form コンポーネントの使用
typescript// 同じファイル内のコンポーネント
import { Form, useActionData } from '@remix-run/react';
export default function NewPost() {
// actionから返されたデータ(エラーなど)を取得
const actionData = useActionData<typeof action>();
return (
<div className='new-post'>
<h1>新しい投稿を作成</h1>
{/* Remix の Form コンポーネント */}
<Form method='post'>
<div className='form-group'>
<label htmlFor='title'>タイトル</label>
<input
type='text'
id='title'
name='title'
required
/>
{/* エラーメッセージの表示 */}
{actionData?.errors?.title && (
<p className='error'>
{actionData.errors.title}
</p>
)}
</div>
<div className='form-group'>
<label htmlFor='content'>内容</label>
<textarea id='content' name='content' required />
{actionData?.errors?.content && (
<p className='error'>
{actionData.errors.content}
</p>
)}
</div>
<button type='submit'>投稿する</button>
</Form>
</div>
);
}
Remix のFormコンポーネントを使用すると、JavaScript が無効でもフォームが動作します。
これはプログレッシブエンハンスメントの原則に基づいていますね。
action 実行のフロー
以下の図は、フォーム送信時の action 実行フローを示しています。
mermaidstateDiagram-v2
[*] --> Idle: フォーム表示
Idle --> Submitting: フォーム送信
Submitting --> Processing: action関数実行
Processing --> Validation: データ検証
Validation --> Error: 検証失敗
Validation --> Saving: 検証成功
Error --> Idle: エラー表示
Saving --> Success: データ保存完了
Success --> Redirect: リダイレクト
Redirect --> [*]
note right of Processing
サーバーサイドで
セキュアに処理
end note
note right of Error
バリデーション
エラーを返却
end note
この状態遷移図から、action の実行プロセスが段階的に理解できますね。
図で理解できる要点:
- フォーム送信は常にサーバーサイドで処理される
- バリデーションエラーは適切にハンドリングされる
- 成功時はリダイレクトにより新しいページに遷移する
楽観的 UI 更新
Remix では、useNavigationやuseFetcherを使用して楽観的 UI 更新も実装できます。
typescriptimport { useFetcher } from '@remix-run/react';
export default function LikeButton({
postId,
initialLikes,
}) {
const fetcher = useFetcher();
// 楽観的に更新された値を計算
const likes = fetcher.formData
? initialLikes + 1
: initialLikes;
// 送信中かどうかを判定
const isLiking = fetcher.state === 'submitting';
return (
<fetcher.Form
method='post'
action={`/posts/${postId}/like`}
>
<button type='submit' disabled={isLiking}>
♥ {likes}
</button>
</fetcher.Form>
);
}
useFetcherを使用すると、ページ遷移なしで action を実行できます。
ボタンをクリックした瞬間に楽観的に UI が更新され、ユーザー体験が向上しますね。
具体例
実際のアプリケーション開発で Remix の仕組みを活用する具体例を見ていきましょう。
ブログアプリケーションの実装
ブログアプリケーションを例に、ルーティング、loader、action の連携を説明します。
アプリケーション構造
bashapp/
routes/
_index.tsx → / (トップページ)
blog._index.tsx → /blog (記事一覧)
blog.$slug.tsx → /blog/:slug (記事詳細)
blog.new.tsx → /blog/new (新規作成)
blog.$slug.edit.tsx → /blog/:slug/edit (編集)
以下の図は、ブログアプリケーション全体のデータフローを示しています。
mermaidflowchart TD
user["ユーザー"]
subgraph routes["ルート構成"]
index["トップページ<br/>(_index.tsx)"]
list["記事一覧<br/>(blog._index.tsx)"]
detail["記事詳細<br/>(blog.$slug.tsx)"]
new_post["新規作成<br/>(blog.new.tsx)"]
edit["編集<br/>(blog.$slug.edit.tsx)"]
end
subgraph data_ops["データ操作"]
load_list["loader<br/>(記事リスト取得)"]
load_detail["loader<br/>(記事詳細取得)"]
create["action<br/>(記事作成)"]
update["action<br/>(記事更新)"]
end
db[("データベース")]
user -->|"GET /blog"| list
user -->|"GET /blog/hello"| detail
user -->|"POST /blog/new"| new_post
user -->|"POST /blog/hello/edit"| edit
list --> load_list
detail --> load_detail
new_post --> create
edit --> update
load_list <--> db
load_detail <--> db
create --> db
update --> db
create -->|"redirect"| detail
update -->|"redirect"| detail
この図から、各ルートが適切な loader/action と連携していることがわかりますね。
図で理解できる要点:
- GET リクエストは loader でデータを取得する
- POST リクエストは action でデータを変更する
- 変更後は該当ページにリダイレクトする
記事一覧ページの実装
typescript// app/routes/blog._index.tsx
import { json } from '@remix-run/node';
import { Link, useLoaderData } from '@remix-run/react';
// 記事リストを取得するloader
export async function loader() {
const posts = await db.post.findMany({
select: {
id: true,
slug: true,
title: true,
excerpt: true,
createdAt: true,
},
orderBy: { createdAt: 'desc' },
});
return json({ posts });
}
ここでは、データベースから記事リストを取得し、JSON 形式で返しています。
必要な項目のみをselectで指定することで、パフォーマンスを最適化していますね。
typescript// コンポーネント部分
export default function BlogIndex() {
const { posts } = useLoaderData<typeof loader>();
return (
<div className='blog-index'>
<header>
<h1>ブログ記事一覧</h1>
<Link to='/blog/new' className='button'>
新規記事を作成
</Link>
</header>
<div className='posts-grid'>
{posts.map((post) => (
<article key={post.id} className='post-card'>
<h2>
<Link to={`/blog/${post.slug}`}>
{post.title}
</Link>
</h2>
<p className='excerpt'>{post.excerpt}</p>
<time dateTime={post.createdAt}>
{new Date(post.createdAt).toLocaleDateString(
'ja-JP'
)}
</time>
</article>
))}
</div>
</div>
);
}
loader から取得したデータを使って、記事カードを表示しています。
Linkコンポーネントによる遷移は、JavaScript によるクライアントサイドナビゲーションとして動作しますね。
記事詳細ページの実装
typescript// app/routes/blog.$slug.tsx
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import type { LoaderFunctionArgs } from '@remix-run/node';
// 記事詳細を取得するloader
export async function loader({
params,
}: LoaderFunctionArgs) {
const { slug } = params;
const post = await db.post.findUnique({
where: { slug },
include: {
author: true,
tags: true,
},
});
if (!post) {
throw new Response('記事が見つかりません', {
status: 404,
});
}
return json({ post });
}
URL パラメータ(slug)を使用して、特定の記事を取得しています。
記事が存在しない場合は 404 エラーをスローすることで、適切なエラーページが表示されますね。
typescript// コンポーネント部分
export default function BlogPost() {
const { post } = useLoaderData<typeof loader>();
return (
<article className='blog-post'>
<header>
<h1>{post.title}</h1>
<div className='post-meta'>
<span className='author'>{post.author.name}</span>
<time dateTime={post.createdAt}>
{new Date(post.createdAt).toLocaleDateString(
'ja-JP'
)}
</time>
</div>
<div className='tags'>
{post.tags.map((tag) => (
<span key={tag.id} className='tag'>
{tag.name}
</span>
))}
</div>
</header>
<div
className='post-content'
dangerouslySetInnerHTML={{ __html: post.content }}
/>
<footer>
<Link
to={`/blog/${post.slug}/edit`}
className='button'
>
編集する
</Link>
</footer>
</article>
);
}
記事の本文、著者情報、タグなど、関連データも含めて表示しています。
記事作成ページの実装
typescript// app/routes/blog.new.tsx
import { json, redirect } from '@remix-run/node';
import {
Form,
useActionData,
useNavigation,
} from '@remix-run/react';
import type { ActionFunctionArgs } from '@remix-run/node';
// バリデーション関数
function validatePost(title: string, content: string) {
const errors: { title?: string; content?: string } = {};
if (!title || title.trim().length === 0) {
errors.title = 'タイトルを入力してください';
} else if (title.length > 100) {
errors.title =
'タイトルは100文字以内で入力してください';
}
if (!content || content.trim().length === 0) {
errors.content = '本文を入力してください';
} else if (content.length < 50) {
errors.content = '本文は50文字以上で入力してください';
}
return Object.keys(errors).length > 0 ? errors : null;
}
まず、バリデーション関数を定義しています。 ビジネスロジックを関数として分離することで、テストやメンテナンスが容易になりますね。
typescript// 記事を作成するaction
export async function action({
request,
}: ActionFunctionArgs) {
const formData = await request.formData();
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// バリデーション
const errors = validatePost(title, content);
if (errors) {
return json({ errors }, { status: 400 });
}
// スラッグの生成
const slug = title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
// データベースに保存
const post = await db.post.create({
data: {
title,
content,
slug,
excerpt: content.substring(0, 150),
},
});
// 作成した記事ページにリダイレクト
return redirect(`/blog/${post.slug}`);
}
action では、フォームデータの検証、スラッグの生成、データベースへの保存を行っています。 すべての処理が完了したら、作成した記事の詳細ページにリダイレクトしますね。
typescript// コンポーネント部分
export default function NewPost() {
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
// フォーム送信中かどうかを判定
const isSubmitting = navigation.state === 'submitting';
return (
<div className='new-post'>
<h1>新しい記事を作成</h1>
<Form method='post' className='post-form'>
<div className='form-group'>
<label htmlFor='title'>タイトル</label>
<input
type='text'
id='title'
name='title'
placeholder='記事のタイトルを入力'
aria-invalid={
actionData?.errors?.title ? true : undefined
}
disabled={isSubmitting}
/>
{actionData?.errors?.title && (
<p className='error' role='alert'>
{actionData.errors.title}
</p>
)}
</div>
<div className='form-group'>
<label htmlFor='content'>本文</label>
<textarea
id='content'
name='content'
rows={15}
placeholder='記事の本文を入力'
aria-invalid={
actionData?.errors?.content ? true : undefined
}
disabled={isSubmitting}
/>
{actionData?.errors?.content && (
<p className='error' role='alert'>
{actionData.errors.content}
</p>
)}
</div>
<div className='form-actions'>
<button
type='submit'
className='button primary'
disabled={isSubmitting}
>
{isSubmitting ? '投稿中...' : '投稿する'}
</button>
<Link to='/blog' className='button secondary'>
キャンセル
</Link>
</div>
</Form>
</div>
);
}
useNavigationフックを使用して、フォームの送信状態を取得しています。
送信中はボタンを disable にし、適切なフィードバックを提供することで、ユーザー体験が向上しますね。
データの再検証(Revalidation)
Remix の優れた機能の 1 つが、自動的なデータの再検証です。
再検証のタイミング
以下の表は、Remix が自動的にデータを再検証するタイミングをまとめています。
| # | タイミング | 説明 | 再検証対象 |
|---|---|---|---|
| 1 | action 実行後 | フォーム送信などの action 完了時 | 現在のページの全 loader |
| 2 | URL パラメータ変更 | /blog/post-1 から /blog/post-2 への遷移時 | パラメータを使用する loader |
| 3 | 手動再検証 | useRevalidatorフック使用時 | 現在のページの全 loader |
| 4 | フォーカス復帰 | ブラウザタブにフォーカスが戻った時(オプション) | 指定した loader |
この自動再検証により、常に最新のデータが表示されることが保証されますね。
mermaidsequenceDiagram
participant User as ユーザー
participant Form as フォーム
participant Action as action
participant Loader as loader
participant UI as UI表示
User->>Form: 記事を投稿
Form->>Action: POST送信
Action->>Action: データ保存
Action-->>Form: 成功レスポンス
Note over Action,Loader: action完了後に<br/>自動再検証
Form->>Loader: loader再実行
Loader->>Loader: 最新データ取得
Loader-->>UI: 更新されたデータ
UI->>User: 最新の記事一覧表示
この図から、action の実行後に自動的に loader が再実行され、UI が更新されることがわかりますね。
図で理解できる要点:
- action 完了後は自動的に loader が再実行される
- ユーザーは常に最新のデータを見ることができる
- 開発者は手動でデータ更新処理を書く必要がない
手動での再検証
typescriptimport { useRevalidator } from '@remix-run/react';
export default function Dashboard() {
const revalidator = useRevalidator();
const handleRefresh = () => {
// 手動でデータを再検証
revalidator.revalidate();
};
return (
<div>
<button
onClick={handleRefresh}
disabled={revalidator.state === 'loading'}
>
{revalidator.state === 'loading'
? '更新中...'
: 'データを更新'}
</button>
</div>
);
}
useRevalidatorフックを使用すると、任意のタイミングでデータを再検証できます。
エラーハンドリングの実装
Remix では、エラーハンドリングを階層的に実装できます。
ルートレベルのエラーハンドリング
typescript// app/routes/blog.$slug.tsx
import {
isRouteErrorResponse,
useRouteError,
} from '@remix-run/react';
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
// HTTPステータスコードに応じた処理
if (error.status === 404) {
return (
<div className='error-404'>
<h1>記事が見つかりません</h1>
<p>
お探しの記事は存在しないか、削除された可能性があります。
</p>
<Link to='/blog' className='button'>
記事一覧に戻る
</Link>
</div>
);
}
if (error.status === 500) {
return (
<div className='error-500'>
<h1>サーバーエラーが発生しました</h1>
<p>
一時的な問題が発生している可能性があります。
</p>
<p>しばらく待ってから再度お試しください。</p>
</div>
);
}
}
// その他の予期しないエラー
return (
<div className='error-unknown'>
<h1>予期しないエラーが発生しました</h1>
<p>申し訳ございません。問題が発生しました。</p>
<details>
<summary>エラー詳細</summary>
<pre>
{error instanceof Error
? error.message
: 'Unknown error'}
</pre>
</details>
</div>
);
}
各ルートにErrorBoundaryを実装することで、そのルート固有のエラーハンドリングが可能になります。
エラーが発生しても、アプリケーション全体がクラッシュすることなく、適切なエラーメッセージが表示されますね。
グローバルエラーハンドリング
typescript// app/root.tsx
export function ErrorBoundary() {
const error = useRouteError();
return (
<html lang='ja'>
<head>
<meta charSet='utf-8' />
<meta
name='viewport'
content='width=device-width, initial-scale=1'
/>
<title>エラーが発生しました</title>
</head>
<body>
<div className='global-error'>
<h1>アプリケーションエラー</h1>
<p>予期しない問題が発生しました。</p>
{process.env.NODE_ENV === 'development' && (
<details>
<summary>開発者向け情報</summary>
<pre>
{error instanceof Error
? error.stack
: JSON.stringify(error, null, 2)}
</pre>
</details>
)}
</div>
</body>
</html>
);
}
ルートレベルでキャッチされなかったエラーは、グローバルなErrorBoundaryで処理されます。
まとめ
本記事では、Remix の核となる 3 つの仕組み「ルーティング」「データロード」「アクション」について、図解を交えながら詳しく解説してきました。
Remix の主要な特徴
Remix は、以下のような特徴により、モダンな Web 開発を実現します。
ファイルベースルーティング:
- 直感的なファイル構造によるルート定義
- ネストされたルートによるレイアウト共有
- 動的パラメータのサポート
loader 関数によるデータロード:
- サーバーサイドでのデータ取得
- 並列実行によるパフォーマンス最適化
- 型安全なデータアクセス
action 関数によるデータ変更:
- Web 標準に基づいたフォーム処理
- サーバーサイドでのバリデーション
- 自動的なデータ再検証
Remix を使うメリット
従来の React 開発と比較して、Remix は以下のようなメリットを提供しますね。
| # | 項目 | 従来の方法 | Remix |
|---|---|---|---|
| 1 | データフェッチ | クライアントサイド(useEffect) | サーバーサイド(loader) |
| 2 | 状態管理 | 手動管理が必要 | 自動管理 |
| 3 | フォーム処理 | 複雑なコード | シンプルな宣言的記述 |
| 4 | エラーハンドリング | コンポーネントごとに実装 | 階層的な一元管理 |
| 5 | パフォーマンス | ウォーターフォール問題 | 並列データフェッチ |
| 6 | SEO 対応 | 追加設定が必要 | デフォルトで SSR |
今後の学習
Remix の仕組みを理解したら、次のステップとして以下のトピックに取り組むことをお勧めします。
認証・認可:
- セッション管理の実装
- ログイン/ログアウト機能
- 保護されたルートの作成
パフォーマンス最適化:
- キャッシュ戦略の設定
- プリフェッチの活用
- レスポンスの最適化
デプロイメント:
- 各種プラットフォームへのデプロイ方法
- 環境変数の管理
- 本番環境の設定
Remix は、Web 標準を重視しながら、開発者体験とユーザー体験の両方を向上させる優れたフレームワークです。 本記事で学んだ基本的な仕組みを土台に、さらに高度な機能にもチャレンジしていただければと思います。
これから Remix を使った Web 開発を始める皆さまの一助となれば幸いです。
関連リンク
articleRemix の仕組みを図解で理解:ルーティング/データロード/アクションの全体像
articleRemix で「Hydration failed」を解決:サーバ/クライアント不整合の診断手順
articleRemix 本番運用チェックリスト:ビルド・監視・バックアップ・脆弱性対応
articleRemix で管理画面テンプレ:表・フィルタ・CSV エクスポートの鉄板構成
articleRemix でブログをゼロから構築:Markdown・検索・タグ・OGP まで実装
articleRemix でスケーラブルなディレクトリ設計:routes/リソース/ユーティリティ分割
articleVite プラグインフック対応表:Rollup → Vite マッピング早見表
articleNestJS Monorepo 構築:Nx/Yarn Workspaces で API・Lib を一元管理
articleTypeScript Project References 入門:大規模 Monorepo で高速ビルドを実現する設定手順
articleMySQL Router セットアップ完全版:アプリからの透過フェイルオーバーを実現
articletRPC アーキテクチャ設計:BFF とドメイン分割で肥大化を防ぐルータ戦略
articleMotion(旧 Framer Motion)× TypeScript:Variant 型と Props 推論を強化する設定レシピ
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来