- Angular
Angularリゾルバーの使い方|ResolveFnでデータを先に読み込む
リゾルバー(resolver)は、ルートの画面を表示する前にデータを読み込む関数です。 ResolveFn<T> 型の関数を作り、ルート定義の resolve に書くと、ルーターはその結果がそろうまで画面の切り替えを待ちます。コンポーネントは、最初からデータがある状態で作られます。
// item.resolver.ts
export const itemResolver: ResolveFn<Item> = (route) => {
const id = Number(route.paramMap.get('id'));
return inject(ItemStore).getItem(id); // Observable・Promise・値のどれでもよい
};
// app.routes.ts
{ path: 'items/:id', component: ItemDetail, resolve: { item: itemResolver } }
// item-detail.ts(withComponentInputBinding() を有効にしておく)
export class ItemDetail {
item = input.required<Item>(); // resolve の item がここに入る
}
/items から /items/1 へ移動したときの、実際の実行順です(API は 500ms かかる作りにしています)。
0ms NavigationStart /items/1
2ms GuardsCheckEnd /items/1
3ms ResolveStart /items/1
3ms itemResolver 開始 id=1
4ms ItemStore.getItem(1) 開始(1回目)
505ms itemResolver 完了 id=1
505ms ResolveEnd /items/1
508ms ItemDetail constructor
511ms NavigationEnd /items/1
ガード → リゾルバー → コンポーネントの作成、の順に動き、コンポーネントはデータがそろった後(505ms の後)に作られました。 待っている間は前の画面のままで、アドレスバーの URL も /items のままでした。
確認環境:Angular 22.2.2(@angular/core・@angular/router・@angular/cli)、TypeScript 6.0.3、Node.js 24.21.0(Linux)。ng new のプロジェクトにこの記事のコードを入れて ng build し、Chromium(Playwright 1.56.1)で画面を操作して、ルーターのイベントとリゾルバーのログを記録しました。API の代わりに、delay() で 300〜500ms 待つサービスを使っています(確認日 2026年10月12日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angularリゾルバーの使い方|ResolveFnでデータを先に読み込む」
完成コード
一覧(/items)と詳細(/items/:id)の2画面で、詳細のデータをリゾルバーで読み込みます。データが無いときは一覧へ戻します。
データを取るサービス
// item-store.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export interface Item { id: number; name: string; }
@Injectable({ providedIn: 'root' })
export class ItemStore {
private http = inject(HttpClient);
getItem(id: number) {
return this.http.get<Item>(`/api/items/${id}`);
}
}
HttpClient の使い方は関連記事「Angular HttpClientの使い方|GET・POSTとエラー処理」で扱っています。
リゾルバー(エラー時は RedirectCommand で移動)
// item.resolver.ts
import { inject } from '@angular/core';
import { RedirectCommand, ResolveFn, Router } from '@angular/router';
import { catchError, of } from 'rxjs';
import { Item, ItemStore } from './item-store';
export const itemResolver: ResolveFn<Item | RedirectCommand> = (route) => {
const store = inject(ItemStore);
const router = inject(Router); // inject は最初に全部済ませる
const id = Number(route.paramMap.get('id'));
return store.getItem(id).pipe(
catchError(() => of(new RedirectCommand(router.parseUrl('/items?error=not-found')))),
);
};
ResolveFn<T> の引数は、移動先のルートの情報(ActivatedRouteSnapshot)と、ルーター全体の状態(RouterStateSnapshot)です。関数は注入コンテキストで呼ばれるので inject() が使えます。ただし pipe() の中や await の後では使えないので、先頭で受け取ります(関連記事「NG0203の直し方|inject()とinjection context」で扱っています)。
ルート定義と app.config.ts
// app.routes.ts
import { Routes } from '@angular/router';
import { ItemList } from './item-list';
import { ItemDetail } from './item-detail';
import { itemResolver } from './item.resolver';
export const routes: Routes = [
{ path: 'items', component: ItemList },
{ path: 'items/:id', component: ItemDetail, resolve: { item: itemResolver } },
{ path: '', redirectTo: 'items', pathMatch: 'full' },
];
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()), // ← resolve の結果を input に入れる
provideHttpClient(),
],
};
コンポーネント
// item-detail.ts
import { Component, input } from '@angular/core';
import { Item } from './item-store';
@Component({
selector: 'app-item-detail',
template: `<h2>{{ item().name }}</h2>`,
})
export class ItemDetail {
item = input.required<Item>();
}
// item-list.ts
import { Component, input } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-item-list',
imports: [RouterLink],
template: `
@if (error() === 'not-found') { <p>商品が見つかりませんでした</p> }
<a routerLink="/items/1">商品1</a>
<a routerLink="/items/999">商品999</a>`,
})
export class ItemList {
error = input<string>(); // ?error=not-found が入る
}
provideRouter・ルートパラメータの基本は、関連記事「Angularルーティング設定|provideRouter・パラメータ・404」で扱っています。
結果の受け取り方:input と ActivatedRoute
| 受け取り方 | 書き方 | 向いている場面 |
|---|---|---|
input()(withComponentInputBinding() が要る) |
item = input.required<Item>() |
新しく書くコード(第一候補) |
ActivatedRoute の data |
toSignal(inject(ActivatedRoute).data) |
withComponentInputBinding() を使わない場合 |
// ActivatedRoute から受け取る場合
private route = inject(ActivatedRoute);
item = toSignal(this.route.data.pipe(map(d => d['item'] as Item)), { requireSync: true });
withComponentInputBinding() を有効にすると、ルートパラメータ(:id)・クエリパラメータ(?error=)・ルートの data・リゾルバーの結果が、同じ名前の input() に入ります。上の ItemList の error は、クエリパラメータから入りました。@angular/router の型定義のコメントでは、同じ名前が重なったときはリゾルバーの結果がいちばん優先され、値が無い input には undefined が入ると説明されています。input() の基本は関連記事「Angular input・output・modelの使い方|親子の受け渡し」で扱っています。
エラーのとき
RedirectCommand で別の画面へ移す
完成コードの itemResolver で /items/999(無い商品)へ移動したときのログです。
0ms NavigationStart /items/999
1ms ResolveStart /items/999
2ms itemResolver 開始 id=999
303ms itemResolver 失敗: 404 Not Found → /items へリダイレクト
304ms NavigationCancel /items/999 code=0 NavigationCancelingError: Redirecting to "/items?error=not-found"
0ms NavigationStart /items?error=not-found
3ms NavigationEnd /items?error=not-found
RedirectCommand を返すと、今の移動は取り消され(NavigationCancel)、指定した URL への移動が新しく始まりました。 一覧の画面に「商品が見つかりませんでした」が出ました。RedirectCommand の第2引数には、{ replaceUrl: true } のような移動のオプション(NavigationBehaviorOptions)を渡せます。
何もしないとどうなるか
catchError を付けないリゾルバーで同じ移動をすると、次のようになりました。
310ms NavigationError /raw/999 Error: 404 Not Found
ERROR Error: 404 Not Found
移動は失敗し、画面は一覧のまま、コンソールにエラーが出ただけでした。 利用者には何も起きていないように見えます。
まとめて扱う:withNavigationErrorHandler
リゾルバーごとに catchError を書かず、移動のエラーを1か所で扱う方法もあります。angular.dev のリゾルバーの説明にある形です。
provideRouter(
routes,
withComponentInputBinding(),
withNavigationErrorHandler((error) => {
const router = inject(Router);
console.error('Navigation error:', error.error?.message);
router.navigate(['/error']);
}),
);
| 方法 | 向いている場面 |
|---|---|
リゾルバーで catchError → RedirectCommand |
エラーごとに移動先を変えたい(404 は一覧へ、など) |
リゾルバーで catchError → 代わりの値 |
データが無くても画面は出したい |
withNavigationErrorHandler |
全部のリゾルバー・ガードのエラーを同じ画面に送る |
HTTP のエラーを共通で扱う方法は、関連記事「Angularエラーハンドリング|ErrorHandlerとHTTP共通処理」で扱っています。
ローディング表示
リゾルバーが動いている間は前の画面のままなので、何かを表示しないと、クリックが効いていないように見えます。v22 では Router の currentNavigation が Signal(値の変化を知らせる入れ物)です。
@Component({
selector: 'app-root',
imports: [RouterOutlet],
template: `
@if (loading()) { <div class="loading-bar">読み込み中…</div> }
<router-outlet />`,
})
export class App {
private router = inject(Router);
loading = computed(() => !!this.router.currentNavigation());
}
クリックの 100ms 後には「読み込み中…」が出ていて、移動が終わると消えました。getCurrentNavigation() は v20.2 で非推奨になり、currentNavigation に置き換わっています(@angular/router の型定義による)。
Router のイベントで書く場合は次の形です。NavigationCancel・NavigationError でも消すのを忘れないようにします(上のリダイレクトでは NavigationCancel が出ました)。
loading = toSignal(
inject(Router).events.pipe(
filter(e => e instanceof NavigationStart || e instanceof NavigationEnd
|| e instanceof NavigationCancel || e instanceof NavigationError),
map(e => e instanceof NavigationStart),
),
{ initialValue: false },
);
runGuardsAndResolvers:いつリゾルバーが動き直すか
同じルートの中で URL が変わったとき(/items/1 → /items/1?tab=a など)に、リゾルバーを動かし直すかを決める設定です。
| 値 | 動き直す条件 |
|---|---|
paramsChange(既定) |
パスかパスのパラメータ(マトリックスパラメータを含む)が変わったとき |
pathParamsChange |
パスのパラメータが変わったとき |
paramsOrQueryParamsChange |
上に加えてクエリパラメータが変わったとき |
pathParamsOrQueryParamsChange |
パスのパラメータかクエリパラメータが変わったとき |
always |
毎回 |
関数 (from, to) => boolean |
関数が true を返したとき |
/items/1 から、クエリパラメータだけを ?tab=a → ?tab=b と変えたときの、ItemStore.getItem の呼び出し回数です。
| 設定 | 呼ばれた回数 | 画面の切り替え |
|---|---|---|
既定(paramsChange) |
1回(最初だけ) | すぐ(約3ms で NavigationEnd) |
paramsOrQueryParamsChange |
3回(最初+タブ2回) | 毎回 約500ms 待つ |
{
path: 'items/:id',
component: ItemDetail,
resolve: { item: itemResolver },
runGuardsAndResolvers: 'paramsOrQueryParamsChange',
}
クエリパラメータで並び順や絞り込みを持つ画面では、動き直さないと前のデータのまま、動き直すと毎回待たされます。 どちらが合うかで選びます。
親子のリゾルバーは順番に動く
親ルートと子ルートの両方にリゾルバーがあると、親が終わってから子が動きます。 子は route.parent?.data で親の結果を使えます。
{
path: 'items/:id',
component: ItemDetail,
resolve: { item: itemResolver },
children: [{
path: 'reviews',
component: ItemReviews,
resolve: {
reviews: (route: ActivatedRouteSnapshot) => {
const item = route.parent?.data['item'] as Item; // 親の結果
return inject(ItemStore).getReviews(item.id);
},
},
}],
}
/items/2/reviews を直接開いたときのログです(親 500ms・子 300ms)。
3ms ResolveStart /items/2/reviews
6ms itemResolver 開始 id=2
509ms itemResolver 完了 id=2
509ms reviewsResolver 開始(親の item.id=2)
810ms ResolveEnd /items/2/reviews
813ms ItemDetail constructor
合計で約 810ms 待ちました。angular.dev でも、親子のリゾルバーは順番に動き、階層ごとに待ち時間が足されると説明されています。
比較:ルートの resources(v22.2 の developer preview)
v22.2 では、ルート定義に resources を書いて、Resource(Signal でデータを持つしくみ)でデータを読み込む方法が加わりました(withRouterResources() を provideRouter に足す。型定義では @developerPreview 22.2)。同じ親子の構成を resources で書くと、親と子の読み込みが同時に始まりました。
{
path: 'r/:id',
component: ItemDetail,
resources: (ctx) => {
const store = inject(ItemStore);
return {
item: rxResource({
params: () => Number(ctx.params()['id']),
stream: ({ params: id }) => store.getItem(id),
}),
};
},
children: [ /* reviews も同じ形で resources に書く */ ],
}
10ms resources item 開始 id=3
14ms ItemStore.getItem(3) 開始
14ms ItemStore.getReviews(3) 開始
516ms ItemDetail constructor
リゾルバーの約 810ms に対し、約 516ms で画面が出ました。 developer preview(今後 API が変わる可能性がある段階)なので、使うときは版ごとの変更を確かめます。
@defer・httpResource との使い分け
| 方法 | 画面の切り替え | ローディング表示の置き場所 | 向いている場面 |
|---|---|---|---|
リゾルバー(ResolveFn) |
データがそろうまで待つ | アプリ全体(ルーター) | 無いと画面が成り立たないデータ |
ルートの resources |
既定は待つ。nonBlocking() で待たない |
待たない場合はコンポーネント | Signal ベースで、親子を並べて読みたい |
httpResource |
すぐ切り替える | コンポーネントの中 | 一部が後から出てもよいデータ |
@defer |
すぐ切り替える | テンプレートの @placeholder |
重い部品・画面の下の方の部品を後で読む |
| 判断の目安 | 選ぶもの |
|---|---|
| 無い商品なら別の画面へ移したい(404 など) | リゾルバー+RedirectCommand |
| SSR でデータの入った HTML を返したい | リゾルバー(angular.dev が利点に挙げている) |
| 一覧の上部だけ先に出し、残りは後で | httpResource |
| データではなく部品の JS を後で読みたい | @defer |
httpResource は関連記事「Angular httpResource使い方|v22安定版の実例とテスト」、@defer は関連記事「Angular @defer 使い方|トリガー早見表と遅延されない例を実測」、resource() は関連記事「Angular resourceとlinkedSignalの使い方と使い分け」で扱っています。
よくある質問
Q. クラスの Resolve インターフェースはまだ使えますか。
A. v22.2.2 にも Resolve インターフェースは残っています。angular.dev の説明は関数の ResolveFn で書かれていて、クラスで書いた既存のリゾルバーは mapToResolve で関数に変換して使えます。
Q. リゾルバーの中でローディングを出したい。
A. リゾルバーは画面の外で動くので、上の「ローディング表示」のように、アプリ全体(App など)でルーターの状態を見て出します。
Q. ガードとどちらが先に動きますか。
A. ログのとおり、ガード(GuardsCheckEnd)の後にリゾルバー(ResolveStart)が動きました。ガードで「通さない」と判断するものは、リゾルバーより前に置く形になります。ガードは関連記事「Angularガードの使い方|CanActivateFnでログインリダイレクト」で扱っています。
Q. 遅延ロードのルートにも書けますか。
A. loadComponent のルートにも resolve を書けます。遅延ロードは関連記事「Angular遅延ロード|loadComponentとloadChildren」で扱っています。
まとめ
- リゾルバーは「表示の前にデータを読む関数」。
ResolveFn<T>で書き、ルートのresolveに置く withComponentInputBinding()で、結果をinput.required<T>()で受け取れる- エラーは
catchError→RedirectCommandで別の画面へ。何もしないとNavigationErrorで画面がそのまま - 待っている間は前の画面のまま。
router.currentNavigation()でローディングを出す - クエリの変化で動き直すかは
runGuardsAndResolvers。親子のリゾルバーは順番に動き、待ち時間が足される
参考・出典
確認日はいずれも 2026年10月12日です。
- Angular「Route data resolvers」:https://angular.dev/guide/routing/data-resolvers
- Angular「ResolveFn」:https://angular.dev/api/router/ResolveFn
- Angular「Data fetching with resources」:https://angular.dev/guide/routing/data-fetching-with-resources
- Angular「Customizing route behavior」:https://angular.dev/guide/routing/customizing-route-behavior
- Angular「Reactive data fetching with httpResource」:https://angular.dev/guide/http/http-resource
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angularリゾルバーの使い方|ResolveFnでデータを先に読み込む」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。