react router v7のレポート actionについて
私個人としては、シンプルでフルスタックなWeb開発が好きです。そのため、高機能ながらも少し複雑なNext.jsよりも、React Router v7が広く普及してほしいと考えています。
React Router v7について
React Router v7は、これまで利用してきたReact RouterとRemixを統合したバージョンです。従来の馴染みのあるReact RouterでSSR(サーバーサイドレンダリング)が利用できるようになりました。
そのため、今までReactを使ってきて、Next.jsは使いたくないけれどSSRを試してみたい方にとって、非常に良い選択肢となります。
触ってみる
1. 環境構築
npx create-react-router@latest
npmを利用するかどうかを聞かれるので、yarnなど他のパッケージマネージャーを使いたい場合は、dlxなどのコマンドで対応してください。
このコマンドを入力した時点での気づき:
- Tailwind v4がデフォルトで組み込まれている
Tailwindを利用していないプロジェクトでは、これを削除する必要があります。また、Tailwind v4からはtailwind.config.jsがなくなっているので、新しい仕様をキャッチアップしましょう。 - app/route/home.tsxの型定義
homeを開くと以下のようなインポートがあります:
import type { Route } from "./+types/home";
初めの段階ではこのファイルが型エラーを起こしていますが、
npm run dev
開発サーバーを起動すると、型エラーが解消されます。これはReact Routerが提供するパスの型安全性を確保するために必要なファイルです。
詳細は以下のリンクで確認できます:
簡単に説明すると、Routerで設定したパラメーターなどの型情報を.react-router/配下に格納してくれており、その情報からパラメーターの型を取得します。
開発サーバーが起動されました
画面を見てみます
問題なさそうですね
action
actionとはサーバー側でデータ変更やフォーム送信を処理するための直接的で安全な方法を提供します。
試しにconform+zodを利用して簡易的なバリデーションを取り入れたtodo作成フォームを作成してみましょう
1. conformとzodのinstall
npm i @conform-to/react @conform-to/zod zod
2. todoを作成してみる
全体のコードです
ほんとだったらファイルを分割したいですが、技術ブログなので1ファイルにまとめました。
import { Form, useActionData, useNavigate, useNavigation } from "react-router";
import type { Route } from "./+types/todo";
import { z } from "zod";
import { getFormProps, getInputProps, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
// todoを作成するためのschema
const todoSchema = z.object({
title: z.string({ message: "タスク名を入力してください" }).min(1),
description: z.string({ message: "詳細を入力してください" }).min(1),
});
type Todo = z.infer<typeof todoSchema>;
const useCreateTodo = () => {
const [form, fields] = useForm<Todo>(
{
lastResult: null,
defaultValue: {
title: "",
description: "",
},
onValidate({ formData }) {
return parseWithZod(formData, { schema: todoSchema });
}
}
);
const navigation = useNavigation();
const submitting = navigation.state === "submitting";
return { form, fields, submitting };
};
// todoを作成するためのaction
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const title = formData.get("title");
const description = formData.get("description");
// バリデーション
const success = todoSchema.safeParse({ title, description });
if (!success.success) {
return {
ok: false,
errors: success.error.flatten().fieldErrors,
};
}
// 自己満で作ったインジケーターを見たいがために3秒待機
await new Promise((resolve) => setTimeout(resolve, 3000));
// データベースに保存
// 今回はAPIないので省略
// 作成したtodoの結果を返す
return {
ok: true,
data: {
title: success.data.title,
description: success.data.description,
},
};
}
/** todoの作成とtodoの一覧表示 */
export default function Todo() {
// actionの結果を取得
const actionData = useActionData<typeof action>();
const { form, fields, submitting } = useCreateTodo();
return (
<div className="min-h-screen bg-gradient-to-br from-pink-50 to-purple-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden p-8 space-y-6 transition-transform hover:scale-[1.02]">
<h2 className="text-center text-3xl font-bold tracking-tight text-gray-700 mb-8">
✨ ToDo作成 ✨
</h2>
<Form method="post" {...getFormProps(form)} className="space-y-6">
<div>
<label htmlFor={fields.title.id} className="block text-sm font-medium text-gray-700 mb-2">
タスク名
</label>
<input
{...getInputProps(fields.title, { type: "text" })}
className="block w-full rounded-lg border border-pink-200 px-4 py-3 text-gray-700 focus:border-pink-400 focus:outline-none focus:ring focus:ring-pink-200 transition-colors"
placeholder="タスクの名前を入力"
/>
{fields.title.errors && (
<p className="mt-1 text-sm text-red-600">{fields.title.errors[0]}</p>
)}
</div>
<div>
<label htmlFor={fields.description.id} className="block text-sm font-medium text-gray-700 mb-2">
詳細
</label>
<input
{...getInputProps(fields.description, { type: "text" })}
className="block w-full rounded-lg border border-pink-200 px-4 py-3 text-gray-700 focus:border-pink-400 focus:outline-none focus:ring focus:ring-pink-200 transition-colors"
placeholder="タスクの詳細を入力"
/>
{fields.description.errors && (
<p className="mt-1 text-sm text-red-600">{fields.description.errors[0]}</p>
)}
</div>
<button
type="submit"
className="w-full bg-gradient-to-r from-pink-400 to-purple-400 text-white font-semibold py-3 px-6 rounded-lg shadow-md hover:from-pink-500 hover:to-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-300 transition-all duration-300 transform hover:-translate-y-1"
>
作成 ✨
</button>
</Form>
{ !submitting && actionData && actionData.data && (
<div className="mt-8">
<h3 className="text-lg font-semibold text-gray-700 mb-4">
作成したタスク
</h3>
<p className="text-gray-600">{actionData.data.title}</p>
<p className="text-gray-600">{actionData.data.description}</p>
</div>
)}
{submitting && (
<div className="mt-8 flex flex-col items-center justify-center space-y-4">
<div className="flex space-x-3">
<div className="h-3 w-3 rounded-full bg-pink-400 animate-bounce [animation-delay:-0.3s]"></div>
<div className="h-3 w-3 rounded-full bg-purple-400 animate-bounce [animation-delay:-0.15s]"></div>
<div className="h-3 w-3 rounded-full bg-pink-400 animate-bounce"></div>
</div>
<p className="text-gray-600 font-medium animate-pulse">
✨ タスクを作成中です ✨
</p>
</div>
)}
</div>
</div>
)
}
実際の動画
これまでのreactと違うところ
actionの利用
- Formのリクエスト先はactionになります。
- actionでreturnした結果はuseActionDataで取得することができる
actionの何がいい?
とりあえずAIに聞いておきました
パフォーマンスの向上
クライアントサイドJavaScriptの削減: Server Actionsはサーバー上で実行されるため、クライアントバンドルに含まれず、初期ページ読み込み時間が短縮されます。
サーバーサイドでのデータ操作: 別個のAPIエンドポイントを作成・管理する必要がなく、不要なネットワークラウンドトリップを避けることでパフォーマンスが向上します。
開発の簡素化
フロントエンドとバックエンドの統合: コンポーネント内で直接SQLクエリを実行したり、サーバー側の関数を直接呼び出したりできるため、開発がシンプル化されます。
型の一貫性: フロントエンドとバックエンドで一貫した型定義が利用でき、型違反を防ぎやすくなります。
ユーザー体験の向上
プログレッシブエンハンスメント: JavaScriptが無効でもフォームが機能し、有効時にはより良い体験を提供します。
アクセシビリティの向上: Server ActionsのJSコードはクライアントに送信されないため、JavaScriptが無効でも機能し、アクセシビリティが向上します。
セキュリティとデータ保護
データセキュリティ: サーバーサイドで処理を行うことで、機密データの取り扱いがより安全になります。
ビジネスリソースの保護: クライアントに公開すべきでないサーバーリソースへのアクセスを必要とする操作を実装できます。
柔軟性と機能性
多様なタスク処理: 単純なデータ取得から複雑なビジネスロジックまで、幅広いタスクを処理できます。
キャッシュデータの再検証: キャッシュされたデータを再検証し、最新の状態を保つことができます。
だそうです
Discussion