React Routerのローダーで、表示する前にデータを読み込む方法
React Router のデータモードでは、ルートに loader を書いておくと、そのルートのコンポーネントを描画する前にデータが読み込まれます。コンポーネントは useLoaderData で読み込み済みのデータを受け取るだけなので、useEffect で取得して「まだデータがない状態」を描画する必要がありません。
筆者は検証環境で、猫の一覧・猫のページ・ごはんの記録という3段のルートを作り、ローダーが呼ばれる時刻とコンポーネントが描画される時刻を記録して確かめました。
検証環境は Ubuntu 24.04、Node.js 22.22.0、Chromium 141 です。使ったのは react-router 8.5.0、react / react-dom 19.3.1、@types/react 19.3.0、typescript 7.0.2 です。データはサーバーの代わりに、決まった時間(300〜800ミリ秒)だけ待ってから返す関数で用意しました。ブラウザでの確認は、esbuild で変換したコード(開発用のビルド)を Chromium で動かし、Playwright で操作して行いました。
ルートに loader を書き、useLoaderData で受け取る
ローダーを使うには、公式のインストール手順のとおり、createBrowserRouter でルーターを作り、RouterProvider に渡します。ルートの定義に loader と Component を並べて書きます。この記事のコードは、検証に使ったコードから、時刻を記録する部分、エラーを試すためのルートやリンク、一部の import を省いたものです。
import { createRoot } from 'react-dom/client';
import { createBrowserRouter } from 'react-router';
import { RouterProvider } from 'react-router/dom';
const router = createBrowserRouter([
{
path: '/',
loader: rootLoader,
Component: Root,
HydrateFallback: Loading,
ErrorBoundary: ErrorPage,
children: [
{ index: true, Component: Home },
{
path: 'cats/:catId',
loader: catLoader,
Component: CatPage,
ErrorBoundary: ErrorPage,
children: [{ path: 'meals', loader: mealsLoader, Component: Meals }],
},
],
},
]);
createRoot(document.getElementById('root')!).render(<RouterProvider router={router} />);
ローダーは、データを返す関数です。URL の :catId の部分は params.catId で受け取れます。
import { Link, Outlet, useLoaderData, type LoaderFunctionArgs } from 'react-router';
export async function catLoader({ params, request }: LoaderFunctionArgs) {
const cat = await fetchCat(params.catId!, request.signal);
// 見つからなかったときの処理は、後のエラーの節で足します
return { cat };
}
export function CatPage() {
const { cat } = useLoaderData<typeof catLoader>();
return (
<section>
<h1>{cat.name}</h1>
<p>毛の色:{cat.color}</p>
<Link to={`/cats/${cat.id}/meals`}>ごはんの記録</Link>
<Outlet />
</section>
);
}
useLoaderData のリファレンスにあるように、useLoaderData<typeof catLoader>() と書くと、ローダーが返す値の型が付きます。検証で、cat.name を cat.nam と書き間違えると、型チェックで次のエラーになりました。
errors/WrongField.tsx(6,18): error TS2551: Property 'nam' does not exist on type 'Cat'. Did you mean 'name'?
fetchCat は検証用に作った関数で、600ミリ秒待ってから猫のデータを返します。実際のアプリでは、ここで fetch を呼びます。
入れ子のルートのローダーは、同時に読み込みを始める
/cats/1/meals を直接開くと、URL に合うルートが3つ(一覧・猫のページ・ごはんの記録)あります。検証環境で、それぞれのローダーの開始と終了、コンポーネントの描画の時刻を記録しました。左の数字は、ページを開いてからの経過時間です。
7ms rootLoader: 開始
7ms catLoader(1): 開始
7ms mealsLoader(1): 開始
7ms createRoot して描画を始める
18ms Loading(HydrateFallback)を描画
307ms rootLoader: 終了
608ms catLoader(1): 終了
808ms mealsLoader(1): 終了
809ms router: state=idle location=/cats/1/meals
812ms Root を描画(navigation.state = idle)
815ms CatPage を描画(たま)
816ms Meals を描画(2件)
3つのローダーは同じ時刻に始まりました。親のローダーが終わるのを待ってから子のローダーを始める、という順番待ちにはなっていません。dataStrategy の説明にも、React Router は既定ではすべてのローダーを並行して実行すると書かれています。いちばん遅い 800ミリ秒のローダーが終わったところで、3つのコンポーネントがまとめて描画されました。

ローダーを待っているあいだは、HydrateFallback に渡した Loading コンポーネントが表示されました。ページを開いてから 100ミリ秒後の画面は「最初の読み込み中…」だけでした。HydrateFallback は、最初にページを開いたときの読み込みのあいだに表示するものです。
データを読み込み終える前に、CatPage や Meals が描画されることはありませんでした。コンポーネントの中で「データがまだないとき」の分岐を書かずに済みます。
ページを移動するときは、前の画面のまま待つ
次に、/ を開いてから「たま」のリンクを押し、/cats/1 へ移動しました。押してから約200ミリ秒後には、URL はまだ / のままで、前の画面(「猫を選んでください」)が表示されていました。新しいページは、ローダーが終わってから表示されます。
383ms router: state=loading location=/
383ms catLoader(1): 開始
390ms Root を描画(navigation.state = loading)
983ms catLoader(1): 終了
984ms router: state=idle location=/cats/1
985ms Root を描画(navigation.state = idle)
990ms CatPage を描画(たま)
待っているあいだに何も変わらないと、押した人に、リンクが効いたことが伝わりません。読み込み中の表示(Pending UI)についてのガイドでは、useNavigation で読み込み中かどうかを調べる方法が紹介されています(データモードでも同じだと、データモード向けのページに書かれています)。検証では、一覧のコンポーネントに次の表示を入れました。
import { Link, Outlet, useLoaderData, useNavigation } from 'react-router';
export function Root() {
const { cats } = useLoaderData<typeof rootLoader>();
const navigation = useNavigation();
return (
<div>
<nav>
{cats.map((cat) => (
<Link key={cat.id} to={`/cats/${cat.id}`}>
{cat.name}
</Link>
))}
</nav>
{navigation.state === 'loading' && <p className="pending">読み込み中…</p>}
<Outlet />
</div>
);
}
リンクを押すと navigation.state が loading になり、前の画面の上に「読み込み中…」が出ました。

このとき、一覧の rootLoader は呼び直されていません。続けて「ごはんの記録」を押して /cats/1/meals へ移動したときも、呼ばれたのは mealsLoader だけでした。ルートオブジェクトの説明によると、データモードでは、ローダーが呼び直されるのは、そのルートの URL パラメーターが変わったとき、クエリ文字列が変わったとき、アクションがエラーではないステータスコードを返したあとの3つです。移動先で新しく表示されるルートのローダーは、そのつど呼ばれます。
見つからないときは 404 を投げ、ErrorBoundary で表示する
ローダーの中でデータが見つからないときは、data() でステータスコードを付けて投げます。エラーの扱いのガイドでも、404 はこの書き方が紹介されています。
import { data } from 'react-router';
export async function catLoader({ params, request }: LoaderFunctionArgs) {
const cat = await fetchCat(params.catId!, request.signal);
if (!cat) {
throw data('猫が見つかりません', { status: 404 });
}
return { cat };
}
投げたものは、ルートの ErrorBoundary に渡したコンポーネントで、useRouteError を使って受け取ります。data() で投げたものは isRouteErrorResponse で見分けられ、status と data が入っています。
import { isRouteErrorResponse, useRouteError } from 'react-router';
export function ErrorPage() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<p role="alert">
{error.status}:{error.data}
</p>
);
}
const message = error instanceof Error ? error.message : '不明なエラー';
return <p role="alert">エラー:{message}</p>;
}
検証で /cats/99(いない猫)を開くと、「404:猫が見つかりません」と表示されました。上のリンクの並びは残っています。猫のページのルートに付けた ErrorBoundary がエラーを表示し、一覧のルートはそのまま描画されたためです。
ローダーの中で throw new Error('データの読み込みに失敗しました') としたルートを開いたときも、投げたエラーは同じ ErrorPage に渡り、「エラー:データの読み込みに失敗しました」と表示されました。
移動の途中で別のリンクを押すと、前の読み込みは中止される
ローダーは引数で request を受け取ります。上の catLoader では、request.signal を fetchCat に渡していました。検証用の fetchCat は、この signal が中止されたら待つのをやめて、ログを残すように作ってあります。
「くろ」を押し、読み込みが終わる前(約100ミリ秒後)に「しろ」を押しました。
389ms catLoader(2): 開始
396ms Root を描画(navigation.state = loading)
519ms fetchCat(2): 中止された(signal.aborted = true)
519ms router: state=loading location=/
519ms catLoader(3): 開始
529ms Root を描画(navigation.state = loading)
1120ms catLoader(3): 終了
1120ms router: state=idle location=/cats/3
1121ms Root を描画(navigation.state = idle)
1122ms CatPage を描画(しろ)
「しろ」を押した時点で、「くろ」の読み込みの signal が中止され、「くろ」のページは一度も描画されませんでした。最後に表示されたのは「しろ」のページです。操作が重なったときの扱いについての公式の説明にも、移動の途中で別の移動が始まると、前の移動のリクエストを中止すると書かれています。実際のアプリでは、公式の例のように fetch に signal: request.signal を渡しておけば、中止されたときにブラウザの通信も打ち切られます。検証では fetch は使っておらず、この部分は試していません。
同じ説明によると、ブラウザ側で中止しても、すでに送ったリクエストはサーバーに届きます。ブラウザ側の中止で解放されるのはブラウザの資源だけで、リクエストがサーバーに届くのは止められません。
ローダーに読み込みを任せ、コンポーネントは表示に専念する
React Router のデータモードでは、ルートに loader を書き、コンポーネントで useLoaderData<typeof loader>() を呼ぶだけで、表示する前にデータを読み込めます。検証では、入れ子のルートのローダーは同時に始まり、すべて終わってから描画されました。移動中は前の画面のまま待つので、useNavigation で読み込み中の表示を出します。見つからないときは data() で 404 を投げて ErrorBoundary で表示し、検証では、途中で別のページに移ると、前の読み込みの request.signal が中止されました。公式の説明では、fetch に signal を渡しておけば通信も打ち切られます(この部分は検証していません)。





