Skip to content

Commit 5c0d688

Browse files
JeanMechethePunderWoman
authored andcommitted
fix(core): Ensure that a destroyed effect never run. (#59415)
Prior to this change, a scheduled root effect, even if destroyed instantly, would still run at least once. This commit fixes this. fixes #59410 PR Close #59415
1 parent dd1d69b commit 5c0d688

File tree

4 files changed

+64
-5
lines changed

4 files changed

+64
-5
lines changed

goldens/size-tracking/integration-payloads.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
},
2121
"forms": {
2222
"uncompressed": {
23-
"main": 176726,
23+
"main": 181773,
2424
"polyfills": 33772
2525
}
2626
},

packages/core/src/render3/reactivity/effect.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ export const ROOT_EFFECT_NODE: Omit<RootEffectNode, 'fn' | 'scheduler' | 'notifi
312312
consumerDestroy(this);
313313
this.onDestroyFn();
314314
this.maybeCleanup();
315+
this.scheduler.remove(this);
315316
},
316317
}))();
317318

packages/core/src/render3/reactivity/root_effect_scheduler.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export abstract class EffectScheduler {
3636
*/
3737
abstract flush(): void;
3838

39+
/** Remove a scheduled effect */
40+
abstract remove(e: SchedulableEffect): void;
41+
3942
/** @nocollapse */
4043
static ɵprov = /** @pureOrBreakMyCode */ /* @__PURE__ */ ɵɵdefineInjectable({
4144
token: EffectScheduler,
@@ -56,6 +59,17 @@ export class ZoneAwareEffectScheduler implements EffectScheduler {
5659
this.enqueue(handle);
5760
}
5861

62+
remove(handle: SchedulableEffect): void {
63+
const zone = handle.zone as Zone | null;
64+
const queue = this.queues.get(zone)!;
65+
if (!queue.has(handle)) {
66+
return;
67+
}
68+
69+
queue.delete(handle);
70+
this.queuedEffectCount--;
71+
}
72+
5973
private enqueue(handle: SchedulableEffect): void {
6074
const zone = handle.zone as Zone | null;
6175
if (!this.queues.has(zone)) {

packages/core/test/render3/reactivity_spec.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
computed,
1717
ContentChildren,
1818
createComponent,
19-
createEnvironmentInjector,
2019
destroyPlatform,
2120
Directive,
2221
effect,
@@ -37,16 +36,14 @@ import {
3736
ViewContainerRef,
3837
} from '@angular/core';
3938
import {SIGNAL} from '@angular/core/primitives/signals';
40-
import {takeUntilDestroyed, toObservable} from '@angular/core/rxjs-interop';
41-
import {createInjector} from '@angular/core/src/di/create_injector';
39+
import {toObservable} from '@angular/core/rxjs-interop';
4240
import {
4341
EffectNode,
4442
setUseMicrotaskEffectsByDefault,
4543
} from '@angular/core/src/render3/reactivity/effect';
4644
import {TestBed} from '@angular/core/testing';
4745
import {bootstrapApplication} from '@angular/platform-browser';
4846
import {withBody} from '@angular/private/testing';
49-
import {filter, firstValueFrom, map} from 'rxjs';
5047

5148
describe('reactivity', () => {
5249
let prev: boolean;
@@ -487,6 +484,53 @@ describe('reactivity', () => {
487484
(fix.componentInstance.injector as Injector & {destroy(): void}).destroy();
488485
expect(destroyed).toBeTrue();
489486
});
487+
488+
it('should not run root effects after it has been destroyed', async () => {
489+
let effectCounter = 0;
490+
const counter = signal(1);
491+
const effectRef = TestBed.runInInjectionContext(() =>
492+
effect(
493+
() => {
494+
counter();
495+
effectCounter++;
496+
},
497+
{injector: TestBed.inject(EnvironmentInjector)},
498+
),
499+
);
500+
expect(effectCounter).toBe(0);
501+
effectRef.destroy();
502+
TestBed.flushEffects();
503+
expect(effectCounter).toBe(0);
504+
505+
counter.set(2);
506+
TestBed.flushEffects();
507+
expect(effectCounter).toBe(0);
508+
});
509+
510+
it('should not run view effects after it has been destroyed', async () => {
511+
let effectCounter = 0;
512+
513+
@Component({template: ''})
514+
class TestCmp {
515+
counter = signal(1);
516+
effectRef = effect(() => {
517+
this.counter();
518+
effectCounter++;
519+
});
520+
}
521+
522+
const fixture = TestBed.createComponent(TestCmp);
523+
fixture.componentInstance.effectRef.destroy();
524+
fixture.detectChanges();
525+
expect(effectCounter).toBe(0);
526+
527+
TestBed.flushEffects();
528+
expect(effectCounter).toBe(0);
529+
530+
fixture.componentInstance.counter.set(2);
531+
TestBed.flushEffects();
532+
expect(effectCounter).toBe(0);
533+
});
490534
});
491535
});
492536

0 commit comments

Comments
 (0)