画面から画面へ値を渡す方法は、大きく「URL に載せる(パスパラメータ・クエリ)」「URL に載せない(router の state・サービス)」の2つに分かれます。 URL に載せた値は、リロードしても、ブックマークや共有した URL から開いても残ります。URL に載せない値は、別のタブで URL を開くと届きません。まず「その URL だけで画面を再現できる必要があるか」で選びます。

v22.2.2 の画面を Chromium で操作し、値が残るかを確かめた結果です。

渡し方 リロード 別タブで URL を開く 型 向いている値
パスパラメータ /products/2 残る 残る 文字列(変換が要る) 画面の主役の ID
クエリ ?tab=spec 残る 残る 文字列(変換が要る) 絞り込み・並び順・タブ・ページ
router の state 残る(同じタブ) 消える オブジェクトのまま 確認画面へ渡す入力内容など一時的な値
サービス(Signal) 消える 消える オブジェクトのまま カート・ログイン中のユーザーなど複数画面で使う値
リゾルバー 再取得 再取得 戻り値の型 ID からサーバーで読み直すデータ

router の state は、リロードでは消えませんでした。 ブラウザーが履歴の項目ごとに state を保存していて、同じタブのリロードや戻る・進むでは、それが戻ってくるためです。消えるのは、URL を別のタブで開いたとき(ブックマーク・共有・コピーした URL)です。

確認環境:Angular 22.2.2(@angular/core・@angular/router・@angular/cli)、TypeScript 6.0.3、Node.js 24.21.0(Linux)。この記事の完成コードを ng build し、Chromium(Playwright 1.56.1 の chromium-1194)で、画面の移動・リロード・戻る/進む・別タブでの URL の直接表示を行い、画面に出た値を記録しました。withComponentInputBinding や state の仕様は、angular.dev と @angular/router 22.2.2 の型定義で確かめました(確認日 2026年10月13日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angular画面間のデータ受け渡し|パラメータ・state・サービス」

早見表:どれを使うか

渡したいもの 使う方法 理由
一覧で選んだ商品の ID パスパラメータ URL を共有しても同じ画面が出る
検索条件・ページ番号・タブ クエリ 省略できる値。ブックマークで条件ごと残る
入力フォームの内容を確認画面へ router の state URL に出したくない。少しの間だけ使う
カートの中身・選択中の項目 サービス(Signal) 複数の画面で読み書きする
ID に対応する商品データ リゾルバー(ID は URL) 表示の前に読み終える。リロードでも読み直せる
個人情報・トークン どれでもなく、サーバー側で持つ URL は履歴やログに残る

完成コード

一覧・詳細・確認・カートの4画面です。この1つのアプリで、5つの方法をすべて使っています。

ルート設定(withComponentInputBinding を付ける)

// app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter, Routes, withComponentInputBinding } from '@angular/router';
import { ListPage, DetailPage, ConfirmPage, CartPage } from './pages';
import { productResolver } from './data';

export const routes: Routes = [
  { path: '', component: ListPage },
  {
    path: 'products/:id', component: DetailPage,
    data: { title: '商品詳細' },                 // 固定の値
    resolve: { product: productResolver },      // リゾルバー
  },
  { path: 'confirm', component: ConfirmPage },
  { path: 'cart', component: CartPage },
];

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes, withComponentInputBinding()),
  ],
};

データ・サービス・リゾルバー

// data.ts
import { Injectable, signal, computed } from '@angular/core';
import { ResolveFn } from '@angular/router';

export interface Product { id: number; name: string; price: number; }

export const PRODUCTS: Product[] = [
  { id: 1, name: 'ノートPC', price: 120000 },
  { id: 2, name: 'モニター', price: 30000 },
  { id: 3, name: 'キーボード', price: 8000 },
];

/** 画面をまたいで持つ値(Signal で持つサービス) */
@Injectable({ providedIn: 'root' })
export class CartStore {
  private readonly _items = signal<Product[]>([]);
  readonly items = this._items.asReadonly();
  readonly total = computed(() => this._items().reduce((s, p) => s + p.price, 0));
  add(p: Product) { this._items.update(list => [...list, p]); }
  clear() { this._items.set([]); }
}

/** リゾルバー:画面を出す前に商品を読む(本番では HttpClient で読む) */
export const productResolver: ResolveFn<Product | undefined> = (route) => {
  const id = Number(route.paramMap.get('id'));
  return new Promise(r => setTimeout(() => r(PRODUCTS.find(p => p.id === id)), 50));
};

画面

// pages.ts
import { Component, computed, inject, input, numberAttribute } from '@angular/core';
import { ActivatedRoute, Router, RouterLink, RouterOutlet } from '@angular/router';
import { CartStore, PRODUCTS, Product } from './data';

@Component({
  selector: 'app-list',
  imports: [RouterLink],
  template: `
    <h2>商品一覧</h2>
    <!-- パスパラメータ+クエリ -->
    <a [routerLink]="['/products', 2]" [queryParams]="{ tab: 'spec', from: 'list' }">モニター</a>
    <!-- router の state(routerLink で) -->
    <a routerLink="/confirm" [state]="{ product: products[0], at: now }">ノートPCを確認</a>
    <!-- router の state(navigate で) -->
    <button (click)="goConfirm()">モニターを確認</button>
    <!-- サービス -->
    <button (click)="cart.add(products[2])">キーボードをカートへ</button>
    <a routerLink="/cart">カートを見る</a>
  `,
})
export class ListPage {
  products = PRODUCTS;
  now = '2026-10-13';
  cart = inject(CartStore);
  private router = inject(Router);

  goConfirm() {
    this.router.navigate(['/confirm'], { state: { product: PRODUCTS[1], at: 'navigate' } });
  }
}

@Component({
  selector: 'app-detail',
  imports: [RouterLink],
  template: `
    <h2>詳細</h2>
    <p>id={{ id() }}({{ typeOfId() }}) tab={{ tab() }} from={{ from() }} title={{ title() }} product={{ product()?.name }}</p>
    <p>snapshot id={{ snapId }}</p>
    <a [routerLink]="['/products', id() + 1]" [queryParams]="{ tab: 'review' }">次の商品</a>
  `,
})
export class DetailPage {
  id = input.required({ transform: numberAttribute });  // パスパラメータ :id
  tab = input<string>();                                  // クエリ ?tab=
  from = input<string>();                                 // クエリ ?from=
  title = input<string>();                                // ルートの data
  product = input<Product>();                             // リゾルバーの結果
  typeOfId = computed(() => typeof this.id());

  // 比較用:snapshot は最初の1回だけの値
  snapId = inject(ActivatedRoute).snapshot.paramMap.get('id');
}

@Component({
  selector: 'app-confirm',
  template: `<h2>確認</h2><p>{{ text }}</p>`,
})
export class ConfirmPage {
  private router = inject(Router);
  // 遷移中(コンストラクターの時点)なら currentNavigation() で読める
  private nav = this.router.currentNavigation()?.extras.state as { product?: Product; at?: string } | undefined;
  // 遷移が終わったあとは history.state で読む
  private hist = history.state as { product?: Product; at?: string; navigationId?: number };

  text = `nav=${this.nav?.product?.name ?? 'なし'} history=${this.hist?.product?.name ?? 'なし'}`
       + ` at=${this.hist?.at ?? '-'} navigationId=${this.hist?.navigationId}`;
}

@Component({
  selector: 'app-cart',
  template: `<h2>カート</h2><p>{{ cart.items().length }}件 合計{{ cart.total() }}円</p>`,
})
export class CartPage { cart = inject(CartStore); }

@Component({
  selector: 'app-root',
  imports: [RouterOutlet],
  template: `<router-outlet />`,
})
export class App {}

動かした結果

Playwright で操作し、画面に出た文字を記録しました。

== 1. パス・クエリ・data・リゾルバー
一覧 → 詳細(routerLink)
    URL: /products/2?tab=spec&from=list
    表示: id=2(number) tab=spec from=list title=商品詳細 product=モニター
  ↻ リロード
    URL: /products/2?tab=spec&from=list
    表示: id=2(number) tab=spec from=list title=商品詳細 product=モニター
URL を直接開く(ブックマーク)
    URL: /products/3?tab=review
    表示: id=3(number) tab=review from= title=商品詳細 product=キーボード
クエリなし
    URL: /products/2
    表示: id=2(number) tab= from= title=商品詳細 product=モニター
id が数字でない
    URL: /products/abc
    表示: id=NaN(number) tab= from= title=商品詳細 product=
== 2. router の state
routerLink [state]
    URL: /confirm
    表示: nav=ノートPC history=ノートPC at=2026-10-13 navigationId=2
  ↻ リロード
    URL: /confirm
    表示: nav=ノートPC history=ノートPC at=2026-10-13 navigationId=1
  ← 戻る → 進む
    URL: /confirm
    表示: nav=ノートPC history=ノートPC at=2026-10-13 navigationId=3
router.navigate state
    URL: /confirm
    表示: nav=モニター history=モニター at=navigate navigationId=2
別タブで URL を開く
    表示: nav=なし history=なし at=- navigationId=1
    history.state = {"navigationId":1}
== 3. サービス(Signal)
2回追加 → カート
    URL: /cart
    表示: 2件 合計16000円
  ↻ リロード
    URL: /cart
    表示: 0件 合計0円

この結果から分かることを、方法ごとに説明します。

1. パスパラメータ:画面の主役の ID

ルートに :id のように : 付きで書いた部分が、パスパラメータ(URL の道の一部に入れる値)です。送る側は routerLink に配列で渡します。

<a [routerLink]="['/products', product.id]">詳細</a>
this.router.navigate(['/products', product.id]);

受け取る側は、withComponentInputBinding() を付けておくと、同じ名前の input() に自動で入ります。 URL から来る値は文字列なので、数値にしたいときは transform: numberAttribute を付けます。結果の id=2(number) のとおり、number 型で受け取れました。

数字でない値(/products/abc)では NaN になりました。 numberAttribute は変換できないと NaN を返します(第2引数で代わりの値を指定できます)。URL は利用者が書き換えられるので、NaN や存在しない ID のときの表示を用意しておきます。

input で受け取ると、同じ画面のまま ID が変わっても追従する

詳細画面の「次の商品」で /products/1 → 2 → 3 と移ったときの結果です。

開く /products/1?tab=spec   | id=1 tab=spec   product=ノートPC   | snapshot id=1
次へ /products/2?tab=review | id=2 tab=review product=モニター   | snapshot id=1
次へ /products/3?tab=review | id=3 tab=review product=キーボード | snapshot id=1

同じルートの中で移ると、Angular はコンポーネントを作り直さずに使い回します。そのため、ActivatedRoute.snapshot で最初に読んだ値は 1 のままでした。input() で受け取った値は、移るたびに変わりました。ActivatedRoute を使う場合は、snapshot ではなく paramMap(Observable)を購読します。

受け取り方 書き方 同じ画面のまま ID が変わったとき
input(おすすめ) id = input.required({ transform: numberAttribute }) 追従する
ActivatedRoute の Observable toSignal(route.paramMap.pipe(map(p => p.get('id')))) 追従する
ActivatedRoute の snapshot route.snapshot.paramMap.get('id') 最初の値のまま

2. クエリパラメータ:省略できる条件

?tab=spec&from=list の部分です。絞り込み・並び順・ページ番号など、無くても画面が出せる値に使います。

<a [routerLink]="['/products', 2]" [queryParams]="{ tab: 'spec', from: 'list' }">モニター</a>
// 今のクエリに足す(他のクエリを消さない)
this.router.navigate([], { queryParams: { page: 2 }, queryParamsHandling: 'merge' });

受け取りはパスパラメータと同じく input() です。クエリが無いとき、input は undefined になりました(結果の tab=)。@angular/router 22.2.2 の型定義の説明でも、ルートに同じ名前の値が無い input には undefined が入る、と書かれています。前の画面の値が残らないようにするためです。既定値がほしいときは、input の transform で補うか、リゾルバーで値を入れます。

型定義には withComponentInputBinding の設定として、次の2つがあります。

設定 意味
{ queryParams: false } クエリを input に入れない(パスと data・リゾルバーだけ)
{ unmatchedInputBehavior: 'undefinedIfStale' } 一度もルートから値が来ていない input には undefined を入れない
既定('alwaysUndefined') ルートに値が無い input には毎回 undefined を入れる

同じ名前の値が複数あるときは、クエリ → パス(とマトリックス)パラメータ → data → リゾルバーの順に上書きされ、リゾルバーが一番強い、と型定義に書かれています。

3. router の state:URL に出さない一時的な値

routerLink の [state] か、navigate の state で、オブジェクトをそのまま渡せます。

<a routerLink="/confirm" [state]="{ product: products[0], at: now }">確認</a>
this.router.navigate(['/confirm'], { state: { product: PRODUCTS[1], at: 'navigate' } });

受け取りは2通りです。

読む場所 書き方 読めるタイミング
遷移中 router.currentNavigation()?.extras.state コンストラクターなど、遷移が終わる前
遷移のあと history.state いつでも(同じ履歴の項目にいる間)

currentNavigation() は v20.2 で入った Signal です。以前の getCurrentNavigation() は、22.2.2 の型定義で @deprecated 20.2 Use the currentNavigation signal instead. とされています。

リロードでは消えず、別タブでは消える

結果のとおり、同じタブでのリロードと、戻る→進むでは、state が残りました。 router は state を history.pushState() でブラウザーの履歴に保存し(型定義の説明による)、ブラウザーは履歴の項目ごとに state を持っているためです。リロードのたびに navigationId(router が足す番号)は変わりますが、渡した値はそのまま戻りました。

消えたのは、同じ URL を別のタブで開いたときです(history.state = {"navigationId":1})。ブックマーク・共有した URL・コピーした URL から開いた場合も同じです。state だけに頼る画面は、state が無いときの表示(一覧へ戻すなど)を用意しておきます。

state に入れられるもの

history.pushState() に渡すため、入れられるのはブラウザーが複製できる値です。試した結果です。

入れた値 結果
ふつうのオブジェクト・配列・文字列・数値 そのまま読めた
Date Date のまま読めた([object Date])
関数を含むオブジェクト DataCloneError:Failed to execute 'pushState' on 'History': () => 1 could not be cloned. 画面は移らなかった
クラスのインスタンス(new Point(3)) 値(x=3)は読めたが、instanceof Point は false、メソッドは undefined
10MB・100MB の文字列 Chromium では渡せた(長さ 10485760・104857600 で読めた)

関数は入れられません。クラスのインスタンスは、ただのオブジェクトになって届きます。容量については、MDN が「一部のブラウザーは state をディスクに保存し、大きさに上限を設けていて、超えると例外を投げる」と説明し、大きいデータは sessionStorage・localStorage を使うよう勧めています。Chromium で通った大きさが、他のブラウザーでも通るとは限りません。state には ID や入力内容など小さな値を入れ、大きなデータはサービスかサーバーに置きます。

4. サービス(Signal):複数の画面で使う値

providedIn: 'root' のサービスは、アプリの中で1つだけ作られ、どの画面からも同じものが使われます。 中身を Signal で持つと、値が変わったときに画面も変わります。

結果のとおり、一覧で2回追加してカートへ移ると 2件 合計16000円 でした。リロードすると 0件 に戻りました。 サービスはメモリーの中にあるだけなので、ページを読み直すと作り直されます。

リロード後も残したいときは、Signal の値を sessionStorage(タブを閉じるまで残る保存場所)に書き写します。

@Injectable({ providedIn: 'root' })
export class CartStore {
  private readonly _items = signal<Product[]>(
    JSON.parse(sessionStorage.getItem('cart') ?? '[]'),
  );
  readonly items = this._items.asReadonly();

  constructor() {
    effect(() => sessionStorage.setItem('cart', JSON.stringify(this._items())));
  }
  add(p: Product) { this._items.update(list => [...list, p]); }
}

サービスと inject() のしくみは、関連記事「Angularサービスと依存性注入|injectとconstructorの違い」で扱っています。Signal の API は、関連記事「Angular Signals使い方|v22対応の全API早見表と実行結果つきコード」で扱っています。

「サービスで渡したのに undefined になる」とき

以前からよくある質問に、Subject を持つサービスで値を流したのに、次の画面で受け取れない、というものがあります。Subject は、購読した後に流れた値だけを届けます。 前の画面で next() した時点では、次の画面はまだ作られていないので、値を受け取れません。画面をまたぐ値は、上のように Signal(または BehaviorSubject)で「今の値」を持つ形にします。Signal なら、あとから作られた画面でも items() で今の値を読めます。

5. リゾルバー:ID から読み直すデータ

リゾルバーは、画面を出す前にデータを読み終えておくしくみです。ID だけを URL に載せ、データはリゾルバーで読むと、リロードや別タブでも同じ画面が出ます。 結果でも、URL を直接開いたときに product=キーボード が入っていました。

withComponentInputBinding() を付けると、resolve: { product: … } の結果が product という名前の input に入ります。リゾルバーの詳しい書き方とエラー時の扱いは、関連記事「Angularリゾルバーの使い方|ResolveFnでデータを先に読み込む」で扱っています。

22.2 では、ルートに resources(Signal の resource を返す関数)を書く機能も、developer preview(試験提供)として型定義に入っています。

ルート設定の data:画面ごとの固定値

data: { title: '商品詳細' } のように、ルートに固定の値を書いておけます。結果のとおり、title という input に入りました。画面のタイトルやパンくずの文字、権限の種類など、ルートごとに決まっている値に使います。

方法ごとの比較(詳しい版)

項目 パス・クエリ router の state サービス
URL に出る 出る 出ない 出ない
型 文字列だけ 複製できる値 何でも(関数も可)
容量の目安 URL の長さ。RFC 9110 は 8000 オクテット以上の対応を推奨 ブラウザーごとの上限 メモリーの範囲
戻る・進む URL ごとに戻る 履歴の項目ごとに戻る 戻らない(今の値のまま)
使う画面 送り先の1画面 送り先の1画面 どの画面からでも

URL は、ブラウザーの履歴・サーバーのアクセスログ・共有先に残ります。メールアドレスなどの個人情報は、クエリに載せない方が安全です。

よくある質問

Q. withComponentInputBinding() を付けないとどうなりますか。 A. ルートの値は input に入りません。input.required() の部品で試すと、画面は空のまま、コンソールに NG0950: Input "id" is required but no value is available yet. が出ました。付けない場合は ActivatedRoute から読みます。付けるときは provideRouter(routes, withComponentInputBinding()) のように、provideRouter の2番目以降に書きます。

Q. 子ルートのコンポーネントで、親のパスパラメータを input で受け取れますか。 A. 22.2.2 では受け取れました。{ path: 'shop/:id', component: Parent, children: [{ path: 'info', component: Child }] } で /shop/5/info を開くと、子の id input に 5 が入りました。22.2.2 の型定義では paramsInheritanceStrategy の既定が 'always'(親のパラメータをすべて引き継ぐ)と説明されています。以前の動き(子のパスが空のときなどだけ引き継ぐ)は withRouterConfig({ paramsInheritanceStrategy: 'emptyOnly' }) で選べます。

Q. 親子のコンポーネント(同じ画面の中)で値を渡したいです。 A. それは router ではなく input()・output() で渡します。関連記事「Angular input・output・modelの使い方|親子の受け渡し」で扱っています。

Q. state を受け取る画面でリロードしても値が残るなら、state だけで足りますか。 A. 同じタブの中だけなら残ります。ただし、URL を別のタブで開くと state はありません。確認画面のように「前の画面から来たときだけ」意味のある画面なら、state が無いときは前の画面へ戻す形が扱いやすくなります。

Q. ルートの書き方や 404 の設定は。 A. 関連記事「Angularルーティング設定|provideRouter・パラメータ・404」で扱っています。

まとめ

  • URL だけで画面を再現したい値は、パスパラメータかクエリ。リロード・別タブ・ブックマークで残る
  • withComponentInputBinding() を付けると、パス・クエリ・data・リゾルバーの値が同じ名前の input() に入る。数値は numberAttribute で変換する
  • snapshot は同じ画面のまま ID が変わっても最初の値のまま。input か paramMap の購読を使う
  • router の state は、同じタブのリロードと戻る・進むでは残り、別タブで開くと消える。関数は入らず DataCloneError
  • サービス(Signal)はリロードで消える。残したいなら sessionStorage に書き写す
  • ID は URL、データはリゾルバーで読み直す形にすると、どこから開いても同じ画面になる

参考・出典

確認日はいずれも 2026年10月13日です。

  • Angular「Read route state」:https://angular.dev/guide/routing/read-route-state
  • Angular「Navigate to routes」:https://angular.dev/guide/routing/navigate-to-routes
  • Angular API「withComponentInputBinding」:https://angular.dev/api/router/withComponentInputBinding
  • Angular API「NavigationBehaviorOptions」:https://angular.dev/api/router/NavigationBehaviorOptions
  • Angular API「Router」:https://angular.dev/api/router/Router
  • Angular API「ResolveFn」:https://angular.dev/api/router/ResolveFn
  • MDN「History: pushState() method」:https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
  • RFC 9110「HTTP Semantics」4.1 URI References:https://www.rfc-editor.org/rfc/rfc9110#section-4.1

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angular画面間のデータ受け渡し|パラメータ・state・サービス」

株式会社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で一緒に開発しながら、 成長を楽しみませんか?