feat(angular): DestroyRef support for subscribeWithPriority
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 3/5
- 見積もり時間
- 1〜2日
- 初心者へのやさしさ
- 35/100
- issue の種類
- 機能追加
- 明瞭さ
- 明確に書かれている
- 活発さ
- 停滞
- 技術スタック
- angular, typescript
調査の方向性
BackButtonEmitter.subscribeWithPriority から始め、要求されたシグネチャと teardown の動作を既存の実装と比較してください。core/src/utils/test/hardware-back-button.spec.ts をイベントディスパッチのリファレンスとして使用し、packages/angular/test/base/e2e の back-button.spec.ts の隣に実行可能なケースを追加してください。DestroyRef を省略した場合の動作は変えずに、破棄されたページのハンドラーが優先されなくなれば完了です。
索引モデルが issue の本文から書いたものです。
説明
Prerequisites
- I have read the Contributing Guidelines.
- I agree to follow the Code of Conduct.
- I have searched for existing issues that already include this feature request, without success.
Describe the Feature Request
Add an optional third argument to BackButtonEmitter.subscribeWithPriority, a DestroyRef, so the subscription ends when the scope that opened it is destroyed:
this.platform.backButton.subscribeWithPriority(10, (processNext) => { ... }, inject(DestroyRef));
Omitted, behaviour is unchanged.
Correction to the first version of this issue. It claimed that three visits to a page make one back press run the handler three times. That number came from a stand-in dispatcher in my own harness which ignored priority and invoked every registered handler; Ionic runs one handler per press, as the documentation says plainly. The real consequence is worse than a duplicate run and is described below. Two other corrections: the docs page has eight
subscribeWithPrioritycalls across six constructor examples, not seven; and #30115, cited previously, is not evidence for this — see the end of the use case.
Describe the Use Case
Platform is providedIn: 'root', so backButton is one application-lifetime Subject. A handler registered from a page stays registered after that page is destroyed, and nothing in the emitter removes it.
Combined with the documented selection rules, that goes further than an extra call. From the hardware back button page:
By default, only one handler is fired per hardware back button press.
In the event that there are handlers with the same priority value, the handler that was registered last will be called.
So take the ordinary case. Page A is on screen and registered a handler at priority 10. The user pushes B, which registers one at priority 10 too, and pops back to A. B is destroyed; B's subscription is not, and it was registered after A's. The next press is a tie, the tie goes to the handler registered last, and that is B's — a page the user has already left. A's handler never runs, and the press is consumed by a destroyed page.
Measured with Ionic's own dispatcher rather than a stand-in this time: released @ionic/core's startHardwareBackButton, the real Platform, a jsdom document and a synchronous NgZone stand-in, and a press is a real backbutton event on document, which is what core listens for and what it turns into the ionBackButton event carrying the real register.
A on screen, destroyed B still subscribed -> B (destroyed)
same, but B was given a DestroyRef -> A (on screen)
destroyed page at priority 20, live page at 10 -> destroyed page (20)
same, with a DestroyRef on the destroyed one -> live page (10)
control -- B unsubscribed by hand instead -> A (on screen)
Rows three and four are the same defect without needing a tie: a page that registered a higher priority keeps outranking whatever is on screen for the rest of the session.
The rows with a DestroyRef run the proposed function itself — the change is an assignment to backButton.subscribeWithPriority inside Platform's constructor, so the harness assigns exactly that body to the real Subject. The rows without it go through an installed 8.8.3 that carries the change locally, but with no third argument its path is source$ = this followed by the same subscribe, so it behaves as released code on that path. Saying which build produced which row matters here, and the first version of this issue did not.
Two things I should be straight about:
- This is already solvable. The control row is the workaround that exists today: keep the
Subscriptionand unsubscribe inngOnDestroy. It works. The request is about ergonomics and about what the documentation teaches, not about capability. - Not every revisit adds a subscription. A page still in the stack is reattached rather than reconstructed —
StackController.getExistingViewcallschangeDetectorRef.reattach()— so a second subscription needs the page to be constructed again, which happens after it has been popped, or after a root navigation.
What makes it worth changing rather than documenting alone: the signature takes a callback and returns something most callers ignore, and the hardware back button page has eight subscribeWithPriority calls inside six constructor examples and does not mention cleanup once. The documented pattern is the one that accumulates, and the symptom — a back press handled by a page that is gone — is silent and gets worse with use.
On #30115, which I cited in the first version of this issue: it does not support this. That reporter keeps the Subscription and unsubscribes in ngOnDestroy, which is the control row above, and still sees a duplicate. It is evidence that this API confuses people, not evidence of the accumulation this parameter would prevent, and I should not have filed it under the use case.
Describe Preferred Solution
An optional DestroyRef parameter, with the teardown registered directly rather than through an operator:
subscribeWithPriority(
priority: number,
callback: (processNextHandler: () => void) => Promise<any> | void,
destroyRef?: DestroyRef
): Subscription;
const subscription = this.subscribe((ev) => {
return ev.register(priority, (processNextHandler) => zone.run(() => callback(processNextHandler)));
});
destroyRef?.onDestroy(() => subscription.unsubscribe());
return subscription;
DestroyRef has been @publicApi since Angular 16, so it is within this package's >=16 floor. takeUntilDestroyed would not be: it is @developerPreview in 16 and still in 18, and only @publicApi from 19.
One consequence to note either way: subscribeWithPriority can then throw, where today it never does. For a component's DestroyRef, registering on an already-destroyed view throws VIEW_ALREADY_DESTROYED.
One scope limit worth settling here rather than in a pull request. A DestroyRef ties to ngOnDestroy, and with ion-router-outlet that runs when a page is popped, not when it is navigated away from — StackController.cleanup() destroys only the views that have left the stack and detaches the others. So this ends handlers that outlive their page. It does nothing for a page still in the stack whose handler can win a press while another page is on screen; that needs the enter and leave lifecycle hooks, and no lifetime-based API can address it. Those are two different problems, and if the team would rather have one visibility-aware mechanism than a lifetime one, that is a reasonable answer and better decided now.
Describe Alternatives
- Keep the
Subscriptionand unsubscribe manually. Works today. Easy to omit, and the omission is invisible until a back press goes to the wrong page. - Document the cleanup instead of changing the API. Worth doing regardless of the outcome here, since the current examples teach the shape that accumulates, but it leaves every caller writing the same boilerplate.
- A separate method such as
subscribeWithPriorityUntil, leaving the existing signature untouched. - Have
Platformtrack subscriptions itself. Rejected: it cannot know which scope a caller belongs to.
Related Code
Minimal page:
export class LeakyPage implements OnInit {
private readonly platform = inject(Platform);
ngOnInit() {
this.platform.backButton.subscribeWithPriority(10, (processNext) => {
console.log('handler ran');
processNext();
});
}
}
Push to it from a page that registers its own handler at the same priority, pop back, then press the hardware back button: the log comes from the page that is gone.
The pull request will add an executable version of this to packages/angular/test/base/e2e, beside the existing back-button.spec.ts, dispatching a backbutton event on document the way core/src/utils/test/hardware-back-button.spec.ts does — hand-dispatching ionBackButton would supply a fake detail.register and prove nothing, which is the mistake the corrected measurement above avoids.
Additional Information
Pull request: #31348.
The change has been running in production in an Ionic 8 app of ours, applied to the built fesm2022 output, which is what prompted submitting it upstream.
- 主要言語
- TypeScript
- スター
- 52.7k
- フォーク
- 13.3k
- 平均マージ
- 2日 22時間
- マージ済み PR(30日)
- 61
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
ionic-team/ionic-framework のほかの issue
-
triage
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
ionic-team/ionic-framework#31514 ·
メンテナーはふだん 1 日以内に返信
-
bug: ion-searchbar input is always labelled "search text"; aria-label on the host is not forwardedオープンtriage
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
ionic-team/ionic-framework#31492 ·
メンテナーはふだん 1 日以内に返信
-
triage
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
ionic-team/ionic-framework#31410 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
triage
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
ionic-team/ionic-framework#31314 ·
メンテナーはふだん 1 日以内に返信
-
triage
難易度 2/5 1〜3時間 初心者へのやさしさ 86/100
ionic-team/ionic-framework#31291 ·
メンテナーはふだん 1 日以内に返信
ionic-team/ionic-framework の issue をすべて見る
似ている issue
-
bug HemiStake
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
hemilabs/ui-monorepo#2413 ·
メンテナーはふだん 1 日以内に返信
-
component/ui framework/react kind/bug language/javascript
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
meshery/meshery#22216 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
-
type/bug
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
paperclipai/paperclip#14982 ·
メンテナーはふだん 1 日以内に返信
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
難易度 1/5 1時間未満 初心者へのやさしさ 95/100
lingdojo/kana-dojo#31515 · コメント 1 件 · リアクション 5 件 ·
メンテナーはふだん 1 日以内に返信