Dev Study
← Angular コース

72. @Input / @Output / @ViewChild — デコレータ時代の部品API

input() / output() / viewChild() というシグナルベースの関数APIには、それぞれ1世代前の姿があります。プロパティにデコレータを付ける書き方です。読み替えのコツを先に言うと、「デコレータ版はただのプロパティ、関数版はシグナル」。@Input() name = ''; と書かれた親からの値は普通のプロパティに入るので、() を付けずにそのまま参照します。値の変更に反応したいときは、シグナルの computed が使えないため、ngOnChanges というライフサイクルフックや setter で受け止めていました。

イベント通知は @Output() と EventEmitter のセットです。@Output() saved = new EventEmitter<string>(); と宣言し、this.saved.emit(値) で親へ通知します。親側のテンプレートが (saved)="onSaved($event)" で受け取る形は output() と同じなので、テンプレート側の読み方は変わりません。EventEmitter の正体は RxJS の Subject の親戚(Observable)なので、.pipe() で加工しているような凝ったコードにも出会うことがあります。

要素の取得は @ViewChild('el') でした。関数版と違いシグナルではないので値の変化を追えず、しかも「ngAfterViewInit フックより前は undefined」という時差の罠があり、static: true という分かりにくいオプションまでありました。viewChild() がシグナルを返すのは、まさにこの時差問題の解決です。なお、新旧APIは同じコンポーネント内に同居できるため、移行途中のコードでは混在が普通です。デコレータを見たら「シグナルではない」と切り替えて読んでください。

サンプルコード(フレームワーク環境が必要なため表示のみ)

import {
  Component, Input, Output, EventEmitter, ViewChild, ElementRef,
} from '@angular/core';

@Component({ selector: 'app-user-card', template: '...' })
export class UserCardComponent {
  // 旧: ただのプロパティ。参照は this.name(()は付けない)
  @Input() name = '';
  @Input({ required: true }) userId!: number; // 必須指定の旧形

  // 旧: EventEmitter で親へ通知
  @Output() saved = new EventEmitter<string>();

  // 旧: ngAfterViewInit より前は undefined という罠があった
  @ViewChild('nameInput') nameInput?: ElementRef<HTMLInputElement>;

  save() {
    this.saved.emit(this.name); // output() の emit と同じ形
  }
}

// 対応: @Input()→input() / @Output()+EventEmitter→output()
//       @ViewChild()→viewChild() / @Input({required})→input.required()
サンプルコードで身につける(5本) →

学んだ概念を、素朴な例から実務寄りの例まで5つの具体例で確認します。

公式ドキュメントで詳しく ↗