Skip to content

Commit d1617c4

Browse files
feat(core): allow removal of previously registered DestroyRef callbacks (#49493)
This change makes it possible to remove a previously registered destroy callback - to do so it is enough to call the unregistration function returned from the onDestroy method call. PR Close #49493
1 parent 24b3e8f commit d1617c4

File tree

6 files changed

+134
-8
lines changed

6 files changed

+134
-8
lines changed

goldens/public-api/core/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ export function destroyPlatform(): void;
445445

446446
// @public
447447
export abstract class DestroyRef {
448-
abstract onDestroy(callback: () => void): void;
448+
abstract onDestroy(callback: () => void): () => void;
449449
}
450450

451451
// @public

goldens/size-tracking/aio-payloads.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"aio-local": {
1313
"uncompressed": {
1414
"runtime": 4325,
15-
"main": 470131,
15+
"main": 469779,
1616
"polyfills": 33836,
1717
"styles": 74561,
1818
"light-theme": 92890,

packages/core/src/di/r3_injector.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export abstract class EnvironmentInjector implements Injector {
127127
/**
128128
* @internal
129129
*/
130-
abstract onDestroy(callback: () => void): void;
130+
abstract onDestroy(callback: () => void): () => void;
131131
}
132132

133133
export class R3Injector extends EnvironmentInjector {
@@ -211,8 +211,9 @@ export class R3Injector extends EnvironmentInjector {
211211
}
212212
}
213213

214-
override onDestroy(callback: () => void): void {
214+
override onDestroy(callback: () => void): () => void {
215215
this._onDestroyHooks.push(callback);
216+
return () => this.removeOnDestroy(callback);
216217
}
217218

218219
override runInContext<ReturnT>(fn: () => ReturnT): ReturnT {
@@ -398,6 +399,13 @@ export class R3Injector extends EnvironmentInjector {
398399
return this.injectorDefTypes.has(providedIn);
399400
}
400401
}
402+
403+
private removeOnDestroy(callback: () => void): void {
404+
const destroyCBIdx = this._onDestroyHooks.indexOf(callback);
405+
if (destroyCBIdx !== -1) {
406+
this._onDestroyHooks.splice(destroyCBIdx, 1);
407+
}
408+
}
401409
}
402410

403411
function injectableDefOrInjectorDefFactory(token: ProviderToken<any>): FactoryFn<any> {

packages/core/src/linker/destroy_ref.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
import {EnvironmentInjector} from '../di';
1010
import {LView} from '../render3/interfaces/view';
1111
import {getLView} from '../render3/state';
12-
import {storeLViewOnDestroy} from '../render3/util/view_utils';
12+
import {removeLViewOnDestroy, storeLViewOnDestroy} from '../render3/util/view_utils';
1313

1414
/**
1515
* `DestroyRef` lets you set callbacks to run for any cleanup or destruction behavior.
1616
* The scope of this destruction depends on where `DestroyRef` is injected. If `DestroyRef`
1717
* is injected in a component or directive, the callbacks run when that component or
1818
* directive is destroyed. Otherwise the callbacks run when a corresponding injector is destroyed.
19+
*
20+
* @publicApi
1921
*/
2022
export abstract class DestroyRef {
2123
// Here the `DestroyRef` acts primarily as a DI token. There are (currently) types of objects that
@@ -24,9 +26,22 @@ export abstract class DestroyRef {
2426
// - `EnvironmentInjector` when retrieved from an environment injector
2527

2628
/**
27-
* Registers a destroy callback in a given lifecycle scope.
29+
* Registers a destroy callback in a given lifecycle scope. Returns a cleanup function that can
30+
* be invoked to unregister the callback.
31+
*
32+
* @usageNotes
33+
* ### Example
34+
* ```typescript
35+
* const destroyRef = inject(DestroyRef);
36+
*
37+
* // register a destroy callback
38+
* const unregisterFn = destroyRef.onDestroy(() => doSomethingOnDestroy());
39+
*
40+
* // stop the destroy callback from executing if needed
41+
* unregisterFn();
42+
* ```
2843
*/
29-
abstract onDestroy(callback: () => void): void;
44+
abstract onDestroy(callback: () => void): () => void;
3045

3146
/**
3247
* @internal
@@ -46,8 +61,9 @@ class NodeInjectorDestroyRef extends DestroyRef {
4661
super();
4762
}
4863

49-
override onDestroy(callback: () => void): void {
64+
override onDestroy(callback: () => void): () => void {
5065
storeLViewOnDestroy(this._lView, callback);
66+
return () => removeLViewOnDestroy(this._lView, callback);
5167
}
5268
}
5369

packages/core/src/render3/util/view_utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,15 @@ export function storeLViewOnDestroy(lView: LView, onDestroyCallback: () => void)
192192
}
193193
lView[ON_DESTROY_HOOKS].push(onDestroyCallback);
194194
}
195+
196+
/**
197+
* Removes previously registered LView-specific destroy callback.
198+
*/
199+
export function removeLViewOnDestroy(lView: LView, onDestroyCallback: () => void) {
200+
if (lView[ON_DESTROY_HOOKS] === null) return;
201+
202+
const destroyCBIdx = lView[ON_DESTROY_HOOKS].indexOf(onDestroyCallback);
203+
if (destroyCBIdx !== -1) {
204+
lView[ON_DESTROY_HOOKS].splice(destroyCBIdx, 1);
205+
}
206+
}

packages/core/test/acceptance/destroy_ref_spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,40 @@ describe('DestroyRef', () => {
2222
envInjector.destroy();
2323
expect(destroyed).toBe(true);
2424
});
25+
26+
it('should allow removal of destroy callbacks', () => {
27+
let destroyed = false;
28+
const envInjector = createEnvironmentInjector([], TestBed.inject(EnvironmentInjector));
29+
30+
const unRegFn = envInjector.get(DestroyRef).onDestroy(() => destroyed = true);
31+
expect(destroyed).toBe(false);
32+
33+
// explicitly unregister before destroy
34+
unRegFn();
35+
envInjector.destroy();
36+
expect(destroyed).toBe(false);
37+
});
38+
39+
it('should removal single destroy callback if many identical ones are registered', () => {
40+
let onDestroyCalls = 0;
41+
const onDestroyCallback = () => onDestroyCalls++;
42+
const envInjector = createEnvironmentInjector([], TestBed.inject(EnvironmentInjector));
43+
const destroyRef = envInjector.get(DestroyRef);
44+
45+
// Register the same fn 3 times:
46+
const unregFn = destroyRef.onDestroy(onDestroyCallback);
47+
destroyRef.onDestroy(onDestroyCallback);
48+
destroyRef.onDestroy(onDestroyCallback);
49+
50+
// Unregister the fn 1 time:
51+
unregFn();
52+
53+
envInjector.destroy();
54+
55+
// Check that the callback was invoked only 2 times
56+
// (since we've unregistered one of the callbacks)
57+
expect(onDestroyCalls).toBe(2);
58+
});
2559
});
2660

2761
describe('for node injector', () => {
@@ -139,4 +173,60 @@ describe('DestroyRef', () => {
139173
expect(onDestroySpy).toHaveBeenCalled();
140174
});
141175
});
176+
177+
it('should allow removal of view-scoped destroy callbacks', () => {
178+
let destroyed = false;
179+
180+
@Component({
181+
selector: 'test',
182+
standalone: true,
183+
template: ``,
184+
})
185+
class TestCmp {
186+
unRegFn: () => void;
187+
constructor(destroyCtx: DestroyRef) {
188+
this.unRegFn = destroyCtx.onDestroy(() => destroyed = true);
189+
}
190+
}
191+
192+
const fixture = TestBed.createComponent(TestCmp);
193+
expect(destroyed).toBe(false);
194+
195+
// explicitly unregister before destroy
196+
fixture.componentInstance.unRegFn();
197+
198+
fixture.componentRef.destroy();
199+
expect(destroyed).toBe(false);
200+
});
201+
202+
it('should removal single destroy callback if many identical ones are registered', () => {
203+
let onDestroyCalls = 0;
204+
const onDestroyCallback = () => onDestroyCalls++;
205+
206+
@Component({
207+
selector: 'test',
208+
standalone: true,
209+
template: ``,
210+
})
211+
class TestCmp {
212+
unRegFn: () => void;
213+
constructor(destroyCtx: DestroyRef) {
214+
// Register the same fn 3 times:
215+
this.unRegFn = destroyCtx.onDestroy(onDestroyCallback);
216+
this.unRegFn = destroyCtx.onDestroy(onDestroyCallback);
217+
this.unRegFn = destroyCtx.onDestroy(onDestroyCallback);
218+
}
219+
}
220+
221+
const fixture = TestBed.createComponent(TestCmp);
222+
expect(onDestroyCalls).toBe(0);
223+
224+
// explicitly unregister 1-time before destroy
225+
fixture.componentInstance.unRegFn();
226+
227+
fixture.componentRef.destroy();
228+
// Check that the callback was invoked only 2 times
229+
// (since we've unregistered one of the callbacks)
230+
expect(onDestroyCalls).toBe(2);
231+
});
142232
});

0 commit comments

Comments
 (0)