ハマった問題
Angularのコンポーネントで以下のように@Input()
を使うとき、変数x
を読み取り専用にしたければObservable化して渡せばよいという話を見たことがあったので試していたが、気を付けないといけない点がある(気づいたら当たり前…)。
@Component({
selector: 'app-my-component'
...
})
export class MyComponent implements OnInit {
@Input() x!: string;
...
}
↓(Observable化)
@Component({
selector: 'app-my-component'
...
})
export class MyComponent implements OnInit {
@Input() x$!: Observable<string>;
...
}
[補足]
'!'は初期化チェックを回避するためのDefinite Assignment Assertionというもので、@Input()
を付ける変数のようにコンストラクタで初期化できない変数に対して付ける必要がある。
x
をObservable化する場合、app-my-component
要素が親コンポーネントで*ngIf
などで消えているときにx$
が発火してもapp-my-component内ではsubscribeされていない(*ngIf
で消えているコンポーネントは単に隠されているのではなく消えている(参考:Why remove rather than hide?))ので、子コンポーネントがObservableの初期値発火の後に生成されたときなどに、初期値を取得できなくなってしまう。
解決策
読み取り専用にするにはObservable化はせず、単にreadonly修飾子を付ければよい。
@Component({
selector: 'app-my-component'
...
})
export class MyComponent implements OnInit {
@Input() readonly x!: string;
...
}
子コンポーネントにObservableを渡したいときについて
子コンポーネントがObservableを受け取りたいときも、ただの値で受け取って子コンポーネント内部でObservable化する方が、ただの値を渡したいときも柔軟性があって良さそう(Observableからただの値を取り出すには親template内でasync pipeを使えばよいが、逆は親template内で完結させることができない)。
Inputの値の変化を傍受するにはngOnChanges
を使うしかないかと思っていたが、少なくともAngularの現在のバージョン(v6.1.1)ではInputはsetterに対して以下のように使えるらしく、この中でSubjectへ値を送るという使い方ができるらしい。(参考:Angular 日本語ドキュメンテーション - コンポーネントの相互作用 「セッターによって入力プロパティの変更を傍受する」)
これくらい簡単にInputのObservable化ができるのであればあまり苦にならない。
// 最終版
import { Subject } from 'rxjs';
@Component({
selector: 'app-my-component'
...
})
export class MyComponent implements OnInit {
private xSource = new Subject<string>();
@Input() set x( value: string ) {
this.xSource.next(value);
}
x$: Observable<number> = this.xSource.asObservable();
...
}
そもそも、Observableを渡すというやりかたは親コンポーネントと子コンポーネントが疎結合にならないのであまりよろしくないという気がしてきた。Observableベースな実装にするどうかをコンポーネント単位で考えればよいというメリットは大きい。
親子コンポーネントの値のやり取りはただの値に統一してやるのがシンプルで良さそう。