Angular SignalsとComputedで実現するきめ細かなリアクティビティ:2026年の技術面接対策

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

Angular SignalsとComputedによるきめ細かなリアクティビティの図解

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に変更が通知されます。

counter.component.tstypescript
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でshareReplaydistinctUntilChangedを手動で追加する必要がある場合とは異なります。

user-stats.component.tstypescript
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の変更に応じてコードを実行します。すべての同期更新が完了した後、変更検知サイクル中に非同期で実行されます。

theme-sync.component.tstypescript
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使用

Effectを使用してあるsignalから別のsignalにデータをコピーすることは、設計上の問題を示しています。派生状態にはcomputedを、派生値が書き込み可能である必要がある場合はlinkedSignalを使用することが推奨されます。

linkedSignal:書き込み可能な派生状態

Angular 19で導入され、Angular 20で安定版となったlinkedSignalは、ソースが変更されたときにリセットされる書き込み可能なsignalを作成します。computedが読み取り専用だが、派生値のローカル変更が必要なシナリオを解決します。

pagination.component.tstypescript
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グラフに統合されるリアクティブな代替手段に置き換えます。

user-card.component.tstypescript
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をブリッジするユーティリティを提供しています。

search.component.tstypescript
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読み取りをどのように処理しますか?

typescript
// 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を使用します。オブジェクトの場合、これは参照の等価性を意味します。カスタム等価性を提供できます:

typescript
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をパッチする必要性を排除します。

zoneless-counter.component.tstypescript
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にロードし、手動のサブスクリプションパターンを置き換えます。

user-profile.component.tstypescript
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を注入可能なサービスに移動します:

cart.service.tstypescript
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が共存できます。

typescript
// 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を維持し、境界で変換します:

typescript
// 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())はデコレータをリアクティブな代替手段に置き換えます
  • toSignaltoObservableは、段階的な移行のためにsignalsとRxJSをブリッジします
  • Zoneless AngularはきめきめSignalsに依存しています
  • Resource API(rxResource)は非同期状態を宣言的に管理します
  • 公式のAngular signalsガイドは、APIの詳細とベストプラクティスの権威ある参照として残ります
今日のチャレンジ

Angular のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年9月17日 更新

タグ

#angular
#signals
#computed
#reactivity
#interview

共有

関連記事