- Angular
Angular画面間のデータ受け渡し|パラメータ・state・サービス
画面から画面へ値を渡す方法は、大きく「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期連続の黒字決算。