Remix の使い方 — モダンなフルスタックWebフレームワーク
一言でいうと
Remix は、Web標準(Fetch API、FormData、Response など)を基盤としたフルスタックWebフレームワークです。サーバーサイドレンダリング(SSR)、ネストされたルーティング、データローディングの最適化を中心に設計されており、高速でアクセシブルなWebアプリケーションを構築できます。
注意: Remix は v2 系以降、React Router との統合が進み、2024年後半に React Router v7 へと統合されました。本記事は
remix@2.17.4時点の情報に基づいています。新規プロジェクトでは React Router v7 の利用も検討してください。
どんな時に使う?
- SSR/SSGを活用した高パフォーマンスなWebアプリ — SEOが重要なサービスサイトやECサイトで、サーバーサイドでデータを取得しつつ高速な初期表示を実現したい場合
- フォーム処理が多い業務アプリケーション — Web標準の
<form>を活かしたプログレッシブエンハンスメントにより、JavaScript無効環境でも動作するフォーム処理を実装したい場合 - Next.js以外の選択肢を検討しているReactプロジェクト — ファイルベースルーティング・データローディング・エラーハンドリングを統合的に扱いたいが、Web標準寄りのアプローチを好む場合
インストール
Remix は create-remix CLI を使ってプロジェクトをスキャフォールドするのが推奨です。
# npx
npx create-remix@latest my-remix-app
# yarn
yarn create remix my-remix-app
# pnpm
pnpm create remix my-remix-app
既存プロジェクトに手動で追加する場合:
# npm
npm install @remix-run/node @remix-run/react @remix-run/serve
npm install -D @remix-run/dev
# yarn
yarn add @remix-run/node @remix-run/react @remix-run/serve
yarn add -D @remix-run/dev
# pnpm
pnpm add @remix-run/node @remix-run/react @remix-run/serve
pnpm add -D @remix-run/dev
補足:
remixパッケージ自体はメタパッケージ(CLI含む)です。実際のランタイムは@remix-run/node、@remix-run/reactなどのスコープ付きパッケージに分かれています。
基本的な使い方
Remix の最も基本的なパターンは、ルートファイルに loader(データ取得)と default export(UIコンポーネント)を定義する ことです。
プロジェクト構成(例)
my-remix-app/
├── app/
│ ├── root.tsx
│ └── routes/
│ ├── _index.tsx
│ └── posts.$postId.tsx
├── remix.config.js
├── package.json
└── tsconfig.json
ルートファイルの基本形(app/routes/_index.tsx)
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
// メタ情報の定義
export const meta: MetaFunction = () => {
return [
{ title: "ホーム | My Remix App" },
{ name: "description", content: "Remixで構築したWebアプリケーション" },
];
};
// サーバーサイドでのデータ取得
export async function loader({ request }: LoaderFunctionArgs) {
const posts = await fetchPostsFromDB();
return json({ posts });
}
// UIコンポーネント
export default function Index() {
const { posts } = useLoaderData<typeof loader>();
return (
<main>
<h1>最新の投稿</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
))}
</ul>
</main>
);
}
// ダミーのデータ取得関数
async function fetchPostsFromDB() {
return [
{ id: "1", title: "Remixを始めよう" },
{ id: "2", title: "loaderとactionの使い分け" },
{ id: "3", title: "ネストルーティングの威力" },
];
}
よく使うAPI
1. loader — サーバーサイドでのデータ取得
GETリクエスト時にサーバーで実行される関数です。useLoaderData でクライアント側からデータを取得します。
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export async function loader({ params, request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const query = url.searchParams.get("q") ?? "";
const post = await db.post.findUnique({
where: { id: params.postId },
});
if (!post) {
throw new Response("Not Found", { status: 404 });
}
return json({ post, query });
}
export default function PostDetail() {
const { post, query } = useLoaderData<typeof loader>();
return <article><h1>{post.title}</h1></article>;
}
2. action — フォーム送信・ミューテーション処理
POST/PUT/PATCH/DELETEリクエスト時にサーバーで実行される関数です。Web標準の FormData をそのまま扱えます。
import type { ActionFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react";
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const title = formData.get("title");
const body = formData.get("body");
// バリデーション
const errors: Record<string, string> = {};
if (!title || typeof title !== "string") {
errors.title = "タイトルは必須です";
}
if (!body || typeof body !== "string") {
errors.body = "本文は必須です";
}
if (Object.keys(errors).length > 0) {
return json({ errors }, { status: 400 });
}
await db.post.create({ data: { title: String(title), body: String(body) } });
return redirect("/posts");
}
export default function NewPost() {
const actionData = useActionData<typeof action>();
return (
<Form method="post">
<div>
<label htmlFor="title">タイトル</label>
<input id="title" name="title" type="text" />
{actionData?.errors?.title && (
<p className="error">{actionData.errors.title}</p>
)}
</div>
<div>
<label htmlFor="body">本文</label>
<textarea id="body" name="body" />
{actionData?.errors?.body && (
<p className="error">{actionData.errors.body}</p>
)}
</div>
<button type="submit">投稿する</button>
</Form>
);
}
3. ErrorBoundary — ルート単位のエラーハンドリング
各ルートファイルに ErrorBoundary をエクスポートすると、そのルート内で発生したエラーをキャッチして表示できます。ネストされたルーティングと組み合わせることで、エラーの影響範囲を最小限に抑えられます。
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div className="error-container">
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</div>
);
}
// 予期しないエラー
const errorMessage = error instanceof Error ? error.message : "不明なエラー";
return (
<div className="error-container">
<h1>エラーが発生しました</h1>
<p>{errorMessage}</p>
</div>
);
}
4. useFetcher — ナビゲーションなしのデータ操作
ページ遷移を伴わずにデータの取得・送信を行いたい場合に使います。「いいね」ボタンや自動保存、検索サジェストなどに最適です。
import { useFetcher } from "@remix-run/react";
export default function LikeButton({ postId }: { postId: string }) {
const fetcher = useFetcher();
const isSubmitting = fetcher.state === "submitting";
return (
<fetcher.Form method="post" action="/api/like">
<input type="hidden" name="postId" value={postId} />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "送信中..." : "♥ いいね"}
</button>
</fetcher.Form>
);
}
5. defer + Await — ストリーミングレスポンス
重い処理を遅延ロードし、先にレンダリング可能な部分を表示できます。
import { defer } from "@remix-run/node";
import { Await, useLoaderData } from "@remix-run/react";
import { Suspense } from "react";
export async function loader() {
// 即座に返すデータ
const criticalData = await fetchCriticalData();
// 遅延させるデータ(awaitしない)
const slowDataPromise = fetchSlowAnalytics();
return defer({
criticalData,
slowData: slowDataPromise,
});
}
export default function Dashboard() {
const { criticalData, slowData } = useLoaderData<typeof loader>();
return (
<div>
<h1>ダッシュボード</h1>
<section>
<h2>概要</h2>
<p>{criticalData.summary}</p>
</section>
<section>
<h2>分析データ</h2>
<Suspense fallback={<p>読み込み中...</p>}>
<Await resolve={slowData} errorElement={<p>読み込みに失敗しました</p>}>
{(resolvedData) => (
<ul>
{resolvedData.items.map((item: any) => (
<li key={item.id}>{item.label}: {item.value}</li>
))}
</ul>
)}
</Await>
</Suspense>
</section>
</div>
);
}
類似パッケージとの比較
| 特徴 | Remix | Next.js | Astro |
|---|---|---|---|
| レンダリング方式 | SSR中心 | SSR / SSG / ISR / RSC | SSG中心(SSR対応) |
| ルーティング | ファイルベース(ネスト対応) | ファイルベース(App Router / Pages Router) | ファイルベース |
| データ取得 | loader / action(Web標準) | fetch / Server Actions / getServerSideProps | フロントマター / API Routes |
| フォーム処理 | Web標準 <Form> + action | Server Actions / API Routes | フレームワーク非依存 |
| React Server Components | 非対応(v2時点) | 対応(App Router) | 非対応 |
| デプロイ先 | Node.js / Cloudflare / Deno / Vercel 等 | Vercel最適化 / セルフホスト可 | 各種アダプター対応 |
| 設計思想 | Web標準準拠・プログレッシブエンハンスメント | フルスタック・エッジ最適化 | コンテンツ中心・ゼロJS |
注意点・Tips
1. remix パッケージと @remix-run/* の関係
remix パッケージはメタパッケージであり、実際のコードは @remix-run/node、@remix-run/react、@remix-run/serve などに分かれています。import は必ずスコープ付きパッケージから行ってください。
// ✅ 正しい
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
// ❌ 間違い
import { json } from "remix";
2. React Router v7 への移行
Remix v2 は React Router v7 に統合されました。新規プロジェクトでは react-router v7 を使うことが推奨されています。既存の Remix v2 プロジェクトからの移行ガイドが公式ドキュメントに用意されています。
3. loader と action はサーバー専用
loader と action はサーバーサイドでのみ実行されます。データベース接続やAPIキーなどの機密情報を安全に扱えますが、クライアントバンドルに含まれないことを前提にコードを書く必要があります。*.server.ts ファイルを活用すると、サーバー専用コードの境界を明確にできます。
// app/utils/db.server.ts — サーバー専用モジュール
import { PrismaClient } from "@prisma/client";
let db: