Skip to content
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

Open
1 of 2 tasks
markostanimirovic opened this issue Jun 15, 2024 · 34 comments
Open
1 of 2 tasks

RFC: Introduce watch function to @ngrx/signals #4400

markostanimirovic opened this issue Jun 15, 2024 · 34 comments

Comments

@markostanimirovic
Copy link
Member

markostanimirovic commented Jun 15, 2024

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:

import { signal, effect } from '@angular/core';

const count = signal(0);

effect(() => {
  console.log('count value', count());
});

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:

effect(() => {
  setTimeout(() => {
    console.log('count value after 1s', count());
  }, 1_000);
});

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

effect(() => {
  const value = count();

  setTimeout(() => {
    console.log('count value after 1s', value);
  }, 1_000);
});

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:

import { signal, effect, untracked } from '@angular/core';

const count = signal(0);
const foo = signal('bar');

effect(() => {
  console.log('count changed');
  console.log('count value', count());
  console.log('foo value', untracked(() => foo()));
});

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:

import { signal, effect } from '@angular/core';

const foo = signal('foo');
const bar = signal('bar');

function someSideEffect(): void {
  console.log(foo());

  // ...

  console.log(bar());
}

const count = signal(0);

effect(() => {
  localStorage.setItem('count', count());
  someSideEffect();
});

This effect will register all signals that are read within the someSideEffect function as dependencies.

Explicit dependency tracking with the watch function

The watch function has the same purpose as effect. However, unlike effect, it provides the ability to explicitly specify dependent signals:

import { signal } from '@angular/core';
import { watch } from '@ngrx/signals';

const count = signal(0);

watch(count, (value) => {
  console.log('count value', value);
});

The watch callback is not executed within the reactive context. Therefore, there is no need to use untracked:

import { signal } from '@angular/core';
import { watch } from '@ngrx/signals';

const count = signal(0);
const foo = signal('bar');

watch(count, () => {
  console.log('count changed');
  console.log('count value', count());
  console.log('foo value', foo());
});

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:

const count1 = signal(10);
const count2 = signal(100);

watch(count1, count2, (val1, val2) => {
  console.log(val1 + val2);
});

// or

watch(count1, count2, () => {
  console.log(count1() + count2());
});

Cleanup

const count = signal(0);

watch(count, () => {
  console.log('count changed', count());

  return () => console.log('count on destroy', count());
});

Describe any alternatives/workarounds you're currently using

No response

I would be willing to submit a PR to fix this issue

  • Yes
  • No
@eneajaho
Copy link
Contributor

Hi @markostanimirovic
What if we have more than 1 signal that we want to listen? What about the possibility to cleanup?

These are some questions we had while we were working on https://ngxtension.netlify.app/utilities/signals/explicit-effect/

@e-oz
Copy link
Contributor

e-oz commented Jun 16, 2024

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.

@markostanimirovic
Copy link
Member Author

Hi @markostanimirovic What if we have more than 1 signal that we want to listen? What about the possibility to cleanup?

These are some questions we had while we were working on https://ngxtension.netlify.app/utilities/signals/explicit-effect/

Good point! 👍 I updated the RFC description.

Watching multiple signals

const count1 = signal(10);
const count2 = signal(100);

watch(count1, count2, (val1, val2) => {
  console.log(val1 + val2);
});

// or

watch(count1, count2, () => {
  console.log(count1() + count2());
});

Cleanup

const count = signal(0);

watch(count, () => {
  console.log('count changed', count());

  return () => console.log('count on destroy', count());
});

@markostanimirovic
Copy link
Member Author

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.

I agree that it's easy to get the same result without the watch function:

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 @ngrx/signals package is to provide better DX with Angular Signals. In addition to SignalStore, there are and will be utilities that we can use independently. For example, people can install @ngrx/signals to use rxMethod or signalState only.

@tomastrajan
Copy link

tomastrajan commented Jun 16, 2024

While I get why this is great / necessary, it's just triggers some flashbacks back to 2014 and $watch , are we coming full circle 10 years later !? 😅

@markostanimirovic
Copy link
Member Author

While I get why this is great / necessary, it's just triggers some flashbacks back to 2014 and $watch , are we coming full circle 10 years later !? 😅

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.

@mfp22
Copy link

mfp22 commented Jun 16, 2024

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.

@mfp22
Copy link

mfp22 commented Jun 16, 2024

  1. I could see some devs getting tripped up by this
  2. This is generally a sign of really weird data flow
  3. Am I the only one who even tries to write pure functions? I would never try this example. It's so hard to follow.

@markostanimirovic
Copy link
Member Author

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 watch and watchEffect functions: https://vuejs.org/guide/essentials/watchers

@mfp22
Copy link

mfp22 commented Jun 16, 2024

What's wrong about having both options - explicit and implicit dependency tracking?

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.

@markostanimirovic
Copy link
Member Author

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.

I don't think that the watch function can enforce writing spaghetti code and the effect not. On the contrary, a side effect implemented with the watch function seems more predictable and readable to me - when I take a look at the definition, I immediately know when a side effect is re-executed - I don't have to search for signal reads within the effect callback.

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.

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.

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

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.

To realize how to use the explicit dependency array, it's enough to take a look at the watch function signature which is not the case with effect for the asynchronous signal reads. Isn't it better to have self-descriptive APIs?

@KylerJohnsonDev
Copy link
Contributor

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.

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.

@mfp22
Copy link

mfp22 commented Jun 17, 2024

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.

can cause unintended behavior

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.

  1. devs should know how Angular works, including effect
  2. reading signals without depending on them is 99% of the time a bad idea, especially for regular application developers
  3. implicit dependencies should never occur if devs are taught to write pure functions wherever possible, which they should be

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.

@manfredsteyer
Copy link

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.

@tomastrajan
Copy link

To play a devils advocate, aren't 2 APIs (effect and untracked ) easier to discover, learn about, understand and master compared to 3 APIs ( + watch)

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?
do I want to go with implicit vs explicit deps tracking?
when should I use which type of deps tracking?
should I decide for one style for a whole project?

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?

// possible today
effect(() => {
  // all deps in the beginning
  const v1 = s1();
  const v2 = s2();
  const v3 = s3();

  untracked(() => {
    // effect implementation
    console.log(v1 + v2 + v3);
    // ...
  });
});


// proposed

watch(deps, impl)

// vs (refer back to original naming and concepts)

untrackedEffect(deps, impl)  // or similar

@markostanimirovic
Copy link
Member Author

So:

aren't 2 APIs (effect and untracked ) easier to discover, learn about, understand and master compared to 3 APIs ( + watch)

do you suggest not introducing an API that provides explicit dependency tracking?

// proposed
watch(deps, impl)
// vs (refer back to original naming and concepts)
untrackedEffect(deps, impl) // or similar

or just renaming watch to untrackedEffect? 🙂

If you're suggesting renaming, having the untracked word in the name of the API that does tracking can be very confusing IMO.

@tomastrajan
Copy link

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 effect "overload" with deps and untrackedImpl would be nice but probably would lead to collisions if consumer would want to use both Angular and NgRx one in the same file.

@rainerhahnekamp
Copy link
Contributor

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 untracked is something they always use. With creating an issue angular/angular#56155, I did what was possible.

If it is not going to happen in the core, then I think ngrx/signals and others (like ngxtension) are a perfect place.

untrackedEffect sounds confusing because there some signals are tracked. So I'd prefer watch or explicitEffect.

Overloading, as mentioned #4400 (comment), would be wonderful but I don't think it is possible.

@mfp22
Copy link

mfp22 commented Jun 17, 2024

An overwhelming majority of people I've talked and listened to said that untracked is something they always use.

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.

@rainerhahnekamp
Copy link
Contributor

@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 input() for property binding.

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

How would you do that without effect? Especially, if you want to secure that your components can also handle changes of that id property.

@mfp22
Copy link

mfp22 commented Jun 17, 2024

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 withLatestFrom. But I don't consider it an example that should live in application code.

@rainerhahnekamp
Copy link
Contributor

Yeah, let's get right to the bottom line: I would cover as much as I can with effect and use RxJs only where necessary. rxMethod is a perfect example.

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?

@mfp22
Copy link

mfp22 commented Jun 17, 2024

Given the same number of lines of code, the more uses of effect a codebase has, the more bugs it will have. I guarantee it. You didn't answer this:

What happens if the id changes rapidly?

use RxJs only where necessary. rxMethod is a perfect example.

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 effect like TanStack Query.

@rainerhahnekamp
Copy link
Contributor

rainerhahnekamp commented Jun 17, 2024

I don't speak for NgRx. I am only here for the good discussions.

What happens if the id changes rapidly?

If I deal with an event source that emits multiple times and need to manage them, then RxJs.

  • For a single HTTP request, I use a Promise.
  • If the id's value changes rapidly, but I don't care about the timing; I would also pick a Promise.

If I depend on Signal change, the Promise would be in an effect. Compared to an additional library and boilerplate like TanStack or RxJs, I would have a native JavaScript element and one Angular API function.

It is the simplification that makes it better than TanStack or RxJs. And that's why the effect needs an implicit untracked version like watch.

@markostanimirovic
Copy link
Member Author

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 effect and/or watch functions. I really hope this is clear to everyone included in this conversation.

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 untracked and having nested callbacks.

We cannot prevent people from using effect or watch for the things they are not intended for. We cannot even prevent them from making a mess with RxJS. Any tool or library can be used in the wrong way.

Again, what's wrong with having both options?

  • VueJS provides watch for explicit and watchEffect for implicit dependency tracking.
  • SolidJS provides createEffect for implicit and on for explicit dependency tracking.

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.

  1. No one said that watch should be the default.
  2. Please share the project that proves that using watch is "much less performant" than effect. Show us the code.
  3. Regarding boilerplate - explicitness is sometimes more beneficial than less code. Less code does not always mean better code from my experience. Also, using the watch function does not necessarily mean more code compared to effect:
const count = signal(0);

effect(() => console.log(count());
// vs
watch(count, console.log);

@mfp22
Copy link

mfp22 commented Jun 17, 2024

  1. This is the point I've tried to make 3 times already. In what situation would a person have the first issue you mentioned in this RFC? Only if they didn't already understand how dependency tracking worked. But if they don't understand, then... Why would they know about watch? Only if they were already using it. That means that either it's the default thing they use, or it's not going to prevent this issue.
  2. effect(() => flag() && doStuff(expensiveComputation()))
  3. I said it would be pointless 90% of the time.

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 useEffect directly anymore. They all recommend using an abstractions over it.

If the id's value changes rapidly, but I don't care about the timing; I would also pick a Promise.

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:

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.

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.

@markostanimirovic
Copy link
Member Author

  1. This is the point I've tried to make 3 times already. In what situation would a person have the first issue you mentioned in this RFC? Only if they didn't already understand how dependency tracking worked. But if they don't understand, then... Why would they know about watch? Only if they were already using it. That means that either it's the default thing they use, or it's not going to prevent this issue.

I already answered in this comment:

"To realize how to use the explicit dependency array, it's enough to take a look at the watch function signature which is not the case with effect for the asynchronous signal reads. Isn't it better to have self-descriptive APIs?"

  1. effect(() => flag() && doStuff(expensiveComputation()))

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.

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 useEffect directly anymore. They all recommend using an abstractions over it.

Again, I don't think that effect/watch should be used to handle API calls.

@mfp22
Copy link

mfp22 commented Jun 17, 2024

To realize how to use the explicit dependency array, it's enough to take a look at the watch function signature which is not the case with effect for the asynchronous signal reads. Isn't it better to have self-descriptive APIs?

But... When are they going to go looking for watch to even see the function signature in the first place? If they're just going about their normal development workflow, they'll just start writing effect and then learn the hard way how dependency tracking works. Right?

@danielkleebinder
Copy link

@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 effect function itself to combine APIs like:

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());
});

@rainerhahnekamp
Copy link
Contributor

@danielkleebinder Here's the "open petition" to get it into the official repo: angular/angular#56155

I'm sure Marko will not object ;)

@danielkleebinder
Copy link

@rainerhahnekamp Oh, that looks great! Thank you!

@alxhub
Copy link

alxhub commented Jun 18, 2024

(I just posted an in-depth reply to angular/angular#56155)

My personal takes:

  1. @ngrx/signals shouldn't be in the business of providing a standalone effect()-style API. If there are ways to create effects that are integrated with things like SignalStore, then I could see using this pattern.

  2. I favor the compositional style of Solid's on API over a separate effect / watch function.

@markostanimirovic
Copy link
Member Author

Thanks for the feedback, Alex.

  1. I think we all agree that @angular/core is a better place for it. The idea of ​​introducing watch in @ngrx/signals is in case the Angular team does not plan to provide an API for explicit dependency tracking in the foreseeable future.
  2. I'm a little more in favor of watch or another effect overload, but both options are fine. 👍

@manfredsteyer
Copy link

The good thing about the option @alxhub is proposing is that we don't need to introduce a new term (watch) but can stick with the term effect. It almost looks like an overload of effect, and it makes the caller's intention clear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants