3

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.

  • If create this signal from an @Injectable(), would the inject(DestroyRef) resolve to the component or to the service?
  • Are there other risks of using Option B where some injection error may only surface at runtime?

I need help to find which option would be more idiomatic in this context.

luiscla27
  • 4,956
  • 37
  • 49
FunkySayu
  • 7,641
  • 10
  • 38
  • 61
  • Try changing the name of this question to an actual question (one single question). Somehow it feels like you're are asking "I have this 3 approaches, which one is the best? what are the drawbacks of my code?" which is not the way SO is meant to be used. There's not much that can be answered right now. Currently, you have at least 3 questions and an example that tell us that you already have 3 answers. – luiscla27 May 12 '23 at 20:41
  • Done. Adjusted a bit the text to reduce the size as well. Ultimately I _think_ there is an answer to this question which may benefit the rest of the community (how to cleanup once a signal is unused), but narrowing also sounds good to me. – FunkySayu May 12 '23 at 21:01

1 Answers1

3

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;

https://github.com/angular/angular/blob/237d90f4e38f528ae20aeedd09894aa63fb9de14/packages/core/rxjs-interop/src/to_signal.ts#L163

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).

https://angular.io/errors/NG0203

https://angular.io/api/core/runInInjectionContext

Laurence Mommers
  • 687
  • 5
  • 12