はじめに
前回の記事では、データ取得を loader に任せる方法を解説しました。
今回はその対になる機能、「データの更新(作成・編集・削除)」 を担う action について解説します。
この記事で分かること
- React Router v7 における
actionの基礎概念 -
<Form>コンポーネントとactionを組み合わせたデータ送信方法 - 送信後の自動Re-Validation(データの再取得)の仕組み
- 言葉の意味として
zodのvalidation(検証)と被りがちなのでややこしい…
- 言葉の意味として
action による宣言的なデータ更新
従来のReactでは、フォーム送信時にonSubmitでデータ更新処理を呼び出しuseStateで管理している値をfetchやaxiosで送信するのが一般的でした。
React Router v7 の Data モードでは、action という関数を定義することで、ブラウザの標準的なフォーム送信に近い挙動を、JSの力で最適化して実行できます。
action の大きなメリット:自動リバリデーション
action が完了すると、React Router は現在画面に表示されている すべての loader を自動的に再実行します。
これにより、データを更新した後に
- 手動でステートを更新する
- 再度 API を叩きに行く
- 画面をリロードする
といった処理を書く必要がなくなり、 画面上のデータが常に最新の状態に同期されます。
実装例:ユーザー情報の更新
特定のユーザーの名前を更新する簡単なフォームを例に解説します。
1. action の作成
送信されたフォームデータを受け取り、API に投げる action を定義します。
import { ActionFunctionArgs, redirect } from "react-router";
export async function userUpdateAction({ request, params }: ActionFunctionArgs) {
// 1. フォームデータの取得
const formData = await request.formData();
const updates = Object.fromEntries(formData);
// 2. APIへのリクエスト(例)
await fetch(`https://jsonplaceholder.typicode.com/users/${params.userId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updates),
});
// 3. 更新後は詳細画面などにリダイレクト
return redirect(`/users/${params.userId}`);
}
2. Component の作成(Form コンポーネント)
React Router が提供する <Form> コンポーネントを使用します(ある程度複雑なアプリではReact-Hook-Formを使うことになると思います)。これを使うことで、複雑な onSubmit ハンドラは不要になります。
import { Form, useLoaderData, useNavigation } from "react-router";
export default function UserEdit() {
const user = useLoaderData() as any;
const navigation = useNavigation();
// 送信中の状態を判定
const isSubmitting = navigation.state === "submitting";
return (
<div>
<h2>ユーザー編集</h2>
<Form method="post">
<label>
名前:
<input type="text" name="name" defaultValue={user.name} />
</label>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "保存中..." : "保存"}
</button>
</Form>
</div>
);
}
3. Route Object の定義(main.tsx)
loader と同様に、ルート定義に action を紐付けます。
import { createBrowserRouter, RouterProvider } from "react-router";
import RootLayout from "./layouts/RootLayout";
import App from "./App";
import UserDetails from "./pages/UserDetails";
import { userDetailsLoader } from "./loaders/userDetailsLoader";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import UserEdit from "./pages/UserEdit";
import { userUpdateAction } from "./actions/userUpdateAction";
const router = createBrowserRouter([
{
path: "/",
Component: RootLayout,
children: [
{
index: true,
Component: App,
},
{
path: "users/:userId",
Component: UserEdit,
loader: userDetailsLoader,
action: userUpdateAction,
},
{
path: "users",
Component: UserDetails,
loader: userDetailsLoader,
},
],
},
]);
const root = document.getElementById("root");
createRoot(root!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
)
まとめ
-
actionは データ更新の責任をコンポーネントから分離 できる(すごい
) -
<Form>を使うことで、preventDefaultやuseState管理から解放される - Re-Validation のおかげで、更新後のデータ整合性を気にする必要がなくなる
loader で取得し、action で更新し、再度loaderを呼び出す。このサイクルに乗ることで、コンポーネントのコードがシンプルになります。
蛇足
公式ドキュメントにある通り、Loaderを呼び出すかどうかの調整も可能です。また、Loaderが再度読み込まれるのは次のような場合です。
A route loader is revalidated when:
- its own route params change
- any change to URL search params
- after an action is called and returns a non-error status code