リゾルバー(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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?