DEV Community

Cover image for Sub-RFC 4 for Angular Signals sparks interesting discussion started by RxJS author — Ben Lesh
Daniel Glejzner for This is Angular

Posted on • Edited on

Sub-RFC 4 for Angular Signals sparks interesting discussion started by RxJS author — Ben Lesh

Generated by MidJourney AI

Custom graphics generated by MidJounrey AI

Before we dive in to the interesting discussion — it’s essential to know that everyone has the opportunity to actively participate and express their opinions on upcoming Angular modifications in public discussions under existing sub-RFCs. The focus here is sub-RFC 4.

What are Signals and sub-RFC4?

Quick intro on the whole topic of Angular Signals. In a nutshell Signals are new approach to reactivity in Angular, simplifying reactivity while simultaneously enabling fine-tuned control over component updates.

Sub-RFC 4 is taking on the important topic of interoperability with RxJS Observables, a fundamental way of how we manage reactivity in Angular applications today.

Signals and RxJS compatibility

Seamless compatibility with current RxJS-based applications and libraries is what Angular Signals aim for.

Sub-RFC 4 presents two innovative APIs, toObservable and toSignal, conversation between Observables and Signals. You can find them in @angular/core/rxjs-interop. Keep in mind it’s all work in progress.

toSignal

convert an RxJS Observable to an Angular Signal using toSignal:

const counter: Signal<number> = toSignal(counter$);
Enter fullscreen mode Exit fullscreen mode

Internally, toSignal subscribes to the supplied Observable and updates the returned Signal each time the Observable emits a value. The subscription is established immediately, and Angular will automatically unsubscribe when the context in which it was created is destroyed.

Initial value

Using toSingal, you can provide a default value to use if the Observable hasn’t emitted by the time the Signal is read:

const secondsObs = interval(5000);
const seconds = toSignal(secondsObs, 0);
effect(() => {
  console.log(seconds());
});
Enter fullscreen mode Exit fullscreen mode

Error and completion states

Signals function as value wrappers, notifying consumers when the value changes. Observables, however, have three types of notifications: next, error, and complete. When an Observer created by toSignal is notified of an error, it will throw the error the next time the Signal is read. To handle the error at the Signal usage point, you can employ catchError on the Observable side or computed on the Signal side.

Signals lack a “complete” concept, but you can represent the completion state using an alternative signal or the materialize operator.

toObservable

Signal to Observable? Use toObservable - it takes an Angular Signal and returns an Observable. It does this by creating an effect when the Observable is subscribed to, which takes values from the signal and streams them to subscribers.

const count: Observable<number> = toObservable(mySignal);

Enter fullscreen mode Exit fullscreen mode

The Observable produced by toObservable uses an effect to send the next value. All values emitted by the toObservable Observable are delivered asynchronously.

If you want to get the first value synchronously, you can use the startWith operator:

const obs$ = toObservable(mySignal).pipe(startWith(mySignal()));
Enter fullscreen mode Exit fullscreen mode

Lifecycle and Cleanup

When a toObservable Observable is subscribed, it creates an effect to monitor the signal, which exists until that subscriber unsubscribes.

This differs from toSignal, which automatically cleans up its subscription when the context in which it was created is destroyed.

If desired, it’s straightforward to tie the resulting Observable to the component’s lifecycle manually:

const myValue$ = toObservable(myValue).pipe(takeUntil(this.destroy$))
Enter fullscreen mode Exit fullscreen mode

Conclusion

All of this has been created to allow easy way of bridging RxJS Observables and Signals. Angular team wants to minimise the impact of changes allowing us to easily work with both approaches.

Generated by MidJourney AI

The Angular Signals and Observables Debate

Now to the more interesting part. The sub-RFC 4 proposal sparked a discussion about the possibilities of integrating Angular Signals with Observables. It all comes down to whether Angular Signals should adopt globally understood common interop points like Symbol.asyncIterator and Symbol.observable.

Ben Lesh on his vision

Ben Lesh thinks that making signals fit observable chains directly is a good idea, stating that Signals inherently possess a time dimension that makes them well-suited for this. By adopting common interop points, Angular Signals could achieve better compatibility across various platforms.

Alex Rickabaugh pointing out potential issues

However, Alex Rickabaugh mentions that team has been thinking about this approach, explaining that Angular Signals have unique characteristics that make it challenging to safely read them during the change propagation phase.

Additionally, signals require an injection context, which could result in surprising and burdensome requirements when used inside Observable pipelines. Alex states that signals are not Observables, and converting between them should be an intentional operation to ensure a well-considered application architecture.

The Angular Team’s Stance

Angular team remains firm in their decision not to implement InteropObservable or any Subscribable interface for Angular Signals. Although this means no immediate changes to the framework, the debate has undoubtedly generated valuable insights and raised important questions about the future of Angular Signals and Observables.

Generated by MidJourney AI

Stay informed

The Angular sub-RFC 4 debate highlights the challenges and considerations involved in enhancing Angular. As the community continues to explore these ideas — discussions like this are going to happen in the end resulting in better final implementation. I believe it’s important to stay informed about such conversations and even participate if you feel like you have a valuable point to add to discussion.


I hope you liked my article!

If you did you might also like what I am doing on Twitter. I am hosting live Twitter Spaces about Angular with GDEs & industry experts! You can participate live, ask your questions or watch replays in a form of short clips :)

If you are interested drop me a follow on Twitter @DanielGlejzner — would mean a lot :). Thank You!

Top comments (4)

Collapse
 
fatalmerlin profile image
Merlin

Interesting topic!

To save some boilerplate code I'd like to propose that fromSignal should be extended with either two boolean parameters or an options object parameter to simplify the usage of startsWith and takeUntilDestroyed which should automatically apply the respective pipe operation.

E.g.:

function fromSignal (
  signal: Signal,
  immediate: boolean = false,
  takeUntilDestroyed: boolean = false
) { ... }

// or with options object
function fromSignal (
  signal: Signal,
  options?: {
    immediate?: boolean,
    takeUntilDestroyed?: boolean
  }
) { ... }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
timsar2 profile image
timsar2

And make takeUntilDestroy true by default

Collapse
 
aboudard profile image
Alain Boudard

Oh yeah, I'd love to see that, because let's be honest, it really is the default behavior that we use in most components.

Collapse
 
danielglejzner profile image
Daniel Glejzner

Important note: Since Sub-RFC 4 has been updated after i wrote this. Main methods of conversion have changed. I have updated the names.