[Watch This Space] Angular Reactivity with Signals · angular/angular · Discussion #49090
tl;dr: we've begun some prototyping work around adding signals as a reactive primitive in Angular, in advance of a formal Request For Comments (RFC) which we plan to open soon. Prototyping early and in the open aligns with our team values and allows us to get the most out of an RFC.
If you've seen our talk from ng-conf 2022, Angular: A Design Review 10 Years Later, you know we've been thinking long and hard about some fundamental design decisions of the framework. Last year, we kicked off a long-term research project with an ambitious goal: to embrace fine-grained reactivity in the core of the framework.
In a fine-grained reactive web framework, components track which parts of the application's data model they depend on, and are only synchronized with the UI when that model changes. This is fundamentally different from how Angular works today, where it uses zone.js to trigger global top-down change detection for the whole application.
We believe adding built-in reactivity to Angular unlocks many new capabilities, including:
- A clear and unified model for how data flows through an application.
- Built-in framework support for declarative derived state (a common feature request).
- Synchronizing only the parts of the UI that needs updated, at or even below the granularity of individual components.
- Significantly improved interoperability with reactive libraries such as RxJS.
- Better guardrails to avoid common pitfalls that lead to poor change detection performance and avoid common pain points such as
ExpressionChangedAfterItHasBeenCheckederrors. - A viable path towards writing fully zoneless applications, eliminating the overhead, pitfalls, and quirks of zone.js.
- Simplification of many framework concepts, such as queries and lifecycle hooks.
Changing the reactivity model of an established framework like Angular is a significant project, with numerous challenges. Our plan for this project is broken out into multiple phases:
- Research and development, experimenting with different approaches to reactivity.
- Selection of a candidate design.
- Prototyping of the initial design to demonstrate feasibility.
- One or more community Requests for Comment (RFCs) to explore tradeoffs and inform the final design.
- A developer preview of the core reactivity implementation.
- Iteration and expansion of the core implementation towards the full design.
- Collaboration with the community to bring the entire Angular ecosystem forward into a reactive future.
Over the last few months, we've progressed through the first and second stages of this project, and have converged on a design based on the well-known reactive primitive of signals. During our experimentation we felt that this design demonstrated the strongest alignment with our overall goals.
Signals are not a new idea in the framework space - Preact, Solid, and Vue all employ some version of this concept to great success. We've taken a lot of inspiration from these and other reactive frameworks, and we are especially grateful to Ryan Carniato of SolidJS for his willingness to share his expertise and experience in many conversations over the last year.
That said, requirements across frameworks differ widely, and we've designed our version of signals to both meet Angular's specific needs and as well as take full advantage of Angular's unique strengths. Even though it's still in the prototype stage, there are a few aspects of our design which we're particularly proud of:
- Lazy evaluation, which is both efficient and avoids the need for explicit batching operations
- A computation model which doesn't require immutable data
- Flexible effect scheduling, allowing for seamless integration with Angular
- Clever use of
WeakRefto avoid explicit lifecycle management of signals - Possibilities around using our compiler to optimize various reactive operations, such as scheduling effects to automatically minimize reflows when performing DOM operations
- Bidirectional integration story with RxJS reactivity
We've begun prototyping this design and will be integrating it into Angular over the next few months. We're committed to our open source spirit and plan to conduct these prototyping efforts in the open. This prototyping is essential to proving that our design for signals in Angular is viable and allows us to progress to a community RFC. The RFC will cover the rationale for using signals and the detailed design of various parts (our implementation, integration with RxJS, and other topics related to this effort).
We strongly believe that adding built-in reactivity to Angular is in the long term best interest of the framework and its users, and look forward to sharing more information about this project in the upcoming RFC.
FAQ
When will the RFC be?
Sometime later this year, depending on how smoothly the prototyping efforts progress.
Why signals as the reactive primitive?
This is a great question, and one which will be thoroughly discussed in the upcoming RFC. In short, signals have many of the properties we identified as desirable in a reactivity system designed for Angular, including:
- Values that are always available (synchronously)
- Reading a value does not trigger side effects
- Consistency between values (reads can't show inconsistent state)
- Implicit, low overhead subscriptions
- Automatic and dynamic tracking of dependencies
Why are you committing code before the RFC?
There are a few reasons why we've chosen to begin prototyping before opening an RFC:
-
We feel having a real prototype will amplify the value we can gain from an RFC, as we will be able to share more detailed designs and ask more precise questions.
-
In any major design, there are challenges and constraints which only become visible at the scale of a full prototype. We want to uncover these and account for them in the RFC itself.
-
Many technical problems are unrelated to the overall reactivity system design (such as how it will integrate into Angular's change detection algorithm). We want to tackle these problems sooner rather than later.
-
Google's internal codebase has a different set of build tools and constraints than the external ecosystem, and early prototyping is necessary to validate our designs in that environment as well.
0 replies
Amazing. Can't wait to see the prototype 😃
6 replies
Here is the prototype of the signals lib: #49091
So basically, it is the Reactivity API from Vue with different function names?
Do you have any idea yet how we use it in our HTML templates? Will it be always the function call to extract the value?
In Vue, it is possible to just use the property name, which makes it easier.
In Vue, refs are not functions. You can do {{ foo }} instead of {{ foo.value }}.
With signals as functions, you would just do {{ foo() }} to get the value, and {{ doSomethingWith(foo) }} to pass the signal itself around.
I'd rather stay consistent with the way it's done in TypeScript than allow avoiding two parentheses and wonder why it would call the function.
Here is the prototype of the signals lib: #49091
Not sure why I muted notifications for the post. Anyway, I tried the demo and it WORKS. My only complaint and you addresses it in other comment is the syntax. I wish there is way to directly to get value instead of declaring const readOnlyCounter = () => counter(); or using counter() in template.
const counter = signal<number>(0);
I will play around with it more in coming days if not weeks
1 reply
0 replies
I do not like dirty functions (functions with the internal state, AKA “hooks”), I think that functions should be pure and for keeping the state we have class instances.
So, the fact the “hooks plague” is corrupting Angular is bad news for me, but if it will help us to get rid of zone.js - amazing. Because zone.js is much worse.
15 replies
I think the best way to consume StateAdapt will be with signals. Signals are like the async pipe but not tied to the template. They will have a net simplifying effect on components, despite introducing a new concept. The truth is that there are 2 branches of reactivity: event and state. RxJS (push-based) is best for events, and signals (involving pulling) are best for state.
State selectors probably still have a role in defining portable state management patterns, but I have yet to try to define state adapters with signals.
Not sure if you saw, but I talked about this in my article here https://dev.to/this-is-angular/i-changed-my-mind-angular-needs-a-reactive-primitive-n2g
Couldn't one easily make a decorator to wrap a signal in a getter and setter?
I'm not seeing the downsides here
I'm pretty sure Angular will die quickly if they commit to using hooks for reactivity.
Hooks are one of the most awful design choices that have ever been made.
Hooks are just functions. The awfulness comes from React's rendering model. And hooks are still far superior to what React had before.
So basically after 7 years of development you figured out that RxJS is "doesn't fit all the needs" huh?
Let me guess, after you release these Singals, you'll rewrite the Engine "based on our brand new Signals system" ™️
How about focusing on the real things, like make HMR work in 2023? #39367
14 replies
Yes I want to use RxJS and I want Angular to improve their integration with RxJS instead of making up new things, that could turn out to be a failure in another 7 years. Another operator needed? No problem, make it. But not a hook-like "something else".
Even if @haskelcurry did not take the most diplomatic way, I think he is right in the core statement. In addition, the whole signal topic seems to be very competition-driven - at least that's how it seems to me.
Also, I don't think Angular really needs to be made more beginner friendly or that it should be focused on that.
As @haskelcurry said on another comment:
Improve Error handling. Improve Forms. DevTools. HMR. Elements. Schematics. DI. Improve type checking. etc.
I think the way should be to get rid of ngZone with existing means instead of introducing a new concept.
As someone who teaches Angular within my company, I definitely welcome the easing of the learning journey, and I think it's something the team probably gets feedback on a lot. With standalone components and this evolution, if feels like they're working towards making the framework more approachable to new devs, while also dealing with the architectural issues (zone.js).
Hi all, I'd love to encourage move this discussion to the correct place, since HMR is not blocking or tied to this proposal, and both will improve Angular.
@dgp1130 wrote up a great issue of ideas on this topic here: angular/angular-cli#24755
Angular is the best framework
31 replies
@davidivkovic I did FP/FRP on commercial projects with Haskell, Clojure / Clojurescript with re-frame, Scala on the backend side, Elm, CycleJS, SolidJS and of course React (which is btw also far from true FP, despite their claims and pretending) for decades. I'm with Angular since the initial release of AngularJS all the way through, on enterprise-grade commercial projects in production. And I can tell you that Angular is not FP nor FRP, just by design. Neither it is a decent OOP. Adding +1 concept won't change it.
And do you really think people won't start doing NgRx / Redux / other-overcomplexifying-you-name-it on top of Signals too? Ha! It's not an "integrated state management". It's just an RxJS BehavriorSubject + shareReply + distinctUntilChanged out of the box. So why then all the fuss about? Just provide a new RxJS creational operator, that's it. But guess what, it won't change much. Because it's not the reason of failure.
They are moving from one idea to another, following the "community demands", rather than just settle down the good parts that they have - not much, but they are present in the framework, just ruined by lack of attention "because it's just a nice thing to have, not more". Improve Error handling. Improve Forms. DevTools. HMR. Elements. Schematics. DI. Improve type checking. etc.
@haskelcurry BTW the list of features you mentioned have all seen improvements recently or are in the process of being improved right now except maybe Elements. Error handling has and is improving to be very developer friendly, even going to show element and it's event like normal html, Typed Forms came out, New DevTools released, Esbuild is almost done so proper HMR can be supported, we are gettinh new schematics to support standalone, inject function is a different method for DI and can now be used inside constructor.
To answer all those that wish for a real answer on rxjs vs signal or why signals are needed - as someone who tried to "fight" state using observables and tested the new signals I can say signals are a huge improvement for sync state. Using shareReplay and distinctUntilChanged help solve some of the problems but the main problem is the diamond problem. The moment you use combineLatest you encounter the "glitched selectors" problem. The only way to solve it is using a scheduler which makes it even more complicated.
Now let's take an example - you created a selector using combineLatest and saw the glitch effect so you add asapScheduler and it works! But now this new selector is suddenly async. You have three pitfalls now:
- Update the state and access right after the selector will result in stale data so you have to "wait" (probably
skip(1)) for the updated data - thus making each regular function that needs state access into async function with rxjs. - You can't just bind to the template with async pipe as the data won't be ready in time (it's now async) - so you need
viewModelwithngIfaround your whole template to wait for the async selector. - Can a derived selector be sync? How about half sync half async? Do you suddenly need to make all the selectors async?
Those are the reasons by the way that made ngrx use a custom selectors model. The downsides were explicit dependencies (not such a big deal) and the requirement for explicit global state graph which suffers from drawbacks in local states (see this). The answer was component store - which has the problem from above (see debounce).
Now imagine having state and selectors without glitch effect + memoized + batched and on top of all lazy but always ready when needed? In addition they are sync so no need for rxjs when you just want to check something in the state or bind to the UI (now rxjs is really just for async operations). Guess what? That's signals :)
So a big thanks to the angular team!
That's my opinion anyway.
Exciting development. Thanks for all the work and for the team's transparency along the way.
0 replies
I hope it will not have too much impact on the existing development model.
4 replies
Here's how it's gonna be, remember my words:
- They'll say "we invented new cool thing, Signals! It IS NOT a replacement to RxJS, rather a great addition!"
- (After 6 months) "Angular Signals are better than RxJS and here is why"
- (After anohter 6 months) "RxJS is deprecated in favor of Angular Signals, but here are our semi-working schematics to ease a migration process for you"
- (After anohter 6 months) "We have re-written our Engine, now it's called Angular PopCorn™️, all libs should migrate or else they won't work"
Hopefully HMR is done by that time.
Correct me if I'm wrong, but signals as they are implemented are synchronous. While RxJs allows you to handles asynchronous tasks. I think both will still be needed, along with the conversion between observables to signals and signals to observables.
@haskelcurry Have you used RxJS? It sounds like you haven't. It's pretty great, you should give it a try.
@haskelcurry even though your way of saying stuff is unfiltered but your timeline looks very true and so it makes it even funnier, now ng team has to come up with alternative solution for saying this too. But with that said even though HMR is a pain in the neck so does zonejs. It would be nice to have some other solution. For Rxjs part it is great for doing reactivity but having to use so many operators is also a pain when I can do something simply and even if down the line they say it was a bad alternative so what we can have something else but if you are using angular you should be appreciative and that doesn't mean you have to agree with what they say or anybody say, All you have to do is think before you write and think about how many hours they have to spend to make this work and the stress that comes with this to not break thousands of apps and keep the codebase backward compatible while transitioning. Anway's, looks like you have great insight on all of this than I might ever have but putting your opinions forward like you are not stabbing someone would be great.
Awesome!
Maybe the mental model behind the signals can implement the InteropObservable type to build the bridge between rxjs and signals, translating between both will ease!
5 replies
@menosprezzi The concepts signalFromObservable() and observableFromSignal() are in the planning.
So there is going to be full interoperability between the two concepts.
They complement each other and do not take anything away!
What signals really open up in the feature is an alternative to zoneJs. I'm really looking forward t that future!
@SanderElias nice! But I mean that implementing InteropObservable interface you can use the signal directly in your rxjs pipes if you need.
For instance, lets say that angular changes the @input to wrap values into a signal, you can still use it with the rxjs's reactive utils if you need:
class MyCmp { @Input() value: Signal<number>; ngOnInit() { const aNativeObservable$ = this.myService.get(); combineLatest([this.value, aNativeObservable$]).pipe(/* and so on :D */ ) } }
This will ease the migration between both!
@menosprezzi The concepts
signalFromObservable()andobservableFromSignal()are in the planning. So there is going to be full interoperability between the two concepts. They complement each other and do not take anything away!What signals really open up in the feature is an alternative to zoneJs. I'm really looking forward t that future!
I think signalFromObservable() is really essential to have in place when this lands. It also has to automatically handle the subscription. If this gets done right it will be a really nice solution.
here is a link to the pr for InteropObservable tools. #49154
I kinda want to stress-test this RFC with some questions that the Angular team may want to consider:
- How will affect RxJS and the Angular ecosystem? Many libraries and frameworks rely on both Angular and Rx together, so replacing the reactivity system will be a major blow to RxJS and possibly the entire Angular community.
- How will this affect Angular itself? For instance,
@angular/common/httpheavily relies on RxJS, so this RFC may need to consider how much of the core API needs to be changed.
Those are just some questions I can think of at the moment, and I'll update this comment when I think of more questions. I am sorry being a pessimist with this RFC, and I am not trying to shoot this RFC down; however, RxJS is a critical part of Angular, and replacing it will cause millions of websites to break if this RFC is not handled carefully. If this RFC goes through, I would suggest creating an extended LTS version or a compatibility mode, so websites have a good window of time to migrate before the pre-RFC versions sunset.
5 replies
They mentioned it 2 times:
Significantly improved interoperability with reactive libraries such as RxJS.
Bidirectional integration story with RxJS reactivity
I think it means that they are taking care of it and you don't need to worry about your existing code, relying on RxJS. Also, this tweet might be helpful
And if you're an RxJS master, you'll be able to leverage our RxJS interop to compose Observable inputs, queries, lifecycles into your streams.
No @IRod22 you shouldn't be sorry, because you are right: there will never be "magic", even though they claim there is, like with NgUpgrade. Any interop will help us only partially, but there definitely will be a need to worry about existing code.
Thank Angular team! I have been watching the progress of optional zone for many months. It is fantastic to adopt signal fine-grained reactivity to Angular core. Will the signal be able to run outside of component like Solidjs? If it does, that will be super helpful to reuse/organize many common logics.
1 reply
@BruceWeng Yes, they will be available wherever you want to use them. You can use them in services and functions and wherever else you might need a reactive primitive.
0 replies
Should be a great addition! I guess it might change how we look at state management. Is there any thought how signals can enhance state management? Or maybe even a native angular solution?
1 reply
the problem with that is that you just can’t see code organization as a problem. You’re talking about attacking “real” problems, get things done. Quickly. Forcefully. But it’s not how the reality is. In reality first come codebase. Already existing code base which already do something. And the business consider it's working fine. Then they throw at you problems. And you need to code them. All while keeping previously established correctness of your application intact. So reality is not implementing new features, no! Far from that! Reality is CONTINUOUS REFACTORING. And for that you need to keep your code in certain state, Always ready to be refactored. And this state is what they call "referential transparency”, keeping everything in pure functions. Or FP. And when you see reality in this light you will see that this “pure” code organization will also lead you to “correct” implementations of your problems. Not the other way around. (But yeah, I’m talking about real programming, not about those cases when you just every time rewriting everything from scratch in what fashionable at the moment manner)
…On Mar 10, 2023, at 5:07 PM, Timon ***@***.***> wrote: Are we now philosophers? Is it really a discussion about signals in angular anymore? I think there are real world apps that are roughly at this level of complexity and for that I would consider anything that is not straight forward to implement over engineered. Since I am interested in how it is practical in the real world. — Reply to this email directly, view it on GitHub <#49090 (reply in thread)>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AAMUAE6Q2WXJPIDULO6Z54DW3M7UJANCNFSM6AAAAAAU5H2RGI>. You are receiving this because you were mentioned.
4 replies
I think you are right for everything that is not a really simple app that should not evolve over time. But there are also other cases.
@timonkrebs the thing is if it is that complicated for a simple example what will happen when you have a list of data rendered as a grid with column filters and sorting and checkboxes but also as a list on another screen with a dialog to edit data in specific cells. If it all flows from the UI as observables directly I am not so sure it will be easy.
My point is rxjs is awesome and operators are awesome but going with it all in can sometimes backfire. The most important thing is data consistency and a single source of truth state. To achieve that you have selectors and updates avoiding the diamond problem. Signals will shine in this area. Once you get to async operations you should use rxjs as it simplifies the code and makes it easier to reason about.
I think that so called "pure FRP" shines in simple cases and once you have a lot of UI events all connected to some global state you get to the point a new feature or even UI changes can lead to a lot of code changing. Combining imperative updates together with reactive single source state and selectors with rxjs for async timings seems like a good match.
@Harpush I fully agree with you. I am not at all a FRP purist. I did agree with him on:
In reality first come codebase. Already existing code base which already do something. And the business consider it's working fine.
Then they throw at you problems. And you need to code them. All while keeping previously established correctness of your application intact.
So reality is not implementing new features, no! Far from that! Reality is CONTINUOUS REFACTORING. And for that you need to keep your code in certain state,
Always ready to be refactored.
The other stuff was discussed befor and I have never seen a reasonably complex app that did got 100% pure FRP working. So I do not want to get into this discussion. I think your points are really a lot more important. Never the less I also think that there are usecases that fit 100% pure FRP.
@timonkrebs maybe there are... Especially simple ones I believe. Concerning what he wrote it is true for non frp apps too :)
3 replies
Thank you for the heated discussion. I really enjoied it sometimes. At least I now feel battletested when it comes to discussing signals. Hope you can also take something out of this discussions...
@robounohito You are very, very confused about RxJS and what is wrong with subscribing manually. When you read from a signal it is the exact same thing as passing an observable to the async pipe. You are hyper focused on the () syntax instead of code organization. As long as you don't use next or set, you can structure both observables and signals in a 100% declarative way. The manual subscription is not the problem! It's the code people write inside it that's the problem, because the only thing you can possibly do is write imperative side-effects. But actually causing a subscription is identical to what you're doing by passing it to the async pipe! You think it's going to change your code organization whether you are typing value() or value$ | async? Not at all! So are you really worried about code organization, or are you tripping over a meaningless syntactic difference? value() is a read, and 100% declarative, same as value$ | async. Any way that it affects any signals above is the same way the async pipe affects observables above.
@mfp22 I think you are right with what you wrote. But I think he will not be the last guy to get upset by the introduction of signals if we can not make really clear what it is that signals bring to the table to justify the introduction into angular.
I am a huge fan of signals and think its the best feature that was introduced since the introduction of angular. But I also can see how devs would not see the benefits of signals over rxjs before really working with them in complex apps. And I fear that they will not get adopted in the wider community if the devs do not get the real benefits. Because:
- they seem just less capable than rxjs (Because they hide most of their strenght to be intuitive an ealsy to use).
- it is not easy to define where to use signals and where to switch to rxjs ( Where are best practices)
- signals do a lot that zonejs and the cd did for free (sure with some edgecases but a lot of people did not get these and hack around until they seem to go away => for example NG0100). With signals some of that now gets pushed into the mind of the devs ( and it seems also devs would be responsible like in rxjs). But most will not get it, without making it really clear, that signals are much more a solution to fix the edge cases like NG0100 than a solution to what they (sometimes have to) use rxjs for... And I can see how it feels just like an other (even worse because less capable) solution to the problems that they now solve with rxjs.
I was reading about fromObservable and fromSignal to convert Observables to Signals and vice versa...
How would fromObservable behave if the Observable does not immediately emit?
E.g.
const myObs$ = of('someValue').pipe(
delay(5000)
)
const mySignal = fromObservable(myObs$)
I thought that a Signal always has an initial value...
What is the initial value of the Signal in this case? undefined? null?
6 replies
There's no initial value if the Observable doesn't emit the first item synchronously AND you don't pass in initial value. You'll get an error if the first time you read the Signal, the Observable still hasn't emitted. That's the current impl
There's no initial value if the Observable doesn't emit the first item synchronously AND you don't pass in initial value. You'll get an error if the first time you read the Signal, the Observable still hasn't emitted. That's the current impl
Throwing would be much better than emitting null / undefined. That would be great.
I am secretly hoping for a fromObservable pipe in Angular...
Then the RxJS lovers can do their RxJS stuff as usual and benefit from faster CD without the need to introduce Signals in the TypeScript code. The fromObservable pipe would most likely not have the strange null typing issues of the async pipe.
<p>{{firstName$ | fromObservable}}</p> 😍
I just added a comment. This approach could work, but I would rather have more TS safety.
Just throwing in my 2 cents here...
RxJS doesn't distinguish between observables that emit immediately vs asynchronously. So I don't think consumers of observables should rely on knowledge about when they're going to emit. This could cause unexpected errors. Much better would be to type it as
T | nullalways, but optimally let devs pass in a type argument (or infer from initial state passed in) if they happen to know 100% that the observable emits immediately. But in my opinion even this explicit type parameter is dangerous because you could modify the observable upstream and TypeScript could not prevent a runtime error. And I don't see a significant difference betweennullandundefinedhere, but I haven't thought that through carefully.
The only way safe way out of this TypeScript awkwardness is if the observables themselves contained knowledge about whether they are going to emit immediately or not.
@mfp22 I do agree with you reasoning. The dangerous situation here is that we a given observable might be emitting on subscribe (synchronously) one day and then someone adds an operator somewhere that marks the observable chain as "async" and parts of the application code start to throw. I can see how this could lead to hard-to-diagnose errors.
@alxhub we should probably revisit this and / or ask for feedback in the RFC.
Would there be an equivalent to ngrx component store selectors with signals?
So, when you have a more complex state object with multiple properties in it and your component is only interested in getting updates on one of them, will this be possible and how will it look like with signals?
7 replies
That's what computed is for
Exactly, the computed is essentially a selector built-in into the reactivity / framework.
One that doesn't require a centralized store :)
That's one important statement and a big drawback with current implementations
17 replies
The first example creates a new object on any change, so the result is not the same; thus, any dependencies are executed.
It's true, of course, but how it's possible that the new object created indirectly works without the effect of logging (a new object is also created)?
derivedPropA = computed(() => this.customObject().propA);
derivedIndirectPartialA = computed(() => ({ foo: this.derivedPropA() }));
The second example ...
It's probably a misunderstanding. The question was, what exactly does an object property (or a reference change of any inner object) mutation mean when the object reference is still the same? And when the related effect (binded only to this object) logs.
effect(() => {
console.log('Derived Custom Object:', this.derivedObject());
});
As far as I understand every call to set or update or mutate marks for change. So immutability isn't required.
Anyway derivedPropA is getting memoized so derivedIndirectPartialA don't get affected by non propA changes.
So there are 2 things at play here:
- change notification;
- equality checking.
As @Harpush pointed out, every change method (set, update, mutate) is sending the "changed" notification to all the dependencies. But this change propagation can be "blocked" if an equality method determines that te 2 values are equal. Also, all computed are memoized based on this equality notation.
Our default equality comparator checks for primitive values (strings, numbers etc.) equality, but always assumes that 2 objects are "unequal", effectively making it possible to work with mutable data structures. If one prefers pure immutable data structures an alternative, more strict equality function can be passed as an argument to the signal / computed.
@pkozlowski-opensource is it something that can be globally configured? Or atleast at DI level? So you don't have to pass it over and over? (The equality function)
We can discuss this during the RFC. But generally speaking we would prefer to avoid global config as one can achieve the same effect (pun intended!) by wrapping the signal / computed functions in your own - you could even name those as signal / computed and export from a different entry point.
Just an idea, looking at the current implementation of the graph that keeps tracks of dependencies and versions https://github.com/angular/angular/blob/main/packages/core/src/signals/src/graph.ts specifically at the notification implementation.
ie:
export function producerNotifyConsumers(producer: Producer): void {
for (const [consumerId, edge] of producer.consumers) {
const consumer = edge.consumerRef.deref();
if (consumer === undefined || consumer.trackingVersion !== edge.atTrackingVersion) {
producer.consumers.delete(consumerId);
consumer?.producers.delete(producer.id);
continue;
}
consumer.notify();
}
}
This keeps track of the versions at the edges, could this be replaced with Atomics.waitAsync(sharedArrayOfVersions, consumerId, expectedVersion) ? would that be more performant ?, maybe there is no need to loop through because you are signaled from the OS
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync
0 replies
The prototype of the RxJS interop in this PR looks very promising: https://github.com/angular/angular/pull/49154/files
Thanks for that!
Here a small idea to push RxJS interop one step further (based on the new fromObservable function from the PR):
fromObservable pipe
With the fromObservable pipe it would be possible to move the Observable to Signal conversion to the template.
This pipe could use the new fromObservable function internally to convert an Observable to Signal. When the Signal changes, the Change Detection mechanism has to be notified about changes (and some more stuff which you know much better than me :))
In the template the pipe would look like this:
<p>{{firstName$ | fromObservable}}</p>
Like this, it would be relatively easy to use the better "Signals" Change Detection with existing RxJS code.
Then you only have to change async pipes to fromObservable and you will have the performance gain of Signals (at least in my imagination ;)).
Currently the async pipe also has issues with the initial null typing which is annoying when using strict TypeScript settings. Apparently the fromObservable function does not have the initial null value "issue". That would make the fromObservable pipe the better async pipe.
For code bases which use a lot of RxJS, the fromObservable pipe would be a great addition. There are still a lot of RxJS based APIs in Angular and also a large part of the eco-system exposes RxJS APIs. I am sure you will see a lot of fromObservable conversion code in the TypeScript just for the better "Signals" Change Detection. With the fromObservable pipe we can skip the conversion step. Angular will do it for us.
3 replies
Having conversion in a TS code assures that we are not creating subscription-side-effects multiple times when accessing the same observable multiple times in the same template.
The fromObservable pipe would be an extra. You could still use the fromObservable function in TS code.
The async pipe also creates subscription-side-effects multiple times for accessing the same observable multiple times.
It would be even desirable, if a fromObservable pipe behaves the same way (for consistency).
One more thing I thought about is how would signals play with ngFor for example? If I have a list of objects I want to render I will probably ngFor over them. But that means only the items array is a signal and if the item template is complex I won't get the fine grained benefits.
One option is always to extract to a component and use signal inputs there - but have you thought about those cases? It's the same for a component which accepts content child template with context.
It adds up to the type safety guards in ngIf that won't work as signals values are accessed as functions.
And lastly the ngIf problem whether to use the as syntax or not with signals.
4 replies
Is it true that all of the points mentioned are questions concerning perf optimizations (except the type safety)? Or do you see bigger problems with it? I think the performance problems mentioned could be optimised quite easy in every stage of development...
Maybe the problem could be eased even further with providing pipes for consuming signals:
#49090 (comment)
The scheduling of signals is not optimal for all cases! For debugging or hardcore perf optimization I really would like to have more control over the scheduling! The problem described here could also benefit from this, as it would make it a lot clearer to advanced users that there are more complex things to consider if you need to optimize for perf.
Concerning the type safety guards and the as syntax I think it comes down to providing best practices as I already mentioned in: #49090 (comment)
I think best practices are really important to get a introduction of signals without too much resistance from several corners of the angular community.
Maybe add to Best Practice:
- Do not use
assyntax with signals (if you want to optimize for perf). (maybe provide examples why it could be problematic)
I think this is a much more fundamental problem than just perf improvements!
My conclusion to tackle the underlying problems of what @Harpush mentioned:
- provided best practices are really important
- pipes for control over how signals are scheduled => for better perf and debugging/education
- a third change detection strategy called onPull => to get a better migration story for old apps to signals
0 replies
6 replies
That's quite frustrating that you personally lean toward signals, which are much less powerful than observables. Some kind of "casualization" of Angular - desire to make Angular popular by simplifying it, because most people are lazy and don't want to learn new things. It says that with time observables will be just second-class citizens in Angular, and Angular basically will become a copy of SolidJS. Would be better to read from you that async pipe would support signals.
@e-oz if you think about signals as a much more powerful replacement of zone js and current change detection strategy, the move of the angular team does make a lot more sense.
With signals the days of caring about https://angular.io/errors/NG0100 and worrying about calling (expensive) functions in the template will be gone. You should think of it a lot more as a change detection mechanism.
And in the future it will enable a lot more features that would not be possible without it. It will never be a replacement for rxjs though!
If it was a replacement for rxjs I would not be so happy about the introduction of signals as I am a really big fan of rxjs.
they still allow using async keyword for compute & mutate! I believe it will lead to new bugs :-)
This could be prevented with:
https://stackblitz.com/edit/angular-ynqnke?file=src%2Fmain.ts,src%2Fsignals%2Fcomputed.ts
type NonPromise<T> = T extends Promise<any> ? never : T; /** * Create a computed `Signal` which derives a reactive value from an expression. * * @developerPreview */ export function computed<T>( computation: () => NonPromise<T>, equal: ValueEqualityFn<T> = defaultEquals ): Signal<T> { const node = new ComputedImpl(computation, equal); return markSignal(node.signal.bind(node)); }
But maybe this pattern (counter = computed<number>(async () => { ...})) could be used to get even more power. Its probably not worth to prevent.
@timonkrebs don't tell me what I "should" and I will not tell you where to go. That error you linked doesn't bother me for many years - all our components have an OnPush strategy.
It will never be a replacement for rxjs though!
that's for sure. Something that can be only synchronous can not replace something that can be synchronous or asynchronous.
@e-oz I'm sorry for beeing a bit to pushy. I think it is the following that made me reply:
Some kind of "casualization" of Angular - desire to make Angular popular by simplifying it, because most people are lazy and don't want to learn new things.
For me signals are the new thing that gets me excited! Not becaus I think it makes angular more casual to use. But because it gives more power and knowledge over the change mechanism to the devs.
For some apps the benefit of signals might not be that important but it is not something that has to be adopted... I really hope/think that using angular exclusively with onPush and rxjs will never go away.
@pkozlowski-opensource
I have a few questions that are probably in minds of all here:
- What part of Angular will be rewritten or provided with an alternative for observables?
a. Event emitters (@Output)
b. Forms
c. Routing data (ActivatedRoute etc)
d. If any more please share - Are signals at least in part due for v16 or it will be strictly
@developerPreviewin it - Is Angular team thinking about providing its own store implementation in the future or will depend on community implementations like
ngrx - Are other improvements to Angular postponed as more hands are working on signals and framework overhaul on it? For example improvements to Reactive Forms were taken down from milestones. Or maybe they are postponed because of a big overhaul to underlying elements and are thought to be easier to achieve after signals or will be part of signals overhaul altogether.
2 replies
Please, consider adding something like @SignalInput that could be used instead of @Input and it would indicate that specific input should be exposed as Signal inside component if we would like to go fully with this concept:
class ExampleComponent { @Input() standardInput: number; @SignalInput() signalInput: Signal<number>; }
without this we would need to create setters and private fields that sets these values as signals
3 replies
As a developer of a few Enterprise UI libraries, I'm not so sure that having different Input types will be a success.
On one hand, using that approach on the template side could be quite predictable and the compiler could know how to wrap or not signals but on the other hand, you have to make it possible to set input imperatively after a component resolve phase (see https://angular.io/guide/dynamic-component-loader#resolving-components).
const viewContainerRef = this.adHost.viewContainerRef;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent<AdComponent>(adItem.component);
componentRef.instance.data = adItem.data;
so data could be a signal ... basically you are exposing an internal state handling over the external component API surface.
@meriturva ComponentRef#setInput should be able to take care of the difference?
Thanks @nartc ... Actually setInput is not typed that's why is it not so useful and so used but anyway could be a way to proceed.
I mean: having something like
const componentRef = viewContainerRef.createComponent<AdComponent>(adItem.component);
(componentRef.instance as AdComponent).data = adItem.data;
is really common and I repeat exposing internal concepts is not so great in general.
You're solving one of the interesting problems in Angular. Hats off!!!! That said, this got me extremely curious, and have some questions/comments! :)
-
It feels like signals and state management libraries (such as NGXS and NGRS) are trying to solve the same problem. Why not use the terminologies from the Redux pattern (such as state, action, selector, etc).
-
Regarding the API -
effect(() => xyz).... One of the things, I found odd in the react world is the react hooks - useState(), useEffect(), useContext(). I always felt they take away the readability of the code. I am not sure about bringing those concepts to Angular is a good idea. (Maybe I'll have to see more examples of the API to start accepting it.)
Once again thank you so much for your awesome work!
18 replies
@DibyodyutiMondal
You still removed that "lazyness" by converting a Signal to a Promise. So whenever that method is called, execution will happen (because you have to return a Promise).
In the case of observables, you can return an observable that might be executed or not. That can be "piped", combined with other observables, and all of that will be executed only on subscription.
Please consider implementing some api service method where you cache a GET request (not the result of that request, but the request itself).
what @e-oz is describing is an eager evaluation model of computed used by SolidJS
Solid's analog to computed is createMemo and it's lazy as well - it doesn't immediately execute provided function.
It actually does execute eagerly
Ah, yep, you're right - I defined it outside of the reactive context and in this case function is not called.
@e-oz
Firstly, the signal in question was not a computed signal - it was a settable signal called 'ready'. But your point is pertinent - what if it indeed was a computed signal?
Second, I can't think of a simpler way to say this, forgive my English - but my guiding principle was: on a case-by-case basis, whenever we need to convert to a promise/observable, we should do the conversions as close to the point of actual usage of that promise/observable.
That would keep things simple, and keep things lazy.
Now, one can say, "by that logic, we should be promisifying inside the 'request' function, instead of class property ".
Yes, BUT, since it's a check that is going to be done/shared by all 'requests', it made sense to hoist it up as a class property and eagerly evaluate it once instead of every time on execution. The result is that, even if no requests were made, the service would be 'ready'.
- At that exact point of execution, lazyness was not required.
- The performance impact was negligible at best
So, the entire "removing the lazyness" was done on purpose because I'm consciously using the value. It was my, the developer's, choice, to remove laziness, and is not a problem with signals per se. In fact, I'm happy, I have the freedom to do things how I see fit.
All of this may seem long and convoluted the way I'm writing it, but in practice, it's easier, I promise.
@pkozlowski-opensource
My only question is regarding cleanup. How are signals cleaned up? Effects have a destroy method. But signals stick around until they are GCed? Is this the final form or is it something that you would look into later?
As for feedback, from my experience with signals, technically signals are good to go except for these blockers
- Native template integration
- Conversion to/from observables/promises
- Practical delivery on the promise of 'zoneless'
Apart from that, there is the time-lag in which the ecosystem may/may not adopt signals into their libraries. (Which is why the ability to convert is important).
As I said before, I've found it easier to reason about dependency-trees and change detection, as promised. GG.
As for this discussion, I think it's important because in the face of no documentation, it's important to share and discuss migration experiences. And in the future, we do need a great and detailed blog post on what the angular team's exact vision is in terms of usage pattern.
For example, I can say that signals can't really be integrated in a piece-meal fashion. This is because existing code uses observables from top-to-bottom. So if we remove even a small observable, it messes with observables further down the pipeline. And typescript keeps complaining until we've successfully refactored everything.
This is exacerbated more in the case where state management libraries are used (ngrx in my case).
So, I managed to remove zone.js as well as calls to detectChanges by using the the ngrxPush pipe instead of the standard async pipe.
I also created a method to transform signals to observables. With several cancellation mechanisms - because we must cancel on ngDestroy, right? My personal favorite is using the native 'AbortController' (because I think it's the simplest - and is guaranteed to not dangle in memory, unlike a signal, observable, or promise).
https://stackblitz.com/edit/angular-3xsgv1
Do check it out and tell me what you think. I think, with this, we can start using signals today, and if we don't use libraries that depend on zone.js (like angular material), we can even get rid of zone.js.
EDIT: I added a 'signal' pipe which transforms the signal into an observable so that we don't have to. I have provide both the templates - with and without the signalPipe. The observables can be commented out when using the signalsTemplate, leaving us with signals from start to finish (except in the helper functions, obviously)
6 replies
@pkozlowski-opensource
I think if this pattern gets ironed out a bit more, this could be a better/more powerful approach than #49153
But I still think there is a need for a third ChangeDetectionStrategy to be able to conveniently opt out of the current change detection and its side-effects.
It would also differentiate even more from solid js where you have no control over this. I think it also feels a lot more like angular where we already had the async/ngrxPush pipes (or even custom built pipes) with which we could control how we wanted to consume the observables.
I think this was/is something very powerful that could also be useful for signals.
And maybe also important: I think it would feel much more like angular!
The directive way looks so, so much simpler. I like it.
I am a bit sceptical about the performance, though. For example, what if there are nested elements with the signal directive.
In this example, we have a single settable signal 'loading' which then updates the 'loadingCustomers', 'loadingOrders and 'isLoading'. Therefore, if loading changes, all those 3 will change.
But imagine if loadingCustomers and loadingOrders were independent settable signals. Also, instead of a simple 'pre' element, imagine we are passing the signal's result, as an input into a heavy component. Since the [signal] directive is on a parent, will a change in loadingCustomers, cause ChangeDetection in the component which uses loadingOrders?
Another question is, since there are 3 signals, how many times will 'effect' run? Especially if we have async computed signals?
Also, cleanup is an issue. I am not sure, but I think it's a good idea to destroy the effect the directive creates in the directive's ngOnDestroy function. Or does the angular team have that base covered?
To get a better understanding of how effects and signals interact with each other, I wrote this:
Here is the stackblitz: https://stackblitz.com/edit/angular-oflm3n?file=src/main.ts
@Injectable() export class PageStore implements OnDestroy { readonly loading = signal(new Set<LoadingState>()); readonly loadingCustomers = computed(() => this.loading().has('customers')); readonly loadingOrders = computed(() => this.loading().has('orders')); readonly isLoading = computed( () => this.loadingCustomers() || this.loadingOrders() ); // a signal whose value will never change private readonly staticSignal = signal(false); // a signal that will be independent of the 'loading' series of signals readonly differentSignalValue = signal(false); readonly differentSignalArray = signal([false]); readonly effects = [ // does not use signals - runs only once effect(() => console.log('NOOP')), // this signal is not changed ever - runs only once effect(() => console.log('STATIC', this.staticSignal())), // this signal is changing, but values are not distict - runs only once effect(() => console.log('DIFF VALUE', this.differentSignalValue())), // this signal is changing, and values are distinct - runs on every emit effect(() => console.log('DIFF ARRAY', this.differentSignalArray())), // uses signal - runs on every emit effect(() => console.log('SOURCE', [...this.loading()])), // the following 2 effects are semi-related: // while orders does not change on customers, isLoading does // the interesting ticks to watch for are ticks 4 and 10: // we know that if customers change, then isLoading must recompute // but ORDER + ANY does not emit, because the recomputed values are equal to the old ones effect(() => console.log('CUSTOMERS', this.loadingCustomers())), effect(() => console.log('ORDER + ANY', this.loadingOrders(), this.isLoading()) ), ]; ngOnDestroy() { this.effects.forEach((eff) => eff.destroy()); } }
What I found interesting was that the effect you used in the signal directive does not 'use' any signals directly, yet if you stick a console.log before the detectChanges() call, you'll see that it's being executed on every change.
However, in my code, an effect which does not depend on a signal runs only once.
This raises a question that I have for the angular team: What are the conditions that must exist, for an effect to run?
Either there is a special rule that we're not aware of. Or, the constructor is being executed on every change, which means, that the directive is being torn down and created again on every change. Which presents a perf issue.
Or maybe this is a result of trying to mix the old change detection mechanism with a partially created new change detection mechanism (signals)
#FunTimes 😆
The signal directive would have to be ironed out quite a bit until it would be usable in production.
The "empty" effect runs on every change, because it triggers the change detection and it then triggers the execution of the template that calls all the signals in it. So it is almost the same as if the signals where being called in the effect.
Another question is, since there are 3 signals, how many times will 'effect' run? Especially if we have async computed signals?
The effect will only run once per change not per signal. The scheduling of signals in angular is more optimised for this szenario than in solid js. That is exactly why i would like to have more control over the scheduling.
This kind of problems/solutions lead directly to a better understanding of signals. That is also a reason why I would like it that way.
I am sure clean up will be fixed in the final version of signals in angular.
Thousands of reactions, over 650 comments, many questions, and several discussion topics! This is the level of interest that we didn’t anticipate, but are grateful for. Thank you for taking time to comment, share feedback, and engage in deep technical discussions. It is clear that the Angular community cares as much as we do.
We’ve read and re-read every single comment and responded directly to many. Looking at the discussion as the whole, there are some clear patterns worth addressing.
First of all we should acknowledge that this is a significant change and improvement to the Angular inner workings. This is not the change we are taking lightly: backward-compatibility is non-negotiable and we do everything to assure that the existing applications and libraries continue working as-is, without any modification. This is a gradual, opt-in change.
As any other substantial change, the idea of introducing reactive primitives and modifying change detection, generates lots of questions: both philosophical (“Where is Angular heading?”), architectural (“What about zone.js or RxJS?”), technical (“What is this API doing?”) and tactical (“When?”). More specifically, we’ve noted the following questions that generated most interest:
Backward compatibility and ecosystem evolution:
- Is this change going to be backward compatible? (hint: yes!)
- Is Angular getting less opinionated? (hint: not at all!)
Architecture
- Will it open doors to making zone.js optional? (hint: yes!)
- Will we modify change detection to take advantage of signals and provide more focused updates? (hint: yes!)
- Any plans for @input as signal (hint: yes!)
- Can I create signals outside of components / stores / services? (hint: yes!)
- What about state management with signals?
- Any plans for a dedicated state management based on signals?
- Should I use mutable or immutable data?
- Any guidelines when it comes to granularity of signals?
Role and place of RxJS
- Why use signals as a reactive primitive while RxJS is widely used already?
- Should I use signals and RxJS? What is the difference and role of those primitives?
- What about Angular APIs that currently expose RxJS as a primitive (ex. HTTPClient, EventEmitter, Forms, Router etc.)?
- RxJS interoperability story.
Signals library:
- Isn’t a signal just another
BehaviorSubject? - Why a new library instead of using an existing one (ex. MobX, SolidJS,...)?
- How is it different from MobX, SolidJS, Vue reactivity?
- Will you publish the library as a separate npm package?
- APIs and semantics
- Details of the inner-workings.
Project plans:
- Timelines.
- Rollout plan.
- Future / next steps.
We plan to address all those questions (and more!) in the RFC that we intend to publish in the coming weeks. We’ve spent hundreds and hundreds of hours discussing every aspect of this proposal and can’t wait to share design and technical details with you all.
Stay tuned!
4 replies
very nice summary of all discussion. Really looking forward the RFC to see what the answers will be.
Very nice job from the angular team. 🙏
This is not only the most discussed but also the most transparent change coming to angular and I’m sure people appreciate this approach. Great job!
