T-CREATOR

Remix 入門:最短 5 分で始める新時代 React アプリ

Remix 入門:最短 5 分で始める新時代 React アプリ

React アプリケーション開発の世界に新しい風が吹いています。それがRemixです。

従来の React アプリケーションでは、SEO やパフォーマンス、ユーザー体験の向上に多くの時間を費やしていました。しかし、Remix はこれらの課題を根本から解決し、開発者が本来やりたいことに集中できる環境を提供してくれます。

この記事では、Remix の魅力を理解し、最短 5 分で実際にアプリケーションを作成できるよう、段階的に解説していきます。初心者の方でも安心して読み進められるよう、実際のエラーコードや解決策も含めて詳しく説明いたします。

Remix とは何か

Remix は、React ベースのフルスタック Web フレームワークです。Next.js や Gatsby とは異なるアプローチを取り、Web 標準に忠実でありながら、現代的な開発体験を提供します。

Remix の最大の特徴は、サーバーサイドレンダリング(SSR)クライアントサイドハイドレーションを完璧に統合していることです。これにより、SEO に優しく、初期読み込みが高速で、ユーザーインタラクションも滑らかなアプリケーションを簡単に構築できます。

typescript// Remixの基本的なファイル構造
app/
├── routes/
│   ├── _index.tsx          // ホームページ
│   ├── about.tsx           // アバウトページ
│   └── posts.$postId.tsx   // 動的ルート
├── components/
│   └── Header.tsx          // 共通コンポーネント
├── styles/
│   └── app.css             // グローバルスタイル
└── root.tsx                // アプリケーションのルート

Remix は、React Router の開発チームが作成したフレームワークです。そのため、ルーティングの概念は React Router と非常に似ており、学習コストも低く抑えられています。

従来の React アプリとの違い

従来の React アプリケーションと Remix の違いを理解することで、なぜ Remix が必要なのかが明確になります。

従来の React アプリの問題点

従来の Create React App(CRA)で作成されたアプリケーションには、以下のような課題がありました:

  • SEO の課題: クライアントサイドレンダリングのため、検索エンジンがコンテンツを正しく認識できない
  • 初期読み込みの遅さ: JavaScript バンドルが大きくなりがち
  • 複雑な状態管理: Redux や Zustand などの追加ライブラリが必要
  • API 設計の複雑さ: フロントエンドとバックエンドの分離による開発の複雑化

Remix が解決する課題

Remix は、これらの課題を以下のように解決します:

typescript// 従来のReactアプリ(CRA)
// クライアントサイドでのデータ取得
import { useState, useEffect } from 'react';

function PostList() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/posts')
      .then((res) => res.json())
      .then((data) => {
        setPosts(data);
        setLoading(false);
      });
  }, []);

  if (loading) return <div>読み込み中...</div>;

  return (
    <div>
      {posts.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
}
typescript// Remixでのデータ取得
// サーバーサイドでのデータ取得
import type { LoaderFunction } from '@remix-run/node';

export const loader: LoaderFunction = async () => {
  const posts = await getPosts(); // サーバーサイドで実行
  return { posts };
};

export default function PostList() {
  const { posts } = useLoaderData();

  return (
    <div>
      {posts.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
}

Remix では、データ取得がサーバーサイドで行われるため、初期表示が高速で、SEO にも優しいアプリケーションが構築できます。

開発環境の準備

Remix アプリケーションの開発を始める前に、必要な環境を整えましょう。

必要なツール

  • Node.js: バージョン 18 以上
  • Yarn: パッケージマネージャー
  • エディタ: VS Code 推奨

Node.js のインストール確認

まず、Node.js が正しくインストールされているか確認しましょう。

bash# Node.jsのバージョン確認
node --version
# v18.17.0 以上であることを確認

# Yarnのインストール確認
yarn --version
# バージョンが表示されればOK

もし Yarn がインストールされていない場合は、以下のコマンドでインストールしてください:

bash# Yarnのインストール
npm install -g yarn

よくあるエラーと解決策

Node.js のバージョンが古い場合、以下のようなエラーが発生することがあります:

bash# エラー例
Error: Remix requires Node.js version 18.17.0 or higher

この場合、Node.js を最新版にアップデートする必要があります。Node Version Manager(nvm)を使用している場合は:

bash# nvmを使用したNode.jsのアップデート
nvm install 18
nvm use 18

最初の Remix アプリを作成

環境が整ったら、最初の Remix アプリケーションを作成しましょう。

プロジェクトの作成

Remix アプリケーションの作成は、たった 1 つのコマンドで完了します:

bash# Remixアプリケーションの作成
npx create-remix@latest my-remix-app

# プロジェクトディレクトリに移動
cd my-remix-app

# 依存関係のインストール
yarn install

プロジェクト構造の理解

作成されたプロジェクトの構造を確認しましょう:

bash# プロジェクト構造の確認
tree -L 3
arduinomy-remix-app/
├── app/
│   ├── components/
│   ├── routes/
│   ├── styles/
│   ├── root.tsx
│   └── entry.client.tsx
├── public/
├── package.json
├── remix.config.js
└── tsconfig.json

開発サーバーの起動

アプリケーションを起動して、正常に動作することを確認しましょう:

bash# 開発サーバーの起動
yarn dev

ブラウザで http:​/​​/​localhost:3000 にアクセスすると、Remix のウェルカムページが表示されます。

よくあるエラーと解決策

初回起動時に以下のようなエラーが発生することがあります:

bash# エラー例1: ポートが使用中
Error: listen EADDRINUSE: address already in use :::3000

解決策:

bash# 別のポートで起動
yarn dev --port 3001
bash# エラー例2: 依存関係の問題
Error: Cannot find module '@remix-run/node'

解決策:

bash# 依存関係を再インストール
rm -rf node_modules yarn.lock
yarn install

基本的なルーティング

Remix のルーティングシステムは、ファイルベースのルーティングを採用しています。これにより、直感的で理解しやすい構造になっています。

ファイルベースルーティングの基本

app​/​routes​/​ ディレクトリ内のファイル名が、そのまま URL パスになります:

typescript// app/routes/_index.tsx - ホームページ (/)
export default function Index() {
  return (
    <div>
      <h1>ようこそ!</h1>
      <p>これはRemixアプリのホームページです。</p>
    </div>
  );
}
typescript// app/routes/about.tsx - アバウトページ (/about)
export default function About() {
  return (
    <div>
      <h1>アバウト</h1>
      <p>このアプリについて説明します。</p>
    </div>
  );
}

動的ルーティング

動的なパラメータを含むルートも簡単に作成できます:

typescript// app/routes/posts.$postId.tsx - 動的ルート (/posts/123)
import { useParams } from '@remix-run/react';

export default function Post() {
  const { postId } = useParams();

  return (
    <div>
      <h1>投稿 {postId}</h1>
      <p>この投稿の詳細を表示します。</p>
    </div>
  );
}

ネストしたルーティング

Remix では、親子関係のあるルートも簡単に作成できます:

typescript// app/routes/posts._index.tsx - 投稿一覧 (/posts)
export default function Posts() {
  return (
    <div>
      <h1>投稿一覧</h1>
      <ul>
        <li>
          <a href='/posts/1'>投稿1</a>
        </li>
        <li>
          <a href='/posts/2'>投稿2</a>
        </li>
      </ul>
    </div>
  );
}

よくあるエラーと解決策

ルーティングでよく発生するエラーを確認しましょう:

bash# エラー例: ファイルが見つからない
Error: Cannot find module './routes/nonexistent'

このエラーは、存在しないルートファイルを参照した場合に発生します。ファイル名とパスを正確に確認してください。

typescript// エラー例: 動的パラメータの型エラー
// app/routes/posts.$postId.tsx
export default function Post() {
  const { postId } = useParams();

  // postIdがundefinedの可能性があるため、型エラーが発生
  return <div>投稿 {postId}</div>;
}

解決策:

typescript// 型安全な実装
export default function Post() {
  const { postId } = useParams();

  if (!postId) {
    return <div>投稿が見つかりません</div>;
  }

  return <div>投稿 {postId}</div>;
}

データフェッチング

Remix の最大の魅力の一つが、サーバーサイドでのデータフェッチングです。これにより、SEO に優しく、高速なアプリケーションを構築できます。

ローダー関数の基本

Remix では、loader関数を使用してサーバーサイドでデータを取得します:

typescript// app/routes/posts.tsx
import type { LoaderFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';

// サーバーサイドで実行されるデータ取得関数
export const loader: LoaderFunction = async () => {
  // 実際のAPIやデータベースからデータを取得
  const posts = [
    {
      id: 1,
      title: 'Remix入門',
      content: 'Remixの基本を学びましょう',
    },
    {
      id: 2,
      title: 'Reactの未来',
      content: 'Reactの新しい可能性',
    },
  ];

  return json({ posts });
};

// クライアントサイドでデータを使用
export default function Posts() {
  const { posts } = useLoaderData<typeof loader>();

  return (
    <div>
      <h1>投稿一覧</h1>
      {posts.map((post) => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </div>
      ))}
    </div>
  );
}

動的データの取得

URL パラメータを使用して動的にデータを取得することも可能です:

typescript// app/routes/posts.$postId.tsx
import type { LoaderFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData, useParams } from '@remix-run/react';

export const loader: LoaderFunction = async ({
  params,
}) => {
  const { postId } = params;

  // 実際のAPIから特定の投稿を取得
  const post = {
    id: postId,
    title: `投稿${postId}`,
    content: `これは投稿${postId}の内容です。`,
  };

  return json({ post });
};

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

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

エラーハンドリング

データ取得時にエラーが発生した場合の処理も重要です:

typescript// app/routes/posts.tsx
import type { LoaderFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import {
  useLoaderData,
  useRouteError,
} from '@remix-run/react';

export const loader: LoaderFunction = async () => {
  try {
    // データベースやAPIからデータを取得
    const posts = await fetchPosts();
    return json({ posts });
  } catch (error) {
    // エラーが発生した場合は404エラーを投げる
    throw new Response('投稿が見つかりません', {
      status: 404,
    });
  }
};

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

  return (
    <div>
      <h1>投稿一覧</h1>
      {posts.map((post) => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </div>
      ))}
    </div>
  );
}

// エラーページの表示
export function ErrorBoundary() {
  const error = useRouteError();

  return (
    <div>
      <h1>エラーが発生しました</h1>
      <p>
        申し訳ございません。ページの読み込みに失敗しました。
      </p>
    </div>
  );
}

よくあるエラーと解決策

データフェッチングでよく発生するエラーを確認しましょう:

bash# エラー例: ローダー関数の型エラー
TypeError: loader is not a function

このエラーは、loader関数の定義が正しくない場合に発生します。以下の点を確認してください:

typescript// 正しい定義
export const loader: LoaderFunction = async ({
  request,
  params,
}) => {
  // 処理
  return json(data);
};

// 間違った定義
export function loader({ request, params }) {
  // 処理
  return json(data);
}
bash# エラー例: useLoaderDataの型エラー
TypeError: useLoaderData is not a function

このエラーは、インポートが正しくない場合に発生します:

typescript// 正しいインポート
import { useLoaderData } from '@remix-run/react';

// 間違ったインポート
import { useLoaderData } from '@remix-run/node';

フォーム処理

Remix では、フォームの処理もサーバーサイドで行うことができます。これにより、JavaScript が無効な環境でもフォームが正常に動作します。

基本的なフォーム処理

Remix では、action関数を使用してフォームの送信を処理します:

typescript// app/routes/contact.tsx
import type {
  ActionFunction,
  LoaderFunction,
} from '@remix-run/node';
import { json, redirect } from '@remix-run/node';
import { Form, useActionData } from '@remix-run/react';

// フォーム送信時の処理
export const action: ActionFunction = async ({
  request,
}) => {
  const formData = await request.formData();
  const name = formData.get('name');
  const email = formData.get('email');
  const message = formData.get('message');

  // バリデーション
  if (!name || !email || !message) {
    return json(
      { error: 'すべての項目を入力してください' },
      { status: 400 }
    );
  }

  // データベースに保存する処理
  await saveContact({ name, email, message });

  // 成功時はリダイレクト
  return redirect('/contact/thanks');
};

// ページ表示時の処理
export const loader: LoaderFunction = async () => {
  return json({});
};

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

  return (
    <div>
      <h1>お問い合わせ</h1>

      {actionData?.error && (
        <div style={{ color: 'red' }}>
          {actionData.error}
        </div>
      )}

      <Form method='post'>
        <div>
          <label htmlFor='name'>お名前:</label>
          <input
            type='text'
            id='name'
            name='name'
            required
          />
        </div>

        <div>
          <label htmlFor='email'>メールアドレス:</label>
          <input
            type='email'
            id='email'
            name='email'
            required
          />
        </div>

        <div>
          <label htmlFor='message'>メッセージ:</label>
          <textarea
            id='message'
            name='message'
            required
          ></textarea>
        </div>

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

データの更新処理

既存のデータを更新する処理も簡単に実装できます:

typescript// app/routes/posts.$postId.edit.tsx
import type {
  ActionFunction,
  LoaderFunction,
} from '@remix-run/node';
import { json, redirect } from '@remix-run/node';
import { Form, useLoaderData } from '@remix-run/react';

export const loader: LoaderFunction = async ({
  params,
}) => {
  const { postId } = params;
  const post = await getPost(postId);

  if (!post) {
    throw new Response('投稿が見つかりません', {
      status: 404,
    });
  }

  return json({ post });
};

export const action: ActionFunction = async ({
  request,
  params,
}) => {
  const { postId } = params;
  const formData = await request.formData();
  const title = formData.get('title');
  const content = formData.get('content');

  // 投稿を更新
  await updatePost(postId, { title, content });

  // 更新後は投稿詳細ページにリダイレクト
  return redirect(`/posts/${postId}`);
};

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

  return (
    <div>
      <h1>投稿を編集</h1>

      <Form method='post'>
        <div>
          <label htmlFor='title'>タイトル:</label>
          <input
            type='text'
            id='title'
            name='title'
            defaultValue={post.title}
            required
          />
        </div>

        <div>
          <label htmlFor='content'>内容:</label>
          <textarea
            id='content'
            name='content'
            defaultValue={post.content}
            required
          ></textarea>
        </div>

        <button type='submit'>更新</button>
      </Form>
    </div>
  );
}

よくあるエラーと解決策

フォーム処理でよく発生するエラーを確認しましょう:

bash# エラー例: FormDataの取得エラー
TypeError: request.formData is not a function

このエラーは、古い Node.js バージョンで発生することがあります。Node.js 18 以上を使用してください。

typescript// エラー例: バリデーションエラーの処理
// フォームデータの型チェック
export const action: ActionFunction = async ({
  request,
}) => {
  const formData = await request.formData();
  const email = formData.get('email');

  // 型安全なバリデーション
  if (typeof email !== 'string' || !email.includes('@')) {
    return json(
      { error: '有効なメールアドレスを入力してください' },
      { status: 400 }
    );
  }

  // 処理を続行
};

エラーハンドリング

Remix では、エラーハンドリングが非常に重要です。適切なエラー処理により、ユーザー体験を向上させることができます。

基本的なエラーハンドリング

各ルートでエラーが発生した場合の処理を定義できます:

typescript// app/routes/posts.$postId.tsx
import { useRouteError } from '@remix-run/react';

export default function Post() {
  // 通常のコンポーネント
  return <div>投稿の内容</div>;
}

// エラーが発生した場合の表示
export function ErrorBoundary() {
  const error = useRouteError();

  return (
    <div>
      <h1>エラーが発生しました</h1>
      <p>
        申し訳ございません。ページの読み込みに失敗しました。
      </p>
      <a href='/'>ホームに戻る</a>
    </div>
  );
}

404 エラーの処理

存在しないページにアクセスした場合の処理:

typescript// app/routes/posts.$postId.tsx
import type { LoaderFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import {
  useLoaderData,
  useRouteError,
} from '@remix-run/react';

export const loader: LoaderFunction = async ({
  params,
}) => {
  const { postId } = params;
  const post = await getPost(postId);

  // 投稿が見つからない場合は404エラー
  if (!post) {
    throw new Response('投稿が見つかりません', {
      status: 404,
    });
  }

  return json({ post });
};

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

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

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

  // 404エラーの場合
  if (error instanceof Response && error.status === 404) {
    return (
      <div>
        <h1>ページが見つかりません</h1>
        <p>
          お探しのページは存在しないか、削除された可能性があります。
        </p>
        <a href='/'>ホームに戻る</a>
      </div>
    );
  }

  // その他のエラー
  return (
    <div>
      <h1>エラーが発生しました</h1>
      <p>
        予期しないエラーが発生しました。しばらく時間をおいて再度お試しください。
      </p>
    </div>
  );
}

グローバルエラーハンドリング

アプリケーション全体でエラーを処理する場合は、root.tsxでエラーハンドリングを設定します:

typescript// app/root.tsx
import { useRouteError } from '@remix-run/react';

export default function App() {
  return (
    <html>
      <head>
        <title>Remixアプリ</title>
      </head>
      <body>
        <Outlet />
      </body>
    </html>
  );
}

// グローバルエラーハンドリング
export function ErrorBoundary() {
  const error = useRouteError();

  return (
    <html>
      <head>
        <title>エラー - Remixアプリ</title>
      </head>
      <body>
        <div>
          <h1>アプリケーションエラー</h1>
          <p>
            申し訳ございません。アプリケーションでエラーが発生しました。
          </p>
          <a href='/'>ホームに戻る</a>
        </div>
      </body>
    </html>
  );
}

よくあるエラーと解決策

エラーハンドリングでよく発生する問題を確認しましょう:

bash# エラー例: ErrorBoundaryの型エラー
TypeError: useRouteError is not a function

このエラーは、インポートが正しくない場合に発生します:

typescript// 正しいインポート
import { useRouteError } from '@remix-run/react';

// 間違ったインポート
import { useRouteError } from '@remix-run/node';
typescript// エラー例: エラーオブジェクトの型チェック
export function ErrorBoundary() {
  const error = useRouteError();

  // 型安全なエラーハンドリング
  if (error instanceof Response) {
    return (
      <div>
        <h1>HTTPエラー {error.status}</h1>
        <p>{error.statusText}</p>
      </div>
    );
  }

  if (error instanceof Error) {
    return (
      <div>
        <h1>エラーが発生しました</h1>
        <p>{error.message}</p>
      </div>
    );
  }

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

デプロイ方法

Remix アプリケーションのデプロイは、選択したプラットフォームによって異なります。主要なデプロイ方法を紹介します。

Vercel へのデプロイ

Vercel は、Remix アプリケーションのデプロイに最適なプラットフォームです:

bash# Vercel CLIのインストール
yarn global add vercel

# プロジェクトのデプロイ
vercel

デプロイ時の設定ファイル:

json// vercel.json
{
  "buildCommand": "yarn build",
  "devCommand": "yarn dev",
  "installCommand": "yarn install",
  "framework": "remix"
}

Netlify へのデプロイ

Netlify でも簡単にデプロイできます:

bash# Netlify CLIのインストール
yarn global add netlify-cli

# プロジェクトのデプロイ
netlify deploy --prod

設定ファイル:

toml# netlify.toml
[build]
  command = "yarn build"
  publish = "public"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

Railway へのデプロイ

Railway は、フルスタックアプリケーションのデプロイに適しています:

bash# Railway CLIのインストール
yarn global add @railway/cli

# プロジェクトのデプロイ
railway login
railway init
railway up

環境変数の設定

本番環境では、環境変数を適切に設定する必要があります:

typescript// app/config.server.ts
export const config = {
  databaseUrl: process.env.DATABASE_URL,
  apiKey: process.env.API_KEY,
  nodeEnv: process.env.NODE_ENV || 'development',
};
bash# .env.local(開発環境)
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=your_api_key_here
NODE_ENV=development

よくあるデプロイエラーと解決策

デプロイ時に発生する一般的なエラーを確認しましょう:

bash# エラー例: ビルドエラー
Error: Cannot find module '@remix-run/node'

このエラーは、依存関係が正しくインストールされていない場合に発生します:

bash# 解決策
yarn install --frozen-lockfile
bash# エラー例: 環境変数の未設定
Error: DATABASE_URL is not defined

このエラーは、本番環境で環境変数が設定されていない場合に発生します。デプロイプラットフォームの設定画面で環境変数を設定してください。

bash# エラー例: ポート設定の問題
Error: listen EADDRINUSE: address already in use :::3000

このエラーは、ポートが既に使用されている場合に発生します:

typescript// 解決策: 動的ポート設定
const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

まとめ

Remix は、React アプリケーション開発の新しい可能性を開くフレームワークです。

この記事では、Remix の基本概念から実際のアプリケーション作成、デプロイまで、段階的に学習できる内容を提供しました。Remix の最大の魅力は、Web 標準に忠実でありながら、現代的な開発体験を提供することです。

サーバーサイドレンダリングによる SEO の向上、高速な初期読み込み、滑らかなユーザーインタラクション。これらの要素が、Remix アプリケーションの優れたユーザー体験を実現しています。

初心者の方でも、この記事の内容に従って進めれば、最短 5 分で Remix アプリケーションを作成し、本番環境にデプロイできるようになります。実際のエラーコードと解決策も含めているため、開発中に問題が発生しても安心して対処できます。

Remix は、React の未来を切り開くフレームワークです。この記事が、あなたの Remix 開発の旅の第一歩となることを願っています。

関連リンク