Angular SignalsとComputedで実現するきめ細かなリアクティビティ:2026年の技術面接対策
Angular 20以降で標準となったSignalsとComputed、linkedSignalの仕組みを解説。技術面接で問われるリアクティビティの設計パターンとRxJSとの使い分けを詳しく説明します。

Angular Signalsは、フレームワーク誕生以来、リアクティビティモデルにおける最も重大な変革を象徴しています。Angular 17で導入され、Angular 20以降ではデフォルトのアプローチとなったSignalsは、不要な変更検知サイクルを排除し、ゾーンレスアプリケーションとシームレスに統合するきめ細かなリアクティビティを提供します。
Signalsは同期的でグリッチフリーなリアクティブプリミティブです。Computed signalは依存関係が変化したときのみ再計算され、何度読み取られても変更サイクルごとに一度だけ実行されます。
Angular 20におけるSignalプリミティブの理解
AngularのSignalシステムは、3つの基本プリミティブで構成されています:signal()、computed()、effect()。それぞれがリアクティブグラフにおいて異なる役割を担います。
書き込み可能なsignalは、.set()または.update()で更新できる値を保持します。signalを読み取ると現在の値が返され、それに依存するcomputed signalやeffectに変更が通知されます。
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+1</button>
`
})
export class CounterComponent {
// Writable signal with initial value
count = signal(0);
// Computed signal derives from count
// Only recalculates when count changes
doubleCount = computed(() => this.count() * 2);
increment(): void {
// .update() receives current value as argument
this.count.update(c => c + 1);
}
}RxJSのBehaviorSubjectとの重要な違いは、signalsが同期的でグリッチフリーであることです。countが変更されると、doubleCountは同じ同期ブロック内で何度読み取られても、正確に一度だけ再計算されます。
Computed Signals:遅延評価とメモ化
Computed signalsは、依存関係が変更されるまで結果をキャッシュします。このメモ化は自動的に行われ、RxJSでshareReplayやdistinctUntilChangedを手動で追加する必要がある場合とは異なります。
import { Component, signal, computed } from '@angular/core';
interface User {
id: number;
name: string;
purchases: number[];
}
@Component({
selector: 'app-user-stats',
standalone: true,
template: `
<div>
<p>User: {{ user().name }}</p>
<p>Total spent: {{ totalSpent() | currency }}</p>
<p>Average purchase: {{ averagePurchase() | currency }}</p>
</div>
`
})
export class UserStatsComponent {
user = signal<User>({
id: 1,
name: 'Alice',
purchases: [99.99, 149.50, 29.99]
});
// Computed tracks only the properties it reads
totalSpent = computed(() => {
const purchases = this.user().purchases;
return purchases.reduce((sum, p) => sum + p, 0);
});
// Depends on another computed signal
averagePurchase = computed(() => {
const total = this.totalSpent();
const count = this.user().purchases.length;
return count > 0 ? total / count : 0;
});
}Computed signalは遅延評価されます。計算関数は宣言時ではなく、signalが最初に読み取られたときにのみ実行されます。その後の読み取りは、依存関係が無効化されるまでキャッシュされた値を返します。
Effect:リアクティブグラフにおける副作用
Effectは、signalの変更に応じてコードを実行します。すべての同期更新が完了した後、変更検知サイクル中に非同期で実行されます。
import { Component, signal, effect, inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
@Component({
selector: 'app-theme-sync',
standalone: true,
template: `
<button (click)="toggleTheme()">Toggle Theme</button>
`
})
export class ThemeSyncComponent {
private document = inject(DOCUMENT);
isDarkMode = signal(false);
constructor() {
// Effect syncs signal state to DOM attribute
effect(() => {
const theme = this.isDarkMode() ? 'dark' : 'light';
this.document.documentElement.setAttribute('data-theme', theme);
});
// Effect for localStorage persistence
effect(() => {
localStorage.setItem('theme', this.isDarkMode() ? 'dark' : 'light');
});
}
toggleTheme(): void {
this.isDarkMode.update(dark => !dark);
}
}Effectは依存関係を動的に追跡します。条件分岐が実行されない場合、その分岐内でのみ読み取られるsignalは、条件が変更されるまで追跡されません。
Effectを使用してあるsignalから別のsignalにデータをコピーすることは、設計上の問題を示しています。派生状態にはcomputedを、派生値が書き込み可能である必要がある場合はlinkedSignalを使用することが推奨されます。
linkedSignal:書き込み可能な派生状態
Angular 19で導入され、Angular 20で安定版となったlinkedSignalは、ソースが変更されたときにリセットされる書き込み可能なsignalを作成します。computedが読み取り専用だが、派生値のローカル変更が必要なシナリオを解決します。
import { Component, signal, linkedSignal, computed } from '@angular/core';
@Component({
selector: 'app-pagination',
standalone: true,
template: `
<select (change)="pageSize.set(+$any($event.target).value)">
<option [value]="10">10 per page</option>
<option [value]="25">25 per page</option>
<option [value]="50">50 per page</option>
</select>
<p>Page {{ currentPage() }} of {{ totalPages() }}</p>
<button (click)="prevPage()" [disabled]="currentPage() === 1">Prev</button>
<button (click)="nextPage()" [disabled]="currentPage() === totalPages()">Next</button>
`
})
export class PaginationComponent {
totalItems = signal(243);
pageSize = signal(10);
totalPages = computed(() =>
Math.ceil(this.totalItems() / this.pageSize())
);
// linkedSignal resets to 1 when pageSize or totalItems changes
// but allows manual navigation via prevPage/nextPage
currentPage = linkedSignal(() => 1);
prevPage(): void {
this.currentPage.update(p => Math.max(1, p - 1));
}
nextPage(): void {
this.currentPage.update(p => Math.min(this.totalPages(), p + 1));
}
}linkedSignalがなければ、このパターンは別のsignalに書き込むeffect(アンチパターン)か、複雑なRxJSオーケストレーションを必要としていました。
Angularの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
Signal-Based Component API:input、model、viewChild
Angular 20では、signal-basedコンポーネントAPIが安定版に昇格しました。これらはデコレータをsignalグラフに統合されるリアクティブな代替手段に置き換えます。
import {
Component,
input,
model,
output,
computed,
viewChild,
ElementRef
} from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div #card class="card" [class.selected]="selected()">
<h3>{{ fullName() }}</h3>
<button (click)="toggle()">{{ selected() ? 'Deselect' : 'Select' }}</button>
</div>
`
})
export class UserCardComponent {
// Signal input replaces @Input()
firstName = input.required<string>();
lastName = input<string>('');
// model() creates two-way bindable signal
// Parent uses [(selected)]="parentSignal"
selected = model(false);
// Output emitter for events
cardClicked = output<void>();
// viewChild returns Signal<ElementRef | undefined>
cardElement = viewChild<ElementRef>('card');
// Computed derives from signal inputs
fullName = computed(() =>
`${this.firstName()} ${this.lastName()}`.trim()
);
toggle(): void {
this.selected.update(s => !s);
this.cardClicked.emit();
}
}Signal inputsはより優れた型安全性を提供します。input.required()はInputSignal<T>を返し、input()はデフォルトが提供されない限りInputSignal<T | undefined>を返します。
Signals vs RxJS:高負荷時のパフォーマンス
Angular 21+のベンチマークでは、UI重視のシナリオでsignalsが変更検知のオーバーヘッドを削減することが示されています。頻繁な更新を伴う高負荷状態では、signalsはRxJS駆動のバインディングと比較してよりクリーンなメモリプロファイルと低いCPU使用率を示しました。
関心の分離は明確です:
| ユースケース | 推奨アプローチ |
|---|---|
| ローカルコンポーネント状態 | signal() |
| 派生UI値 | computed() |
| 外部APIとの同期 | effect() |
| HTTPリクエスト | RxJS Observable + toSignal() |
| WebSocketストリーム | RxJS with operators |
| デバウンスされたユーザー入力 | RxJS debounceTime、その後toSignal() |
| 複雑な非同期オーケストレーション | RxJS pipelines |
RxJSは非同期ストリーム、キャンセル、バックプレッシャーのためのツールとして残ります。Signalsは同期状態を処理します。
RxJS相互運用性:toSignalとtoObservable
Angularは@angular/core/rxjs-interopでsignalsとobservablesをブリッジするユーティリティを提供しています。
import { Component, signal, inject } from '@angular/core';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { debounceTime, switchMap, distinctUntilChanged } from 'rxjs';
interface SearchResult {
id: number;
title: string;
}
@Component({
selector: 'app-search',
standalone: true,
template: `
<input
[value]="query()"
(input)="query.set($any($event.target).value)"
placeholder="Search..."
/>
@if (results(); as items) {
<ul>
@for (item of items; track item.id) {
<li>{{ item.title }}</li>
}
</ul>
}
`
})
export class SearchComponent {
private http = inject(HttpClient);
query = signal('');
// Convert signal to observable for RxJS operators
private query$ = toObservable(this.query);
// RxJS pipeline with debounce, then back to signal
results = toSignal(
this.query$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(q => q.length > 2
? this.http.get<SearchResult[]>(`/api/search?q=${q}`)
: []
)
),
{ initialValue: [] as SearchResult[] }
);
}toSignalは作成時にサブスクライブし、コンポーネントが破棄されると自動的にアンサブスクライブします。initialValueオプションにより、signalからundefined型が除去されます。
技術面接でのAngular Signals関連の質問
2026年の技術面接では、signalの理解を頻繁にテストします。シニア候補者を識別するパターンを以下に示します。
Q: Computed signalがエラーをスローした場合、何が起こりますか?
エラーは、それを読み取るeffectやcomputedに伝播します。Angularはエラーをキャッシュしないため、次の読み取り時に計算が再試行されます。本番コードでは、computed関数内でエラーを処理する必要があります。
Q: Computed内でsignalに書き込むことはできますか?
できません。computed内で.set()や.update()を呼び出そうとするとエラーがスローされます。Computed関数は純粋でなければなりません。書き込み可能な派生値が必要な場合は、linkedSignalを使用します。
Q: Effectは条件分岐内のsignal読み取りをどのように処理しますか?
// Dynamic dependency tracking example
const showDetails = signal(false);
const userDetails = signal({ name: 'Alice', email: 'alice@example.com' });
effect(() => {
if (showDetails()) {
// userDetails is only tracked when showDetails is true
console.log('Details:', userDetails().email);
}
});Effectは最新の実行に基づいて依存関係を追跡します。showDetailsがfalseの場合、userDetailsの変更はeffectをトリガーしません。
Q: Signalの等価性はどのように機能しますか?
デフォルトでは、signalsは等価性にObject.isを使用します。オブジェクトの場合、これは参照の等価性を意味します。カスタム等価性を提供できます:
const user = signal(
{ id: 1, name: 'Alice' },
{ equal: (a, b) => a.id === b.id }
);Computed signalsがメモ化され遅延評価されることに言及する候補者は、基本的なAPI使用法を超えた理解を示しています。linkedSignalとcomputedの使い分けについて議論することは、Angular 19+のパターンに精通していることを示します。
変更検知:SignalsとZoneless Angular
Angular 22では、新規プロジェクトでゾーンレス変更検知がデフォルトになります。Signalsはこの中心にあります:状態がいつ変更されるかを正確にAngularに通知し、Zone.jsが非同期APIをパッチする必要性を排除します。
import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-zoneless-counter',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">+1</button>
`
})
export class ZonelessCounterComponent {
count = signal(0);
increment(): void {
// Signal update triggers change detection
// No Zone.js required
this.count.update(c => c + 1);
}
}Signalsにより、Angularはどのコンポーネントがどの状態に依存しているかを正確に追跡します。Signalの更新は、ツリー全体ではなく、影響を受けるコンポーネントのみの変更検知をスケジュールします。
Resource API:Signalとしての非同期データ
Angular 20のResource API(resource()とrxResource())は、非同期データを宣言的にsignalsにロードし、手動のサブスクリプションパターンを置き換えます。
import { Component, signal, computed } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-user-profile',
standalone: true,
template: `
@if (userResource.isLoading()) {
<p>Loading...</p>
}
@if (userResource.error()) {
<p>Error: {{ userResource.error() }}</p>
}
@if (userResource.value(); as user) {
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
}
`
})
export class UserProfileComponent {
private http = inject(HttpClient);
userId = signal(1);
// Declarative async resource
// Refetches when userId changes
userResource = rxResource({
request: () => this.userId(),
loader: ({ request: id }) =>
this.http.get<User>(`/api/users/${id}`)
});
loadUser(id: number): void {
this.userId.set(id);
}
}Resourceは、ローディング状態、エラー、解決された値を自動的に追跡します。リクエストsignalが変更されると再フェッチします。
Signalアーキテクチャのベストプラクティス
大規模アプリケーションでsignalsを構造化するには規律が必要です。これらのパターンは、コンポーネントからサービスまでスケールします。
関連するsignalsを同じ場所に配置する:一緒に変更されるsignalsをグループ化します。フォームコンポーネントは、サービス全体に散らばるのではなく、フィールドsignalsをまとめて保持します。
共有状態をサービスにリフトアップする:複数のコンポーネントが同じ状態を必要とする場合、signalを注入可能なサービスに移動します:
import { Injectable, signal, computed } from '@angular/core';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<CartItem[]>([]);
// Expose read-only signal to consumers
readonly cartItems = this.items.asReadonly();
readonly totalPrice = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
readonly itemCount = computed(() =>
this.items().reduce((sum, item) => sum + item.quantity, 0)
);
addItem(item: Omit<CartItem, 'quantity'>): void {
this.items.update(items => {
const existing = items.find(i => i.id === item.id);
if (existing) {
return items.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
}
return [...items, { ...item, quantity: 1 }];
});
}
}effectよりcomputedを優先する:値を派生できる場合は、computedを使用します。Effectは、リアクティブグラフを離れる副作用のためのものです:DOM操作、localStorage、アナリティクス、ネットワークリクエスト。
RxJSからSignalsへの移行
レガシーのAngularコードベースは段階的に移行できます。RxJS相互運用ユーティリティにより、signalsとobservablesが共存できます。
// Before: RxJS-based
@Component({ ... })
export class LegacyComponent {
private userSubject = new BehaviorSubject<User | null>(null);
user$ = this.userSubject.asObservable();
userName$ = this.user$.pipe(
map(u => u?.name ?? 'Guest')
);
}
// After: Signal-based
@Component({ ... })
export class ModernComponent {
user = signal<User | null>(null);
userName = computed(() => this.user()?.name ?? 'Guest');
}非同期操作については、パイプライン用にRxJSを維持し、境界で変換します:
// Hybrid approach
private search$ = toObservable(this.searchQuery).pipe(
debounceTime(300),
switchMap(q => this.api.search(q))
);
results = toSignal(this.search$, { initialValue: [] });今すぐ練習を始めましょう!
面接シミュレーターと技術テストで知識をテストしましょう。
Angular Signals面接対策のまとめ
- Signalsは同期プリミティブであり、RxJSは非同期ストリームを処理します
- Computed signalsは遅延評価されメモ化され、依存関係が変更されたときのみ再計算されます
- Effectは変更検知中に非同期で実行され、即座には実行されません
- linkedSignalは、effectなしで書き込み可能な派生状態の問題を解決します
- Signal inputs(
input()、model())はデコレータをリアクティブな代替手段に置き換えます toSignalとtoObservableは、段階的な移行のためにsignalsとRxJSをブリッジします- Zoneless AngularはきめきめSignalsに依存しています
- Resource API(
rxResource)は非同期状態を宣言的に管理します - 公式のAngular signalsガイドは、APIの詳細とベストプラクティスの権威ある参照として残ります
Angular のバグを見つけられますか
実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

執筆
Anthony Fillion-MailletSharpSkill 創業者
10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。
2026年9月17日 更新
タグ
共有
関連記事

Angular 19 面接質問集: Signals、SSR、必修概念
Angular 19 でよく出る面接質問: Signals、インクリメンタル ハイドレーション、zoneless change detection、新しいリアクティブ API。コード例と期待される回答付きで解説します。

2026年版 Angular依存性注入の完全ガイド: プロバイダー、トークン、面接対策
Angularの高度な依存性注入(DI)システムを徹底解説。InjectionToken、階層型インジェクター、プロバイダー戦略、そして技術面接で頻出する質問と回答例を網羅的に紹介します。

Angular 20 の Resource API と httpResource を徹底解説:2026年の面接対策に必須の新機能
Angular 20 で導入された Resource API(resource・httpResource・rxResource)の仕組みと実装方法を、実践的なコード例とともに解説します。Zod によるスキーマバリデーション、ResourceStatus の文字列リテラル、HttpClient からの移行パターン、技術面接で問われる質問と回答まで網羅した総合ガイドです。