-
-
Notifications
You must be signed in to change notification settings - Fork 2k
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Introduce watch
function to @ngrx/signals
#4400
Comments
Hi @markostanimirovic These are some questions we had while we were working on https://ngxtension.netlify.app/utilities/signals/explicit-effect/ |
It is a good idea, but any function like this will have one “flaw”: it is too easy to get the same result without this function and without adding a new dependency and related maintenance costs. |
Good point! 👍 I updated the RFC description. Watching multiple signalsconst count1 = signal(10);
const count2 = signal(100);
watch(count1, count2, (val1, val2) => {
console.log(val1 + val2);
});
// or
watch(count1, count2, () => {
console.log(count1() + count2());
}); Cleanupconst count = signal(0);
watch(count, () => {
console.log('count changed', count());
return () => console.log('count on destroy', count());
}); |
I agree that it's easy to get the same result without the effect(() => {
// all deps in the beginning
const v1 = s1();
const v2 = s2();
const v3 = s3();
untracked(() => {
// effect implementation
console.log(v1 + v2 + v3);
// ...
});
}); But the question is do we want our code to look like this. The main goal of the |
While I get why this is great / necessary, it's just triggers some flashbacks back to 2014 and |
Lol 😅 Ideas are recycled over time. I guess that's how we got to the signals again, 10+ years since they were used in KnockoutJS. |
This is funny. React devs don't like the dependency array of useEffect. Ryan Carniato painstakingly invents a pattern of auto-tracking. Angular copies Solid signal pattern. Angular community reinvents dependency arrays. |
|
What's wrong about having both options - explicit and implicit dependency tracking? VueJS provides both options via |
It's easy to say that supporting more options is always good. But the most valuable thing we can do for devs in the community is teach them to not write spaghetti. In 90% of situations where devs would use this, I bet a better solution would have been to simplify the data flow instead. So I don't have a fundamental problem with supporting this as well, but I don't see very much content teaching people how to not screw their code up. I see a lot of tools springing up to support really weird scenarios. Sort of like when people ask to dispatch multiple actions at the same time in ngrx store. Like, the people want it, but they shouldn't want it. They should learn how to use ngrx correctly instead. Also, if using a signal value asynchronously surprises a developer when it isn't tracked, then how would they know to use the explicit dependency array? It seems like it's already too late at that point. |
I don't think that the To be clear, I don't think that implicit dependency tracking is bad. It's a preferable choice in some cases and because of that, having both options will be beneficial in my opinion.
I don't see how treating actions as commands vs unique events is related to explicit vs implicit dependency tracking. If you're referring to explicit dependency tracking as a bad practice (such as treating actions as commands in the case of NgRx Store), I respectfully disagree. 🙂
To realize how to use the explicit dependency array, it's enough to take a look at the |
To be clear, Angular adopted the signal pattern but didn't copy Solid's signal implementation and I only say this because the difference is relevant here. Solid's dependency graph is a directed acyclic graph. It's unidirectional. Angular's signal graph is bidirectional in that producers track "live" consumers while all consumers track all producers. Only live consumers are notified by producers to recompute. There are only two live contexts today: the LView and an effect. An effect is a live consumer of any signal referenced inside of it that is not wrapped with untrack. This is a "react to every signal with the ability to opt out for individual ones". As pointed out in the proposal, this can cause unintended behavior. The watch function is the inverse that says, "don't react to anything unless I explicitly tell you to." I think it would be great to have both. |
Of course the implementation is different, but the auto-tracking syntax was intentionally copied. Solid effects also auto track dependencies. I really don't see how the implementation details are relevant.
What if I told you that Angular templates can cause unintended behavior if you have no idea what you're doing? The answer is education and documentation, not making an API for every random pattern devs feel like doing - especially when the same knowledge required to reach for these extra APIs is the very same knowledge that would help avoid the problem to begin with.
As a rule, explicit dependencies are extra boilerplate that reduce performance in the vast majority of applications. It would be extremely annoying for them to become the go-to syntax, and if they aren't, then the knowledge of when to reach for them should rather be remedied by not sucking at Angular if possible. Honestly, effects should be rare in application code anyway. |
I totally understand both sides here. I agree with @mfp22 when he says that we need to teach people to use signals the right way, which includes avoiding unnecessary effects. I also think that in many cases, effects can indeed be avoided, e.g., by using compute or the underlying event that caused the Signal change we are interested in. On the other hand, I like @markostanimirovic's idea because there are some cases where avoiding effects does not feel right or like a detour. More and more Angular APIs will be signal-based, and there are situations where we want to react directly to a Signal change. Let's think about the router or forms. If a signal is bound to the router or a form, I want to use it directly instead of setting up another handler for the underlying event. Of course, using effects in these cases has consequences, and we need to inform people about them so that they can make informed decisions. So, to cut a long story short: I think we need both. |
To play a devils advocate, aren't 2 APIs ( Especially from the POV that now there are additional questions and decisions to be made, which seem to complicate things and add confusion for beginners even further, eg... why there are two ways to achieve the same outcome? In my experience, the desirable goal which leads to best outcomes is usually the one which minimizes the amount of concepts and approaches so that once that minimal API is fully understood, it allows us to express everything that we need in one standard way. That being said and to address the "do we want our code to look like that" point which is valid, what if such approach could be named in a way which refers back to and strengthens understanding of already existing concepts?
|
So:
do you suggest not introducing an API that provides explicit dependency tracking?
or just renaming If you're suggesting renaming, having the |
I am trying to illuminate a perceived trade off of introducing 3rd API, basically a prepackaged version of original two. And if that tradeoff is resolved to be worth it, then I would try to figure out if there could be a naming for a 3rd API which would express that it is in fact pre-packaged version of existing two, as mentioned "or similar". i've seen explicitEffect preciously, personally an |
I'd prefer to see this as part of Angular Core and not in libraries. An overwhelming majority of people I've talked and listened to said that If it is not going to happen in the core, then I think ngrx/signals and others (like ngxtension) are a perfect place.
Overloading, as mentioned #4400 (comment), would be wonderful but I don't think it is possible. |
To me this is a huge red flag. What are they doing??? I also think ngrx is a great place for stuff like this if the need arises. I'm just used to React where useEffect has a strict lint rule to not ignore dependencies, and it's just like TypeScript where at first you think "ugh, just let me do what I want!" and then you think about it and realize it's saving you from something potentially very weird and there's a situation where it would be catastrophic. Lots of devs stay in the frustration phase because it takes work to imagine situations where you could be wrong and then improve your code. So I guess I'd be interesting in seeing a single, common example of when you might want to use a value without being interested when it changes. Overall I still think the only way this can save confusion is by becoming the new default, which to me would be such a waste of an ingenious API developed by the Angular team. Overall it would make the average app much less performant and would add boilerplate where it wouldn't have been necessary in 90% of places. |
@mfp22 An example could be that your component consumes an id property from the URL and needs to trigger a request for that particular entity. You use Very often, the loading is done by a service. From the component's perspective, you don't want to know about the implementation of that service and what signals it might call. That's why you put the service call into an How would you do that without |
Can we just skip to the end of all this? The React community already went through this and decided that effects should rarely be in regular application code. There are too many things to get wrong with them, and I don't think there's any way to make them easier. This proposal only solves one issue, and I don't think it solves it except by backtracking on a tradeoff the Angular team chose to make because 90% of code benefits from it. What happens if the id changes rapidly? This is basically the canonical example of why you should be using RxJS or TanStack query. That's what we should be encouraging. Not super custom, imperative effect spaghetti code. I get the essence of the example, it's like a |
Yeah, let's get right to the bottom line: I would cover as much as I can with There was much talk of Angular becoming easier. However, if developers need to use RxJs for even the simplest applications, I don't see how that should happen. Please imagine yourself as a seasoned backend developer who sees Angular for the first time and "just wants to send an HTTP request." Wouldn't that be a little bit too much? |
Given the same number of lines of code, the more uses of
RxJS is never "necessary". I hope this isn't the direction of ng RX. If so, God help us, because NgRx sets the trends. I am still open to an example of untracked dependencies that wouldn't be obviously better handled with RxJS or an abstraction written over |
I don't speak for NgRx. I am only here for the good discussions.
If I deal with an event source that emits multiple times and need to manage them, then RxJs.
If I depend on Signal change, the Promise would be in an It is the simplification that makes it better than TanStack or RxJs. And that's why the |
Let's please focus on the main topic of this RFC. The intention is not to advocate the handling of asynchronous side effects via the This RFC proposes having another way of dependency tracking with Angular Signals - the explicit one. People can use it where it makes sense and avoid issues that can be caused by implicit dependency tracking in a more ergonomic way - without using We cannot prevent people from using Again, what's wrong with having both options?
const count = signal(0);
effect(() => console.log(count());
// vs
watch(count, console.log); |
The reason we were talking about that irrelevant example is because these auto-tracking gotchas should be irrelevant to most developers most of the time, and I was asking for a common example proving otherwise. And I saw an example that 99% of developers would implement in a buggy way and shouldn't even attempt normally. Like I said, the React community went through this 4 years ago. Nobody active in the community that I know of is recommending fetching data in a
Why wouldn't you care about timing? I see too many list-detail views as a user where I can click on a few in a row and end up seeing something that wasn't the last thing I clicked. Are you going to manually cancel old promises? Or let the user deal with this confusion when it happens? Writing code that requires its dependencies to behave in certain ways is fragile and should be avoided if possible. Also:
I wouldn't wire up the data flow this way. The service should take in the parameters it needs. Pure functions are extremely good. I would need to know more details of the situation I guess. Maybe we can continue the discussion on x or something. Sorry, I don't actually like stomping on other people's hard work or ideas. I hate writing these comments. And I have my own state management library where I can make things however I want. But I also have to use NgRx. Almost every Angular dev does at some point. And when I saw this, I didn't think it would be a problem itself, but rather a sign that the way people are thinking about signals is making things harder than they need to be. |
I already answered in this comment: "To realize how to use the explicit dependency array, it's enough to take a look at the
That's the example I was looking for 🙂 and the reason why I think having both options is beneficial. In cases like this, implicit tracking is preferable of course.
Again, I don't think that |
But... When are they going to go looking for |
@markostanimirovic I quite like the idea of introducing explicit dependency tracking to signals! I have been using the signals API in production projects, especially in conjuction with presentational components and, as you mentioned correctly, it is not always clear how the dependency graph might look like. However, I am not sure if NgRx is the right place to add such functionality. Shouldn't this be an Issue in the official Angular repository? Maybe use overloads on the import { signal, effect } from '@angular/core';
const count = signal(0);
// tracked effect
effect(() => console.log('count value', count()));
// untracked effect
effect(count, (v) => console.log('count value', v)); Even if developers use this API in a wrong way, it should be quite simple to recognize the error: import { signal, effect } from '@angular/core';
const count = signal(0);
// wrong usage of an untracked effect
effect(count, (v) => console.log('count value', count()));
// Error: implicit signal tracking is not available in explicit effects Or maybe even allow combinations, like: import { signal, effect } from '@angular/core';
const count = signal(0);
const squared = computed(() => count() * count());
effect(count, (v) => {
console.log('count value', v, 'squared value', squared());
}); |
@danielkleebinder Here's the "open petition" to get it into the official repo: angular/angular#56155 I'm sure Marko will not object ;) |
@rainerhahnekamp Oh, that looks great! Thank you! |
(I just posted an in-depth reply to angular/angular#56155) My personal takes:
|
Thanks for the feedback, Alex.
|
The good thing about the option @alxhub is proposing is that we don't need to introduce a new term ( |
I'll convert this issue to a discussion. Let's see what the Angular team will decide on this topic and revisit this RFC later. |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
Which @ngrx/* package(s) are relevant/related to the feature request?
signals
Information
To perform side effects on signal changes, Angular provides the
effect
function:DX issues with
effect
1) Registering dependencies asynchronously
Angular will implicitly track signals whose value is read within the effect callback to re-execute the effect on dependent signal changes. However, if the signal value is read asynchronously:
the auto-tracking mechanism will not register the
count
signal as a dependency, so the effect won't be re-executed on signal changes anymore. As a workaround, it's necessary to read signal value synchronously (within the reactive context):2) Reading signal value without registering it as a dependency
If we want to read a signal value in the effect without registering it as a dependency, it's necessary to use the
untracked
function:3) Unintentional dependencies
Auto-tracking mechanism is powerful and it works great in simple examples. However, in more complex scenarios, it can cause unpredictable behaviors and bugs that are hard to find. For example, if a method/function that synchronously reads signal values is called within the
effect
callback, all signals will be registered as dependencies:This effect will register all signals that are read within the
someSideEffect
function as dependencies.Explicit dependency tracking with the
watch
functionThe
watch
function has the same purpose aseffect
. However, unlikeeffect
, it provides the ability to explicitly specify dependent signals:The
watch
callback is not executed within the reactive context. Therefore, there is no need to useuntracked
:With the
watch
function, the issue with unintentional dependencies is not the case anymore.Watching multiple signals
To watch multiple signals, a sequence of dependent signals is provided to the
watch
function:Cleanup
Describe any alternatives/workarounds you're currently using
No response
I would be willing to submit a PR to fix this issue
The text was updated successfully, but these errors were encountered: