ほぼ全てのObservableに.pipe(takeUntil(ngUnsubscribe))を書く必要があるのが煩わしいので
なんとか自動化できないかと調査しました。
結果、どうやらできそうです。
ただ、多くの人がぶち当たると思われるこの壁に、継承すればいんじゃね?というそれなりに単純な解決方法と思しき記事がほぼ見当たらなかったので、このやり方では問題がある、あるいはもっと良いベストプラクティスがあるような気がしています。
もしよろしければ教えてください。
筆者のレベル
typescript触って半年
angular触って1ヵ月
前提
RxjsでObservableをsubscribeする際には、takeUtilをpipeに渡すことで購読解除を定義します。
export class TestComponent implements OnInit, OnDestroy {
private ngUnsubscribe: Subject<void> = new Subject<void>();
private timerSubscription: Subscription | undefined;
ngOnInit(): void {}
onClicked(){
this.timerSubscription = timer(3000, 3000)
.pipe(takeUntil(ngUnsubscribe))
.subscribe(() => {
console.log("timer log");
})
}
ngOnDestroy(): void {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
}
angularはrouterによる画面遷移を行う場合、ページの再読み込みを行いません。
そのため、subscribe中のObservableがあった場合に、画面遷移後も前画面の購読処理が継続されます。
なので、コンポーネント内で完結させたいObservableは、takeUntilを使って、コンポーネント破棄時に購読解除を行う処理が必要です。(上記ソースコード例)
ですが、これを全てのコンポーネントに実装するのは非常に手間で、takeUntilの漏れも必ず発生します。
これを解決します。
ngUnsubscribeとngOnDestroyの外出し
ngUnsubscribeとngOnDestoryはほぼ全てのコンポーネントに実装されます。
そのため、この処理は親クラスに定義し、コンポーネントは親クラスを継承することで、共通処理を外に出してしまいます。
export class SuperComponent implements OnDestroy {
ngUnsubscribe: Subject<void> = new Subject<void>();
ngOnDestroy(): void {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
}
export class TestComponent extends SuperComponent implements OnInit {
private timerSubscription: Subscription | undefined;
ngOnInit(): void {}
onClicked(){
this.timerSubscription = timer(3000, 3000)
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe(() => {
console.log("timer log");
})
}
}
takeUntilの自動化
このままではまだtakeUntilを書く必要があります。これも外に出してしまいたいです。
javascriptのdeclare、prototypeを使用し、Observableに拡張メソッドを追加します。
これにより、SuperComponentを継承するだけでsubscribeOnComponentが使えるようになります。
export class SuperComponent implements OnDestroy {
ngUnsubscribe: Subject<void> = new Subject<void>();
ngOnDestroy(): void {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
}
declare module 'rxjs/internal/Observable' {
interface Observable<T> {
subscribeOnComponent: (
component: SuperComponent,
observerOrNext?: any | null,
error?: (error: any) => void,
complete?: (() => void) | null
) => Subscription;
}
}
Observable.prototype.subscribeOnComponent = function (
component: SuperComponent,
observerOrNext?: any | null,
error?: (error: any) => void,
complete?: (() => void) | null
): Subscription {
return this.pipe(takeUntil(component.ngUnsubscribe))
.subscribe(
observerOrNext,
error,
complete
);
};
export class TestComponent extends SuperComponent implements OnInit {
private timerSubscription: Subscription | undefined;
ngOnInit(): void {}
onClicked() {
this.timerSubscription = timer(3000, 3000).subscribeOnComponent(
this,
() => {
console.log('test');
}
);
}
}
上記のコードのように、Componentに必ず書いていた処理がなくなり、subscribeの際にtakeUntilを挟み忘れるという事態も防ぐことができます。
画面遷移をまたぐ購読処理は、通常のsubscribeを使えば問題ありません。
takeUntilDestroyed
takeUntilの他に、takeUntilDestroyedというメソッドが存在します。
これもほぼ似たような機能で、インスタンスがdestroyされた際に購読を解除します。(たぶん)
これを使う場合は以下のようになります。
export class SuperComponent {
destroyRef = inject(DestroyRef);
}
declare module 'rxjs/internal/Observable' {
interface Observable<T> {
subscribeUntilDestroy: (
target: SuperComponent,
observerOrNext?: any | null,
error?: (error: any) => void,
complete?: (() => void) | null
) => Subscription;
}
}
Observable.prototype.subscribeUntilDestroy = function (
component: SuperComponent,
observerOrNext?: any | null,
error?: (error: any) => void,
complete?: (() => void) | null
): Subscription {
return this.pipe(takeUntilDestroyed(component.destroyRef)).subscribe(
observerOrNext,
error,
complete
);
};
export class TestComponent extends SuperComponent implements OnInit {
private timerSubscription: Subscription | undefined;
ngOnInit(): void {}
onClicked() {
this.timerSubscription = timer(3000, 3000).subscribeUntilDestroy(
this,
() => {
console.log('test');
}
);
}
}
参考文献