React Routerでデータを読むのが loader、書き込むのが action です。名前が似た clientLoader と clientAction はブラウザ側で動く別物で、どちらを書くかでSSRの挙動もバンドルサイズも変わります。この記事では4つのAPIの実行タイミングと使い分けを実コードで整理し、2026年6月17日にリリースされたv8で context と url 引数がどう変わったかまで踏み込みます。
まとめ:actionとloaderの役割分担
loader=読み取り、action=書き込み。actionが完了すると、ページ上の全loaderデータが自動で再検証されるloaderとactionはサーバ専用でクライアントバンドルから除去される。DBクライアントやAPIキーをそのまま書いてよいclientLoader/clientActionはブラウザ専用。両方定義した場合、書き込みはclientActionが優先される- 型は手書きせず、フレームワークモードのtypegenが出す
Route.LoaderArgs/Route.ComponentPropsを使う - npmの最新は 8.3.0(2026年7月22日公開)。v8ではmiddlewareが既定になり、
contextは常にRouterContextProviderになった
以下、4つのAPIの境界線から順に見ていきます。
loaderとclientLoaderの実行タイミングの違い
サーバだけで動くloaderとRoute.LoaderArgs
公式ドキュメントは loader について「クライアントバンドルから削除されるため、ブラウザに含まれる心配なくサーバ専用APIを使える」と明記しています。初回表示ではサーバ(またはプリレンダリング時のビルド)で実行され、クライアント遷移のときはReact Router自身が自動で fetch を投げて呼び出します。つまり遷移のたびにネットワーク往復が1回発生する前提で設計します。
// app/routes/product.tsx
import type { Route } from "./+types/product";
export async function loader({ params }: Route.LoaderArgs) {
// DB接続やAPIキーを直接書ける(ブラウザには出力されない)
return db.getProduct(params.pid);
}
export default function Product({ loaderData }: Route.ComponentProps) {
return <h1>{loaderData.name}</h1>;
}
戻り値は Response でなくプレーンなオブジェクトで構いません。コンポーネントは useLoaderData を呼ばず、props の loaderData で受け取るのがフレームワークモードの標準形です。
ブラウザで動くclientLoaderとHydrateFallback
clientLoader はブラウザだけで実行されます。SPAとして組む場合や、ログイン後にしか呼べない外部APIを叩く場合はこちらです。初回ロード時、ルートコンポーネントは 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>;
}
loader と clientLoader を両方書いたうえで、初回ハイドレーション時にも clientLoader を走らせたいときは clientLoader.hydrate = true as const; を追加します。as const は型推論のために必要で、省くと loaderData の型が崩れます。
両方定義したときの既定動作とserverLoaderによる合成
両方をexportした場合の既定動作は「初回SSR(またはプリレンダリング)は loader、以降のクライアント遷移は clientLoader」です。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() を呼んだ時点でサーバ往復が発生します。在庫数のように更新頻度が高い一部フィールドだけをブラウザで取り直す用途に限定し、ページ全体のデータ取得を clientLoader に移すのは避けてください。ルーティングそのものの書き方から確認したい場合は、React Routerの基本ルーティングとmeta・title設定を先に押さえると全体像がつかめます。
actionとclientActionによるデータ更新と自動再検証
Formからactionを呼ぶ最小構成
action はサーバ側でのデータ更新を担当します。<Form method="post"> の送信先は既定で同じルートで、受け取りは通常のWeb標準どおり request.formData() です。結果はコンポーネントの actionData に入ります。
// app/routes/project.tsx
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const title = formData.get("title");
return db.updateProject({ 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>
);
}
ここで効いてくるのが自動再検証です。公式ドキュメントは「actionが完了すると、ページ上の全loaderデータが再検証され、コードを一切書かなくてもUIがデータと同期する」と説明しています。更新後に一覧を手で再取得するコードは不要で、書いてしまうと二重リクエストになります。特定のルートだけ再検証を止めたいときは、そのルートに shouldRevalidate をexportします。
clientActionが優先されるルールと使いどころ
clientAction はブラウザだけで実行され、action と両方定義されている場合は clientAction が優先されます。この優先順位を知らずに両方書くと、サーバ側の更新処理が呼ばれない事故になります。
export async function clientAction({ request }: Route.ClientActionArgs) {
const formData = await request.formData();
await someApi.updateProject({ title: formData.get("title") });
return { ok: true };
}
使うべき場面ははっきりしています。localStorageやIndexedDBの更新、ブラウザにしか置いていないトークンで直接叩く外部API、そしてSPAモードでサーバ処理そのものが存在しない構成です。逆に、決済・在庫引き当て・権限変更といった改ざんされて困る処理を clientAction だけで完結させるのは設計ミスです。入力値の検証も更新もブラウザ側にあるため、DevToolsから任意のリクエストを送られれば素通りします。
Form・useSubmit・useFetcherの使い分け基準
呼び出し方は3通りあり、判断軸は「画面遷移させたいか」の1点です。<Form> と useSubmit はナビゲーションを起こし、ブラウザの履歴エントリを追加します。useFetcher はナビゲーションも履歴追加も起こしません。
一覧の各行にある「完了にする」ボタンや、いいねボタンのように画面に留まったまま更新するUIは useFetcher です。逆に、フォーム送信後に完了画面へ進む流れは <Form> を使います。入力から確認、送信までの多段フォームをどう組むかは、確認画面付きフォームの実装とデータ保持で扱っている設計がそのまま応用できます。
loaderData・actionDataの型安全とtypegenの生成タイミング
フレームワークモードでは、ルートごとの型がtypegenで自動生成されます。import type { Route } from "./+types/product"; の1行で、そのルート専用の Route.LoaderArgs・Route.ActionArgs・Route.ComponentProps が使えます。params.pid はルートパスの定義から型付けされるので、パラメータ名のtypoはコンパイル時に落ちます。
注意点は、生成物がビルド時に作られることです。ルート定義を変えた直後にエディタが型エラーを出す場合、多くは型生成が走っていないだけで、開発サーバの起動または react-router typegen の実行で解消します。useLoaderData を手動ジェネリクスで型付けする書き方はv7以降の推奨ではありません。propsの loaderData に寄せてください。
fetcher.formDataで作る楽観的UI
楽観的UIは、サーバの応答を待たずに成功した状態を先に描く手法です。React Routerでは専用のライブラリを足す必要がなく、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 は消え、表示は元のデータに戻ります。ロールバック処理を自分で書く必要はありません。単に処理中であることを示したいだけなら fetcher.state !== "idle"、ページ遷移中のグローバルなスピナーなら useNavigation の navigation.location を見ます。
loaderとactionが使えるモードの境界
React Routerには3つのモードがあり、データAPIが使えるかどうかはここで決まります。Declarative Modeでは loader も action も使えません。「react router data router」で検索して情報が食い違うのは、参照している記事のモードが違うためです。
| モード | ルート定義 | loader / action | clientLoader / clientAction | SSR・プリレンダリング |
|---|---|---|---|---|
| Declarative | JSX(BrowserRouter) | 不可 | 不可 | 組み込みなし |
| Data | createBrowserRouter | 可 | 不可 | 組み込みなし |
| Framework | routes.ts+Viteプラグイン | 可 | 可 | 組み込みあり |
Data Modeはルート定義をReactのレンダリングの外へ出すことで loader・action・useFetcher を有効にする層で、Framework ModeはそのData ModeをViteプラグインで包み、型安全な href、ルートモジュールAPI、コード分割、SSRと静的プリレンダリングを足したものです。clientLoader と clientAction はルートモジュールのexportとして定義するAPIなので、Framework Modeの機能と考えて構いません。ルーターの選定段階で迷っているなら、TanStack Routerの特徴と採用理由との比較も判断材料になります。
React Router v8でloader・actionまわりに入った変更
npmの latest は 8.3.0(2026年7月22日公開)で、8.0.0のリリースは2026年6月17日です。v7.0.0が2024年11月22日ですから、約1年7か月ぶりのメジャー更新にあたります。v8からは年次のメジャーリリースへ移行し、最低要件はNode 22.22以上、React 19.2.7以上、フレームワークモードはVite 7以上に引き上げられました。日本語の解説記事はv7時点で止まっているものが大半なので、ここは実際に手を動かす前に確認しておく価値があります。
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() で値を詰める形に書き換えます。react-router-serve をそのまま使っているなら変更は不要です。
request.urlの正規化廃止と新しいurl引数
future.v8_passThroughRequests の既定化で、request は受信したものがそのまま渡るようになりました。従来 request.url から .data サフィックスやindex用の検索パラメータが取り除かれていた前提のコードは、v8では意図せずデータリクエストのURLを見ることになります。正規化済みのURLは新しい url 引数から取ります。
// v7まで
export async function loader({ request }: Route.LoaderArgs) {
const url = new URL(request.url);
if (url.pathname === "/path") { }
}
// 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 に、ルートの /_root.data は /_.data になりました。CDNやリバースプロキシで .data のパスをマッチさせている設定は書き換えが必要です。
react-router-dom廃止とmetaのdata→loaderData
v6のimportパスを延命していた react-router-dom が削除されました。Link や useLocation は react-router から、RouterProvider は react-router/dom からimportします。meta と useMatches() の data フィールドも廃止され、loaderData に統一されました。
// v7まで
export function meta({ data }: Route.MetaArgs) {
return [{ title: data.title }];
}
// v8
export function meta({ loaderData }: Route.MetaArgs) {
return [{ title: loaderData.title }];
}
v8へ今すぐ上げるべきでない場面
移行そのものは軽い部類です。公式も破壊的変更はいずれもv7の時点でfutureフラグを立てて先行対応できると案内しています。それでも、次の条件に当てはまるなら見送りが妥当です。
第一に、Node 22.22またはReact 19.2.7へ上げられない環境。これは選択の余地がない足切りです。第二に、CommonJSのままビルドしている構成。v8はESM専用として公開されているため、バンドラ設定ごとの作業になります。第三に、Cloudflare向けに @react-router/dev/vite/cloudflare のdev proxyを使っている場合。これは削除され、Cloudflare公式の @cloudflare/vite-plugin への差し替えが前提になります。いずれも該当しないなら、v7系の最新である7.18.1(2026年6月29日公開)でfutureフラグを順に有効化し、動作を確認してから8系へ上げるのが最短です。
よくある質問
loaderとclientLoaderはどちらを使うべきですか?
SSRまたは静的プリレンダリングで初回表示を速くしたい、DBやシークレットを直接触りたいなら loader です。loader はクライアントバンドルから除去されるため、APIキーが漏れる心配がありません。SPAモードで運用している、あるいはブラウザにしか無い認証情報で外部APIを叩くなら clientLoader を選び、初回描画の空白を避けるために HydrateFallback を必ずexportしてください。両方定義した場合は初回SSRが loader、以降のクライアント遷移が clientLoader になります。
actionを呼んだあと、一覧を再取得するコードは必要ですか?
不要です。<Form>・useFetcher・useSubmit のいずれから呼んだ場合も、action の完了時にページ上の全 loader データが自動で再検証されます。手動で再取得を書くと同じデータを二重に取りに行くことになります。逆に、重いクエリを持つルートだけ再検証を抑止したいときは、そのルートに shouldRevalidate をexportして条件を返します。
actionとclientActionを両方定義したらどちらが動きますか?
clientAction が優先されます。サーバ側の action は呼ばれません。両方書く構成は、ブラウザ側で先にバリデーションやキャッシュ更新を行い、そのうえでサーバ処理へ流したい場合に限られます。改ざんされて困る処理をクライアント側だけで完結させるのは避けてください。
React Router v8への移行は大変ですか?
破壊的変更はいずれもv7の段階でfutureフラグを立てて事前に対応できる設計になっています。実作業は react-router-dom のimport差し替え、meta の data を loaderData へ改名、カスタムサーバの getLoadContext が RouterContextProvider を返すことの確認、この3点が中心です。むしろ重いのはNode 22.22以上・React 19.2.7以上・Vite 7以上という実行環境側の要件で、ここが満たせるかを先に確認してください。
Declarative Modeでもloaderは使えますか?
使えません。loader と action はData Mode以上で有効になります。BrowserRouter と Routes / Route をJSXで書くDeclarative Modeは、URLとコンポーネントの対応、Link、useNavigate、useLocation といった基本機能に限定されます。データAPIを使いたい場合は createBrowserRouter によるData Mode、または routes.ts とViteプラグインを使うFramework Modeへ移行してください。