React Routerでは、ルートのデータを読むのが loader、フォーム送信などで書き込むのが action です。action が成功すると、画面上の loader は自動で再実行されます。ただし、入力エラーで data() に400を付けて返したときは再実行されません。名前の似た clientLoader と clientAction はブラウザだけで動く別のAPIです。この記事では、最新版のreact-router 8.5.1(2026年10月27日公開)で4つのAPIを動かし、確かめた挙動をコード付きで整理します。
まとめ:actionとloaderの要点
loader=読み取り、action=書き込み。Framework Modeではどちらもサーバで実行され、クライアントバンドルから除去されるclientLoader/clientActionはブラウザ専用。actionとclientActionを両方定義するとclientActionが優先される- actionが2xxで終わると、画面上のloaderが自動で再実行される。
data(…, { status: 400 })で返した入力エラーでは再実行されない - 入力エラーは
data()で返して画面に出し、成功したらredirect()で一覧へ戻す。想定外のエラーはErrorBoundaryに任せる - 8.3.1と7.18.3(2026年8月28日)から、actionの送信元チェックがスキーム(http/https)まで照合するようになった。TLSをプロキシで終端している構成は400になることがある
loaderとclientLoaderの実行タイミングの違い
サーバだけで動くloaderとRoute.LoaderArgs
公式のData Loadingガイドによると、loader はサーバ(またはプリレンダリング時のビルド)で動き、クライアントバンドルから削除されます。DBクライアントやAPIキーを直接書いても、ブラウザへは出力されません。クライアント側で画面を移るときは、React Routerが自動で fetch を送って呼び出します。遷移のたびにネットワーク往復が1回起きる前提で設計してください。
import type { Route } from "./+types/product";
export async function loader({ params }: Route.LoaderArgs) {
return db.getProduct(params.pid);
}
export default function Product({ loaderData }: Route.ComponentProps) {
return <h1>{loaderData.name}</h1>;
}
戻り値はプレーンなオブジェクトで構いません。Framework Modeでは、コンポーネントは useLoaderData を呼ばずにpropsの loaderData で受け取るのが標準です。
ブラウザで動くclientLoaderとHydrateFallback
clientLoader はブラウザだけで実行されます。SPAとして組む場合や、ブラウザにしか無いトークンで外部APIを叩く場合はこちらを使います。loader を置かず clientLoader だけを使う場合、初回ロードでは clientLoader が終わるまでルートコンポーネントが描画されません。その間の空白を埋めるため、同じファイルから HydrateFallback をexportします。
export async function clientLoader({ params }: Route.ClientLoaderArgs) {
const res = await fetch("/api/products/" + params.pid);
return res.json();
}
export function HydrateFallback() {
return <p>読み込み中です</p>;
}
両方定義したときの既定動作とserverLoaderによる合成
両方exportした場合の既定は「初回SSR(またはプリレンダリング)は loader、以降のクライアント遷移は clientLoader」です。初回のハイドレーションでも clientLoader を走らせたいときは clientLoader.hydrate = true as const; を足します。as const を省くと loaderData の型推論が崩れます。
clientLoader の引数には serverLoader が渡るので、サーバのデータにブラウザで取った値を足す合成ができます。
export async function clientLoader({ params, serverLoader }: Route.ClientLoaderArgs) {
const server = await serverLoader();
const stock = await fetch("/api/stock/" + params.pid).then((r) => r.json());
return { ...server, ...stock };
}
クライアント遷移のときは、serverLoader() を呼ぶたびにサーバ往復が1回発生します。clientLoader を足す理由は、ブラウザにしか無い値との合成、ブラウザ側のキャッシュ、BFFを経由せずに外部APIへ直接行く、のどれかがある場合に絞り、理由なくページ全体の取得を clientLoader へ移すのは避けてください。ルート定義の書き方から確認したい場合は、React Router v8の使い方|meta・title設定と基本ルーティング、v7からの変更点で全体像を押さえられます。
actionの実装:Form送信・入力エラー・redirect
Formからactionを呼ぶ最小構成
<Form method="post"> の送信先は、既定では同じルートの action です。受け取りはWeb標準どおり request.formData() で、戻り値はコンポーネントの actionData に入ります。
import { Form } from "react-router";
import type { Route } from "./+types/project";
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
return db.updateProject({ title: formData.get("title") });
}
export default function Project({ actionData }: Route.ComponentProps) {
return (
<Form method="post">
<input type="text" name="title" />
<button type="submit">保存</button>
{actionData ? <p>{actionData.title} を更新しました</p> : null}
</Form>
);
}
data()による入力エラーの返却と400ステータス
入力チェックに失敗したら、例外を投げずに data() でエラー内容を返します。公式のForm Validationガイドは、400を付ける理由を「2xxのときだけページのデータが再検証されるため、400を返すと通常の再検証を防げる」と説明しています。入力ミスのたびに画面上の全loaderを取り直す無駄がなくなります。
import { data, redirect } from "react-router";
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const title = String(formData.get("title") ?? "");
if (title.trim() === "") {
return data({ errors: { title: "タイトルは必須です" } }, { status: 400 });
}
await db.createProject({ title });
return redirect("/projects");
}
// コンポーネント側
{actionData?.errors?.title ? <em>{actionData.errors.title}</em> : null}
成功時のredirectと、ErrorBoundaryに任せるエラー
保存に成功したら redirect() で一覧や詳細へ移します。送信後の画面でリロードしてもPOSTが再送されず、移動先のloaderが最新のデータを取ります。
DB障害のような想定外のエラーは、actionでcatchせずにそのまま投げ、ルートの ErrorBoundary に出させます。公式のError Boundaryガイドは、制御フローのためにわざとエラーを投げることを勧めていません。例外は404のように、ページを描画できないときの throw data("Record Not Found", { status: 404 }) です。入力エラーは return、描画できない状態は throw、と分けて考えます。
action後の自動再検証の実行条件
action結果ごとの再検証の実測
「actionの後は全loaderが再実行される」という説明は、成功したときだけ当てはまります。Node.js 26.5.0・react-router 8.5.1・React 19.3.1のData Mode(createMemoryRouter)で、親ルートと編集ルートにloaderを置き、actionの結果を変えて呼び出し回数を数えました。
| actionの結果 | actionData | ErrorBoundary | 親loader | 同ルートloader |
|---|---|---|---|---|
| オブジェクトを返す | 入る | 出ない | 再実行 | 再実行 |
| data(…, status 400) | 入る | 出ない | なし | なし |
| data(…, status 500) | 入る | 出ない | なし | なし |
| throw new Error() | null | 出る | 再実行 | なし |
| redirect(“/list”) | null | 出ない | 再実行 | 移動先を実行 |
| fetcher経由で成功 | fetcher.dataへ | 出ない | 再実行 | 再実行 |
見落としやすいのは500の行です。data() で500を返してもErrorBoundaryは出ず、actionDataに入るだけです。エラー画面に切り替えたいなら throw します。親ルートの shouldRevalidate に渡る actionStatus は、400と500のときに数値が入り、プレーンなオブジェクトを返したときは undefined でした。
Data ModeとFramework ModeのGET遷移時の再検証
公式のRevalidation Optimizationによると、送信後の再検証の既定はどのモードでも同じです。違うのは <Link> によるGET遷移で、SSRありのFramework Modeでは表示中のloaderがすべて再実行されます。Data ModeとFramework ModeのSPAモードでは、パスのパラメータか検索パラメータが変わったルートだけが再実行されます。同じURLへのGET遷移は、ページの再読み込みと同じ扱いで全loaderが再実行されます。
shouldRevalidateによるloaderの再検証の除外
集計のような重いloaderを、関係のないactionの後まで再実行したくない場合は、そのルートに shouldRevalidate をexportします。常に false を返すとパラメータが変わっても再実行されなくなるので、条件に当てはまらないときは defaultShouldRevalidate を返します。
import type { ShouldRevalidateFunctionArgs } from "react-router";
export function shouldRevalidate({ formAction, defaultShouldRevalidate }: ShouldRevalidateFunctionArgs) {
if (formAction?.startsWith("/favorites")) return false;
return defaultShouldRevalidate;
}
送信1回だけを対象にするなら、<Form> や fetcher.submit に defaultShouldRevalidate={false} を渡す方法もあります。8.5.1では、fetcherのactionがredirectを返したのに後続の遷移が始まって追従しなかった場合も、loaderが再検証されるよう修正されました。
clientActionが優先されるルールと使いどころ
公式のActionsガイドによると、clientAction はブラウザだけで実行され、action と両方定義されていれば clientAction が優先されます。この順位を知らずに両方書くと、サーバ側の更新処理が呼ばれません。サーバ処理も通したい場合は、引数の serverAction を呼びます。
export async function clientAction({ serverAction }: Route.ClientActionArgs) {
projectCache.clear();
return serverAction();
}
公式のClient Dataガイドも、clientAction でブラウザ側のキャッシュを捨ててから serverAction() を呼ぶ形を示しています。clientAction が向くのは、localStorageやIndexedDBの更新、ブラウザにしか無いトークンで叩く外部API、サーバ処理が無いSPAモードです。決済・在庫の引き当て・権限変更を clientAction だけで完結させるのは設計ミスです。検証も更新もブラウザ側にあるので、DevToolsから任意のリクエストを送られると素通りします。
Form・useSubmit・useFetcherの使い分けと送信中の表示
画面遷移の有無による呼び出し方の選択
判断軸は「送信後に画面遷移させたいか」です。<Form> と useSubmit はナビゲーションを起こします。8.5.1で確かめると、別のURLへのPOSTは履歴に1件追加(PUSH)、表示中と同じURLへのPOSTは置き換え(REPLACE)でした。useFetcher の送信はナビゲーションを起こしません。ただしactionが redirect() を返すと、fetcherからでも移動先へ遷移し、履歴に追加されます。一覧の行ごとの「完了にする」ボタンやいいねボタンは useFetcher、送信後に完了画面へ進むフォームは <Form> です。入力・確認・送信の多段フォームは、Reactで確認画面付きフォームを作る方法の設計がそのまま応用できます。
<Form> の送信中は useNavigation() の state が "submitting" になり、formAction に送信先が入ります。fetcherは独立した状態を持つので、fetcher.state !== "idle" で判定します。連打による二重送信は、この判定でボタンを disabled にすれば画面側で抑えられます。ただし画面の制御はサーバでの重複処理までは防がないので、注文のように重複が困る処理はサーバ側でも冪等キーなどで弾きます。
const navigation = useNavigation();
const busy = navigation.state === "submitting" && navigation.formAction === "/projects/new";
<button type="submit" disabled={busy}>{busy ? "送信中…" : "作成"}</button>
fetcher.formDataで作る楽観的UI
サーバの応答を待たずに成功後の表示を先に出すときは、送信中の値が入っている fetcher.formData を使います。専用のライブラリは要りません。
function Task({ task }) {
const fetcher = useFetcher();
let isComplete = task.status === "complete";
if (fetcher.formData) {
isComplete = fetcher.formData.get("status") === "complete";
}
return (
<fetcher.Form method="post">
<button name="status" value={isComplete ? "incomplete" : "complete"}>
{isComplete ? "未完了に戻す" : "完了にする"}
</button>
</fetcher.Form>
);
}
送信が終わると fetcher.formData は空になり、表示はloaderのデータに戻ります。actionが400を返した場合は再検証されないため、元の値に戻ります。ロールバックを自分で書く必要はありません。
loaderとactionが使えるモードとData Modeの書き方
React Routerには3つのモードがあり、データAPIが使えるかはモードで決まります。Declarative Modeでは loader も action も使えません。「react router v7 loader」で調べて情報が食い違うのは、記事ごとに前提のモードが違うためです。
| モード | ルート定義 | loader / action | clientLoader / clientAction | SSR |
|---|---|---|---|---|
| Declarative | JSX(BrowserRouterなど) | 不可 | 不可 | 手動で構成 |
| Data | createBrowserRouter | 可(実行場所は構成次第) | 不可 | 手動で構成 |
| Framework | routes.ts+Viteプラグイン | 可(サーバで実行) | 可 | 組み込み |
Data Modeではルートオブジェクトに loader と action を書きます。次の例は createBrowserRouter でブラウザだけに描画する構成なので、どちらもブラウザで動きます。Data Modeでも createStaticHandler と createStaticRouter を使えばSSRを自分で組めます。値は useLoaderData と useActionData で受け取ります。
import { createBrowserRouter, data, redirect, useActionData, Form } from "react-router";
import { RouterProvider } from "react-router/dom";
const router = createBrowserRouter([
{
path: "/projects/new",
action: async ({ request }) => {
const title = String((await request.formData()).get("title") ?? "");
if (!title) return data({ error: "必須です" }, { status: 400 });
await api.createProject({ title });
return redirect("/projects");
},
Component: NewProject,
},
{ path: "/projects", loader: () => api.listProjects(), Component: ProjectList },
]);
function NewProject() {
const result = useActionData();
return (
<Form method="post">
<input name="title" />
{result?.error ? <em>{result.error}</em> : null}
<button>作成</button>
</Form>
);
}
// 描画は <RouterProvider router={router} />
Framework Modeでは、ルートごとの型がtypegenで生成されます。import type { Route } from "./+types/project"; の1行で Route.ActionArgs や Route.ComponentProps が使え、params の名前の誤りはコンパイル時に見つかります。ルートを変えた直後の型エラーは、開発サーバの起動か react-router typegen の実行で消えます。ルーター自体の選定で迷っているなら、TanStack Routerとは?型安全ルーティングの特徴・導入手順とReact Routerとの違いも判断材料になります。
React Router v8でloader・actionまわりに入った変更
npmの latest は8.5.1(2026年10月27日公開)で、8.0.0は2026年6月17日に出ました。7系も7.18.5(2026年10月27日)まで並行して更新が続いています。v8の最低要件はNode 22.22以上、React 19.2.7以上で、Framework ModeはVite 7以上です。公式のFuture Flagsガイドによると、メジャー版はおおむね年1回で、v9はNode 22のサポートが終わる2027年半ばの見込みです。
middlewareが既定になりcontextはRouterContextProviderに固定
v7で future.v8_middleware の裏にあったmiddlewareは、v8で常に有効になりました。loader・action・middlewareが受け取る context は必ず RouterContextProvider のインスタンスで、素のオブジェクトは渡せません。
import { createContext } from "react-router";
export const userContext = createContext<User | null>(null);
export async function loader({ context }: Route.LoaderArgs) {
const user = context.get(userContext);
return { profile: await getProfile(user) };
}
カスタムサーバでは、getLoadContext が new RouterContextProvider() を返し、context.set() で値を入れる形に書き換えます。
request.urlの正規化廃止と新しいurl引数
v8では、受け取った request がそのまま渡ります。v7まで request.url から取り除かれていた .data サフィックスやindex用の検索パラメータが、v8では残ります。正規化済みのURLは、新しく加わった url 引数から取ります。
// v8
export async function loader({ request, url }: Route.LoaderArgs) {
if (url.pathname === "/path") { }
const isDataRequest = new URL(request.url).pathname.endsWith(".data");
}
データリクエストのURLも、末尾スラッシュを区別するため /a/b/c.data から /a/b/c/_.data に変わりました。CDNやリバースプロキシで .data のパスに条件を書いている設定は直す必要があります。react-router-dom の削除と、metaの引数 data から loaderData への改名もv8の変更です。
8.3.1・7.18.3でのactionの送信元チェックの厳格化
React Routerは7.12.0(2026年1月7日)から、UIルートのactionに届いた Origin ヘッダが request.url と一致しなければ処理を中止しています(CSRF対策)。8.3.0までは比較がホスト(ホスト名とポート)だけでしたが、8.3.1と7.18.3(どちらも2026年8月28日)からはスキームまで含めた比較になりました。createRequestHandler に同じPOSTを送って比べた結果が次の表です。
| Origin → request.url | 8.3.0 | 8.5.1 |
|---|---|---|
| https://app.example.com → https://app.example.com | 200 | 200 |
| https://app.example.com → http://app.example.com | 200 | 400 |
| https://app.example.com → http://localhost:3000 | 400 | 400 |
| Originヘッダなし | 200 | 200 |
2行目は、ロードバランサーやnginxでTLSを終端し、アプリにはhttpで渡している構成です。Expressで trust proxy を有効にしていないと、request.url は http:// で組み立てられます。8.3.1以降へ上げた途端にフォーム送信だけが400になったら、まずここを疑ってください。対処は、プロキシからの X-Forwarded-Proto と X-Forwarded-Host を信頼する設定にするか、react-router.config.ts の allowedActionOrigins に公開ホストを書くかです。
// react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
allowedActionOrigins: ["app.example.com", "*.example.com"],
} satisfies Config;
Express側で直すなら、trust proxy にプロキシの段数かアドレスを指定します(例:プロキシが1段なら app.set("trust proxy", 1))。true にするとクライアントが送った転送ヘッダまで信用するので、プロキシ側で X-Forwarded-* を必ず上書きしている構成に限ります。なお、この送信元チェックはUIルートのactionだけが対象で、リソースルートへのPOSTには掛かりません。
v8へ今すぐ上げるべきでない場面
Node 22.22またはReact 19.2.7へ上げられない環境は、選択の余地なく見送りです。CommonJSでビルドしている構成も、v8がESM専用で公開されているため、バンドラ設定から作り直すことになります。Cloudflare向けに @react-router/dev/vite/cloudflare のdev proxyを使っている場合は、それが削除されたので @cloudflare/vite-plugin への置き換えが要ります。どれにも当たらないなら、7.18.5でfutureフラグを1つずつ有効にして動作を確かめ、8.5.1へ上げるのが早道です。Reactの知見を持つ開発体制が社内に無い場合は、業務用・Webアプリ開発のように移行ごと外部に任せる選択肢もあります。
よくある質問
loaderとclientLoaderはどちらを使うべきですか?
SSRかプリレンダリングで初回表示を速くしたい、またはDBやシークレットを直接扱いたいなら loader です。SPAモードで運用している、またはブラウザにしか無い認証情報で外部APIを叩くなら clientLoader を選び、HydrateFallback もexportしてください。両方定義すると、初回は loader、以降のクライアント遷移は clientLoader が動きます。
actionを呼んだあと、一覧を再取得するコードは必要ですか?
actionが2xxで終われば不要です。<Form>・useSubmit・useFetcher のどれから呼んでも、画面上のloaderが自動で再実行されます。data() で400や500を返したときは再実行されません。エラーでも一覧を取り直したいなら、ステータスはそのままにして、そのルートの shouldRevalidate で actionStatus を見て true を返します。
actionとclientActionを両方定義したらどちらが動きますか?
clientAction が動き、action は自動では呼ばれません。サーバ側も通したいときは、clientAction の中で引数の serverAction() を呼びます。
React 19のform actionやServer ActionsとReact Routerのactionは同じものですか?
別物です。React 19の <form action={fn}> は、送信時に FormData を1つ受け取る関数をコンポーネント側で渡す仕組みです。React Routerの action はルート単位でexportし、request を受け取ってURLに紐づけて呼ばれます。完了後のloader再検証もReact Routerの action だけの動きです。React 19側の書き方はReact 19のform actionの使い方|引数はFormData・よくある誤りで扱っています。
Declarative Modeでもloaderは使えますか?
使えません。<BrowserRouter> と <Routes> で組んだアプリでloaderやactionを使うには、createBrowserRouter のData Modeへ移るか、Framework Modeを採用します。JavaScriptが無くてもフォームが動く設計にしたい場合は、PESPAとは?SPA・MPAとの違いとReact Router v8・Remix 3での実装が参考になります。