Remix v2 - フルスタックReactフレームワーク

2024.12.25

Remix v2の概要

Remix v2は、Webの基礎技術(HTTP、フォーム、ブラウザキャッシュ)を活用したフルスタックReactフレームワークです。v2ではVite統合、型安全性の向上、パフォーマンス改善が実現しています。

Vite統合

設定

// vite.config.ts
import { vitePlugin as remix } from '@remix-run/dev';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    remix({
      future: {
        v3_fetcherPersist: true,
        v3_relativeSplatPath: true
      }
    })
  ]
});

開発サーバー

npm run dev
# Viteの高速HMRが利用可能

ファイルベースルーティング

app/
├── routes/
│   ├── _index.tsx           # /
│   ├── about.tsx            # /about
│   ├── posts._index.tsx     # /posts
│   ├── posts.$postId.tsx    # /posts/:postId
│   ├── posts.$postId_.edit.tsx # /posts/:postId/edit
│   └── ($lang).docs.$.tsx   # /:lang?/docs/*
└── root.tsx

ルートの命名規則

パターン意味
_indexインデックスルート
$param動的パラメータ
($param)オプショナルパラメータ
$スプラット(キャッチオール)
_レイアウトなしのルート
.ネストしたURL

データローディング

loader

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

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

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

  return json({ post });
}

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

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

action

// routes/posts.new.tsx
import { redirect, type ActionFunctionArgs } from '@remix-run/node';
import { Form, useActionData } from '@remix-run/react';
import { z } from 'zod';

const schema = z.object({
  title: z.string().min(1),
  content: z.string().min(10)
});

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const result = schema.safeParse(Object.fromEntries(formData));

  if (!result.success) {
    return json({ errors: result.error.flatten() }, { status: 400 });
  }

  const post = await db.post.create({ data: result.data });
  return redirect(`/posts/${post.id}`);
}

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

  return (
    <Form method="post">
      <input name="title" />
      {actionData?.errors?.fieldErrors?.title && (
        <span>{actionData.errors.fieldErrors.title}</span>
      )}
      <textarea name="content" />
      <button type="submit">Create</button>
    </Form>
  );
}

fetcher

ナビゲーションなしでデータを送信・取得します。

import { useFetcher } from '@remix-run/react';

export default function LikeButton({ postId }: { postId: string }) {
  const fetcher = useFetcher();
  const isLiking = fetcher.state === 'submitting';

  return (
    <fetcher.Form method="post" action="/api/like">
      <input type="hidden" name="postId" value={postId} />
      <button type="submit" disabled={isLiking}>
        {isLiking ? 'Liking...' : 'Like'}
      </button>
    </fetcher.Form>
  );
}

defer と Suspense

import { defer } from '@remix-run/node';
import { Await, useLoaderData } from '@remix-run/react';
import { Suspense } from 'react';

export async function loader() {
  // 即座に必要なデータ
  const user = await getUser();

  // 遅延読み込みするデータ
  const recommendations = getRecommendations(); // awaitしない

  return defer({
    user,
    recommendations
  });
}

export default function Dashboard() {
  const { user, recommendations } = useLoaderData<typeof loader>();

  return (
    <div>
      <h1>Welcome, {user.name}</h1>

      <Suspense fallback={<div>Loading recommendations...</div>}>
        <Await resolve={recommendations}>
          {(data) => <RecommendationsList items={data} />}
        </Await>
      </Suspense>
    </div>
  );
}

エラーハンドリング

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

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

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  return (
    <div>
      <h1>Error</h1>
      <p>{error instanceof Error ? error.message : 'Unknown error'}</p>
    </div>
  );
}

メタデータ

import { type MetaFunction } from '@remix-run/node';

export const meta: MetaFunction<typeof loader> = ({ data }) => {
  return [
    { title: data?.post.title ?? 'Not Found' },
    { name: 'description', content: data?.post.excerpt },
    { property: 'og:title', content: data?.post.title }
  ];
};

デプロイ

# Node.js
npm run build
npm start

# Cloudflare Pages
npm run build
npx wrangler pages deploy ./build/client

# Vercel
npm run build
# vercel.jsonで設定

まとめ

Remix v2は、Webの基礎技術を活用した堅牢なフルスタックフレームワークです。Vite統合による開発体験の向上、型安全なデータローディング、プログレッシブエンハンスメントにより、モダンなWebアプリケーションを効率的に構築できます。

← 一覧に戻る