In RxJS several observable have cleanup function ran at unsubscription, such as timer()
. I'm trying to understand what's the best approach to implement that in pure signal. Some of my attempts on stackblitz.
The below code shows how one could implement a timer from the ground up in RxJS:
function getTimerRxjs(frequency: number): Observable<number> {
// Below code is equivalent to timer(frequency)
return new Observable((observer) => {
let value = 0;
let lastTimeout;
const loop = () => {
console.log('getTimerRxjs loop is running');
observer.next(value);
value += 1;
lastTimeout = setTimeout(loop, frequency);
};
lastTimeout = setTimeout(loop, frequency);
return () => {
if (lastTimeout) clearTimeout(lastTimeout);
};
});
}
Option A: In an attempt to reproduce a similar behavior, you could pass DestroyRef to the function generating the timer as follow:
function getTimerWithRef(frequency: number, destroyRef: DestroyRef): Signal<number> {
const timer = signal(-1);
let lastTimeout;
const loop = () => {
console.log('getTimerWithRef loop is running');
timer.update((value) => value + 1);
lastTimeout = setTimeout(loop, frequency);
};
lastTimeout = setTimeout(loop, frequency);
destroyRef.onDestroy(() => {
if (lastTimeout) clearTimeout(lastTimeout);
});
return timer;
}
Option B: You could inject destroyRef at runtime in the function as follow:
function getTimerAutoCleanup(frequency: number): Signal<number> {
const timer = signal(-1);
let lastTimeout;
const loop = () => {
console.log('getTimerAutoCleanup loop is running');
timer.update((value) => value + 1);
lastTimeout = setTimeout(loop, frequency);
};
lastTimeout = setTimeout(loop, frequency);
inject(DestroyRef).onDestroy(() => {
if (lastTimeout) clearTimeout(lastTimeout);
});
return timer;
}
While Option B seems elegant, I fear the inject()
call may not resolve to the correct context.
@Injectable()
, would the inject(DestroyRef)
resolve to the component or to the
service?I need help to find which option would be more idiomatic in this context.
Have a look at the solution used by the angular team in the toSignal()
function. This function converts a rxJS Observable into a signal and also includes cleanup functionality. In short, their main solution is option B but they provide a way to inject your own Injector (which in its turn injects a DestroyRef, like option A).
https://angular.io/api/core/rxjs-interop/toSignal
const cleanupRef =
requiresCleanup ? options?.injector?.get(DestroyRef) ?? inject(DestroyRef) : null;
About your concern of the correct context, don't forget that you can only call inject()
from an injection context which should then automatically use the correct context, but this does mean that when calling the function in any other place you'll have to provide the correct injector yourself, either by providing it or using runInInjectionContext(injector, () => {})
(The EnvironmentInjector
member got deprecated in favor of the standalone version).