Skip to content

Commit cc41758

Browse files
garrettldAndrewKushnir
authored andcommitted
fix(core): allow onDestroy unregistration while destroying (#50237)
Ensure that all onDestroy callbacks are called by allowing unregistering of onDestroy hooks while destroying. Fixes #50221 PR Close #50237
1 parent 74099d4 commit cc41758

File tree

4 files changed

+57
-4
lines changed

4 files changed

+57
-4
lines changed

packages/core/src/di/r3_injector.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,18 @@ export class R3Injector extends EnvironmentInjector {
199199
for (const service of this._ngOnDestroyHooks) {
200200
service.ngOnDestroy();
201201
}
202-
for (const hook of this._onDestroyHooks) {
202+
const onDestroyHooks = this._onDestroyHooks;
203+
// Reset the _onDestroyHooks array before iterating over it to prevent hooks that unregister
204+
// themselves from mutating the array during iteration.
205+
this._onDestroyHooks = [];
206+
for (const hook of onDestroyHooks) {
203207
hook();
204208
}
205209
} finally {
206210
// Release all references.
207211
this.records.clear();
208212
this._ngOnDestroyHooks.clear();
209213
this.injectorDefTypes.clear();
210-
this._onDestroyHooks.length = 0;
211214
}
212215
}
213216

packages/core/src/render3/node_manipulation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,12 +467,14 @@ function processCleanups(tView: TView, lView: LView): void {
467467
}
468468
const destroyHooks = lView[ON_DESTROY_HOOKS];
469469
if (destroyHooks !== null) {
470+
// Reset the ON_DESTROY_HOOKS array before iterating over it to prevent hooks that unregister
471+
// themselves from mutating the array during iteration.
472+
lView[ON_DESTROY_HOOKS] = null;
470473
for (let i = 0; i < destroyHooks.length; i++) {
471474
const destroyHooksFn = destroyHooks[i];
472475
ngDevMode && assertFunction(destroyHooksFn, 'Expecting destroy hook to be a function.');
473476
destroyHooksFn();
474477
}
475-
lView[ON_DESTROY_HOOKS] = null;
476478
}
477479
}
478480

packages/core/test/acceptance/destroy_ref_spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,4 +260,31 @@ describe('DestroyRef', () => {
260260
destroyRef.onDestroy(() => {});
261261
}).toThrowError('NG0911: View has already been destroyed.');
262262
});
263+
264+
it('should allow unregistration while destroying', () => {
265+
const destroyedLog: string[] = [];
266+
267+
@Component({
268+
selector: 'test',
269+
standalone: true,
270+
template: ``,
271+
})
272+
class TestCmp {
273+
constructor(destroyCtx: DestroyRef) {
274+
const unregister = destroyCtx.onDestroy(() => {
275+
destroyedLog.push('first');
276+
unregister();
277+
});
278+
destroyCtx.onDestroy(() => {
279+
destroyedLog.push('second');
280+
});
281+
}
282+
}
283+
284+
const fixture = TestBed.createComponent(TestCmp);
285+
expect(destroyedLog).toEqual([]);
286+
287+
fixture.componentRef.destroy();
288+
expect(destroyedLog).toEqual(['first', 'second']);
289+
});
263290
});

packages/core/test/acceptance/environment_injector_spec.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Component, createComponent, createEnvironmentInjector, ENVIRONMENT_INITIALIZER, EnvironmentInjector, inject, InjectFlags, InjectionToken, INJECTOR, Injector, NgModuleRef, ViewContainerRef} from '@angular/core';
9+
import {Component, createComponent, createEnvironmentInjector, DestroyRef, ENVIRONMENT_INITIALIZER, EnvironmentInjector, inject, InjectFlags, InjectionToken, INJECTOR, Injector, NgModuleRef, ViewContainerRef} from '@angular/core';
1010
import {R3Injector} from '@angular/core/src/di/r3_injector';
1111
import {RuntimeError, RuntimeErrorCode} from '@angular/core/src/errors';
1212
import {TestBed} from '@angular/core/testing';
@@ -27,6 +27,27 @@ describe('environment injector', () => {
2727
expect(destroyed).toBeTrue();
2828
});
2929

30+
it('should allow unregistration while destroying', () => {
31+
const destroyedLog: string[] = [];
32+
33+
const parentEnvInjector = TestBed.inject(EnvironmentInjector);
34+
const envInjector = createEnvironmentInjector([], parentEnvInjector);
35+
const destroyRef = envInjector.get(DestroyRef);
36+
37+
const unregister = destroyRef.onDestroy(() => {
38+
destroyedLog.push('first');
39+
unregister();
40+
});
41+
destroyRef.onDestroy(() => {
42+
destroyedLog.push('second');
43+
});
44+
45+
expect(destroyedLog).toEqual([]);
46+
47+
envInjector.destroy();
48+
expect(destroyedLog).toEqual(['first', 'second']);
49+
});
50+
3051
it('should see providers from a parent EnvInjector', () => {
3152
class Service {}
3253

0 commit comments

Comments
 (0)