refactor(core): run effect cleanup as untracked (#60421)

A effect cleanup should not be considered reactive.

fixes #57757

PR Close #60421
This commit is contained in:
Matthieu Riegler
2024-10-05 17:34:43 +02:00
committed by Alex Rickabaugh
parent 34393874e6
commit 217f6d96ba
2 changed files with 47 additions and 1 deletions
@@ -15,12 +15,12 @@ import {
consumerDestroy,
consumerPollProducersForChange,
isInNotificationPhase,
setActiveConsumer,
} from '../../../primitives/signals';
import {FLAGS, LViewFlags, LView, EFFECTS} from '../interfaces/view';
import {markAncestorsForTraversal} from '../util/view_utils';
import {InjectionToken} from '../../di/injection_token';
import {inject} from '../../di/injector_compatibility';
import {performanceMarkFeature} from '../../util/performance';
import {Injector} from '../../di/injector';
import {assertNotInReactiveContext} from './asserts';
import {assertInInjectionContext} from '../../di/contextual';
@@ -267,6 +267,7 @@ export const BASE_EFFECT_NODE: Omit<EffectNode, 'fn' | 'destroy' | 'injector' |
if (!this.cleanupFns?.length) {
return;
}
const prevConsumer = setActiveConsumer(null);
try {
// Attempt to run the cleanup functions. Regardless of failure or success, we consider
// cleanup "completed" and clear the list for the next run of the effect. Note that an error
@@ -276,6 +277,7 @@ export const BASE_EFFECT_NODE: Omit<EffectNode, 'fn' | 'destroy' | 'injector' |
}
} finally {
this.cleanupFns = [];
setActiveConsumer(prevConsumer);
}
},
}))();
@@ -32,6 +32,7 @@ import {
signal,
SimpleChanges,
TemplateRef,
untracked,
ViewChild,
ViewContainerRef,
} from '../../src/core';
@@ -192,6 +193,49 @@ describe('reactivity', () => {
expect(cleanupCount).toBe(2);
});
it('should run effect cleanup as untracked', async () => {
@Component({
template: '',
})
class Cmp {
counter = signal(0);
effectTrigger = signal(0);
effectRef = effect((onCleanup) => {
this.effectTrigger();
untracked(() => {
if (this.counter() > 1) {
// This is an early bailout in case the effect loops infinitely
throw new Error('Updated consummers in cleanup for not re-trigger the effect');
}
});
onCleanup(() => {
this.counter(); // A signal read but not consummed
this.counter.update((v) => v + 1);
});
});
}
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
await fixture.whenStable();
// initially an effect runs but the default cleanup function is noop
expect(fixture.componentInstance.counter()).toBe(0);
// Triggers a cleanup
fixture.componentInstance.effectTrigger.update((v) => v + 1);
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.componentInstance.counter()).toBe(1);
// Destroy triggers a cleanup
fixture.destroy();
expect(fixture.componentInstance.counter()).toBe(2);
});
it('should run effects created in ngAfterViewInit', () => {
let didRun = false;