fix(material/tabs): endless toggling when selecting tabs in quick succession - #33755
Open
meetbhalodi11 wants to merge 1 commit into
Open
fix(material/tabs): endless toggling when selecting tabs in quick succession#33755meetbhalodi11 wants to merge 1 commit into
meetbhalodi11 wants to merge 1 commit into
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
…cession `selectedTabChange` is an async `EventEmitter`, so its subscribers are invoked in a `setTimeout`. When the selection changes several times before those callbacks run, a callback can be delivered with an index that is no longer current. Consumers that write `event.index` back into `[selectedIndex]` then push the group back to the stale index, which emits again and results in an endless toggle. Emits the event from the same microtask as `selectedIndexChange` so that emit time and delivery time coincide, which is why `selectedIndexChange` is already unaffected. Fixes angular#24096
meetbhalodi11
force-pushed
the
fix-tabs-24096
branch
from
September 1, 2026 19:04
b79ed1f to
d451a6b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #24096
The problem
selectedTabChangeis declared as an async emitter:Angular wraps subscribers of async emitters in
setTimeout, so handlers run a macrotask after the event is emitted. InngAfterContentCheckedthe event is emitted before_selectedIndexis committed, and the handler runs later still.When the selection changes more than once before those callbacks run, a callback is delivered carrying an index that is no longer current. Apps that write
event.indexback into[selectedIndex]— a common pattern — then push the group back to the stale index, which emits again, and the two values ping-pong indefinitely.This needs two conditions, which is why some people cannot reproduce it:
[selectedIndex]is bound, andselectedTabChangehandler writes the index back into it.It also requires zone-based change detection, since the loop is sustained by a change detection pass running between the queued callbacks. It does not reproduce under
provideZonelessChangeDetection().Reproduction
https://stackblitz.com/edit/stackblitz-starters-2fiwdygt
Press "Reproduce — 10 real tab clicks" once and then stop interacting with the page. Ten clicks produce 2000+ emissions and the tabs keep toggling on their own; the demo stops only because of a built-in safety cap. Unticking the checkbox runs the identical trigger against
selectedIndexChange, which settles at exactly 10.The demo also logs each delivery as
delivered=<event index> committed=<group index>. Every delivery is markedSTALE, i.e. the event never matches the selection that was actually current when the handler ran.The fix
Emit
selectedTabChangefrom the same microtask asselectedIndexChange, and make it a synchronous emitter so that emit time and delivery time coincide. This is precisely whyselectedIndexChangeis already unaffected, and why switching to it is the workaround people have been using for the last four years.Note on the previously suggested fix
The most upvoted comment on the issue suggests applying 67e02b0 to
selectedTabChangeas well. That commit deferred the emit into a microtask, which worked forselectedIndexChangebecause it is a synchronous emitter — deferring the emit also defers the delivery.selectedTabChangeis async, so emit time and delivery time are decoupled. Deferring only the emit still leaves the payload to be handed over a macrotask later, and a change detection pass in between can commit a different index. Dropping the async flag is therefore also required.Behaviour change
selectedTabChangehandlers now run in a microtask rather than a macrotask, and afterselectedIndexChangein the same tick instead of a later one. Code that relied on the extra delay could be affected. Please let me know if you would like this called out as aBREAKING CHANGE:in the commit footer.Testing
mainand passes with this change. Without the fix it reportsExpected $.length = 1 to equal 2, because onlyselectedIndexChangehas been delivered once the microtask queue is flushed.pnpm test tabs --no-watch— 148/148 pass, including the 147 pre-existing tests.