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}
 )}
 <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を見るときの観点

Remixは、クライアント側だけで状態を抱えるのではなく、Web標準のフォーム、HTTP、URL、サーバー処理を活用する設計が特徴です。loaderとactionを中心に、データ取得と更新の責務をはっきり分けます。

SPA的な実装に慣れている場合、最初は考え方が違って見えます。しかし、フォーム送信、リダイレクト、エラー境界を自然に扱えるため、データ更新が多い業務アプリでは見通しが良くなることがあります。

向いているケース

  • フォーム送信とサーバー処理が多い
  • URLを状態管理の中心にしたい
  • エラー境界やネストルートを活用したい
  • Web標準に寄せたフルスタック設計をしたい

まとめ

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

参考リソース

← 一覧に戻る
PR
PR
PR
PR