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$);
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());
});
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);
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()));
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$))
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.
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.
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)
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 ofstartsWith
andtakeUntilDestroyed
which should automatically apply the respective pipe operation.E.g.:
And make takeUntilDestroy true by default
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.
Important note: Since Sub-RFC 4 has been updated after i wrote this. Main methods of conversion have changed. I have updated the names.