Remixフレームワーク完全ガイド - Next.jsとの違いと実践的な使い方【2026年版】


Remixは、Reactをベースとしたフルスタックフレームワークで、Webの基本に立ち返った設計思想が特徴です。2022年にオープンソース化され、2026年現在ではShopifyに買収されて開発が加速しています。

Remixの特徴

1. Webプラットフォームファースト

RemixはWeb標準を最大限活用します。独自APIを最小限に抑え、ブラウザネイティブの機能を優先的に使用します。

Web標準の活用例:

  • FormData API(フォーム処理)
  • Request/Response API(データ取得)
  • URLSearchParams(検索パラメータ)

2. ネストされたルーティング

Remixの最大の特徴は、UI階層とルート階層が一致する「ネストルーティング」です。

app/
  routes/
    _index.tsx          # /
    about.tsx           # /about
    blog._index.tsx     # /blog
    blog.$slug.tsx      # /blog/awesome-post
    dashboard.tsx       # /dashboard (レイアウト)
    dashboard._index.tsx # /dashboard (コンテンツ)
    dashboard.settings.tsx # /dashboard/settings

3. サーバーとクライアントのシームレスな統合

loaderとactionを使って、サーバーサイドとクライアントサイドのコードを同じファイルに書けます。

Next.jsとの違い

項目RemixNext.js
データ取得loader関数getServerSideProps等
フォーム処理action + FormDataAPI Routes + useState
ルーティングネストルート優先フラット + Parallel Routes
デプロイ先エッジ対応多数Vercel最適化
学習曲線Web標準重視React重視

Remixプロジェクトの始め方

# プロジェクト作成
npx create-remix@latest my-remix-app

# 開発サーバー起動
cd my-remix-app
npm run dev

プロジェクト作成時に選択肢が表示されます:

  • デプロイ先(Vercel、Cloudflare Workers、Node.jsなど)
  • TypeScript or JavaScript
  • パッケージマネージャー

基本的なルート構成

シンプルなページ

// app/routes/about.tsx
export default function About() {
  return (
    <div>
      <h1>About Us</h1>
      <p>Remixで構築されたWebサイトです。</p>
    </div>
  );
}

データを取得するページ

// app/routes/blog._index.tsx
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

export async function loader() {
  const posts = await db.post.findMany({
    orderBy: { createdAt: "desc" },
    take: 10,
  });

  return json({ posts });
}

export default function BlogIndex() {
  const { posts } = useLoaderData<typeof loader>();

  return (
    <div>
      <h1>ブログ記事一覧</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <a href={`/blog/${post.slug}`}>{post.title}</a>
          </li>
        ))}
      </ul>
    </div>
  );
}

動的ルート

// app/routes/blog.$slug.tsx
import { json, LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await db.post.findUnique({
    where: { slug: params.slug },
  });

  if (!post) {
    throw new Response("Not Found", { status: 404 });
  }

  return json({ post });
}

export default function BlogPost() {
  const { post } = useLoaderData<typeof loader>();

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

フォーム処理とaction

Remixの真骨頂は、フォーム処理の簡潔さです。

// app/routes/contact.tsx
import { json, ActionFunctionArgs, redirect } from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react";

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const name = formData.get("name");
  const email = formData.get("email");
  const message = formData.get("message");

  // バリデーション
  const errors: Record<string, string> = {};
  if (!name) errors.name = "名前は必須です";
  if (!email) errors.email = "メールアドレスは必須です";
  if (!message) errors.message = "メッセージは必須です";

  if (Object.keys(errors).length > 0) {
    return json({ errors }, { status: 400 });
  }

  // データベースに保存
  await db.contact.create({
    data: { name, email, message },
  });

  return redirect("/contact/success");
}

export default function Contact() {
  const actionData = useActionData<typeof action>();

  return (
    <Form method="post">
      <div>
        <label htmlFor="name">名前</label>
        <input type="text" name="name" id="name" required />
        {actionData?.errors?.name && (
          <span className="error">{actionData.errors.name}</span>
        )}
      </div>

      <div>
        <label htmlFor="email">メールアドレス</label>
        <input type="email" name="email" id="email" required />
        {actionData?.errors?.email && (
          <span className="error">{actionData.errors.email}</span>
        )}
      </div>

      <div>
        <label htmlFor="message">メッセージ</label>
        <textarea name="message" id="message" required />
        {actionData?.errors?.message && (
          <span className="error">{actionData.errors.message}</span>
        )}
      </div>

      <button type="submit">送信</button>
    </Form>
  );
}

JavaScriptが無効でも動作するのがRemixの強みです。

ネストレイアウト

// app/routes/dashboard.tsx (レイアウト)
import { Outlet } from "@remix-run/react";

export default function DashboardLayout() {
  return (
    <div className="dashboard">
      <nav>
        <a href="/dashboard">ダッシュボード</a>
        <a href="/dashboard/settings">設定</a>
        <a href="/dashboard/profile">プロフィール</a>
      </nav>
      <main>
        <Outlet /> {/* 子ルートがここに表示される */}
      </main>
    </div>
  );
}
// app/routes/dashboard._index.tsx
export default function DashboardIndex() {
  return <h1>ダッシュボードへようこそ</h1>;
}

エラーハンドリング

// app/routes/blog.$slug.tsx
import { useRouteError, isRouteErrorResponse } from "@remix-run/react";

export function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    if (error.status === 404) {
      return (
        <div>
          <h1>記事が見つかりません</h1>
          <p>お探しの記事は存在しないか、削除された可能性があります。</p>
        </div>
      );
    }
  }

  return (
    <div>
      <h1>エラーが発生しました</h1>
      <p>予期しないエラーが発生しました。</p>
    </div>
  );
}

セッション管理

// app/sessions.ts
import { createCookieSessionStorage } from "@remix-run/node";

export const { getSession, commitSession, destroySession } =
  createCookieSessionStorage({
    cookie: {
      name: "__session",
      httpOnly: true,
      maxAge: 60 * 60 * 24 * 7, // 1週間
      path: "/",
      sameSite: "lax",
      secrets: [process.env.SESSION_SECRET!],
      secure: process.env.NODE_ENV === "production",
    },
  });
// app/routes/login.tsx
import { ActionFunctionArgs, redirect } from "@remix-run/node";
import { getSession, commitSession } from "~/sessions";

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const email = formData.get("email");
  const password = formData.get("password");

  const user = await verifyLogin(email, password);
  if (!user) {
    return json({ error: "認証に失敗しました" }, { status: 401 });
  }

  const session = await getSession(request.headers.get("Cookie"));
  session.set("userId", user.id);

  return redirect("/dashboard", {
    headers: {
      "Set-Cookie": await commitSession(session),
    },
  });
}

メタタグとSEO

// app/routes/blog.$slug.tsx
import type { MetaFunction } from "@remix-run/node";

export const meta: MetaFunction<typeof loader> = ({ data }) => {
  if (!data) {
    return [{ title: "記事が見つかりません" }];
  }

  return [
    { title: data.post.title },
    { name: "description", content: data.post.description },
    { property: "og:title", content: data.post.title },
    { property: "og:description", content: data.post.description },
    { property: "og:image", content: data.post.ogImage },
  ];
};

デプロイ

Vercelへのデプロイ

npm install @vercel/remix
// remix.config.js
export default {
  serverModuleFormat: "esm",
};

Cloudflare Workersへのデプロイ

npm install @remix-run/cloudflare @cloudflare/workers-types

Cloudflare Workersなら、グローバルエッジで超高速動作します。

まとめ

Remixは、Webの基本原則に立ち返りつつ、モダンな開発体験を提供するフレームワークです。特に以下の場合におすすめ:

  • Web標準を重視した開発をしたい
  • フォーム処理が多いアプリケーション
  • JavaScriptなしでも動作するプログレッシブエンハンスメント
  • エッジデプロイを活用したい

Next.jsと比較しながら選択するのが良いでしょう。どちらも優れたフレームワークですが、思想とアプローチが異なります。