As the Angular docs mention that Effects are rarely needed in most applications, there is some confusion about how and when to use them. With this article, I try to shed some light on this topic in an objective and emotionless way.
The Main Use Case for Effects
Effects are primarily for rendering stuff you cannot render using data binding.
Here are some examples you can also find in the docs:
1) Logging
2) Painting on a Canvas
3) Custom DOM behavior
As data binding is the preferred way to display data to the user, it's obvious why the docs tell us effects are only sometimes needed.
A good example for 2) is Chau Tran's fantastic library Angular Three, bridging Angular and Three.js.
3) is about DOM behavior that cannot be expressed via the template syntax. An example is showing a SnackBar
with an imperative API, e.g., the one in Angular Material.
Keep Auto-Tracking in Mind
Please remember that Angular uses implicit tracking (auto-tracking) for computed
and effects
. In the following example, the effect will track the error
Signal even though it's not directly used in the effect function but inside the called logError
method.
effect(() => {
this.logError();
});
:
logError(): void {
const error = this.error();
if (error) {
console.error(error);
}
}
This also emphasizes that Angular's current Effects implementation is primarily intended for rendering. Signals touched during rendering are tracked. If you change them, rendering kicks in again. Angular's Alex Rickabaugh discussed this design decision in this GitHub Issue.
When NOT to use Effects?
The docs also explicitly mention to NOT use effects for propagating state. There are several possible issues, such as circular updates. I would add: The auto-tracking behavior can quickly lead to code that becomes hard to maintain. This is also discussed the mentioned GitHub issue.
Furthermore, Effects make your code more imperative and less declarative. The latter is in the sense of reactive programming. If you want to know how to make your code more declarative, my GDE fellow Mike F. Pearson, who is the author of StateAdapt, has you covered.
Another thing we need to keep in mind is that Signals are glitch-free: If you change a Signal several times in a row (within a stack frame), only the last change is seen. This fits for rendering but also shows that Signals are not suited for representing events.
Effects for Reactive Helpers
While not mentioned in the docs, Effects are also used behind the covers to build reactive helpers. An example is toObservable
in Angular, rxMethod
in the NGRX Signal Store, and the many helpers in Chau Tran's and Enea Jahollari's library ngxtension.
I will come back to this topic at the end of this short article.
Reacting on Signal Changes
So far, we have discussed when to use effects and when they are to be avoided. However, this leaves a question that usually arises quickly: How to react to changing Signals?
There are several answers:
1) Use computed
2) Use the event behind the Signal change
3) Use RxJS
4) Use a reactive helper
Using computed is the way to go when you can derive the needed value from existing Signals synchronously.
In the case of 2), you don't track the Signal change but react to the Event behind it:
Using the event behind the change mitigates issues mentioned above, such as issues with auto-tracking or circular updates.
With 3), I mean using RxJs instead or in addition to Signals. In the latter case, you convert between the two worlds with Angular's RxJS interop, providing functions like toObservable
and toSignal
. RxJS allows you to have a reactive flow end-to-end, e.g., from the user's interaction to the output. Please also keep in mind that RxJS' flattening operators, such as switchMap,
provide guarantees in terms of overlapping requests and hence help to avoid race conditions. My GDE fellow Jan-Niklas Wortmann discussed this topic during his Session about reactivity in Angular Applications at ng-conf 2024.
Examples of reactive helpers, as mentioned in 4), are rxMethod
in NGRX Signal Store or deriveAsync
in ngxtension. A method set up with rxMethod
can take a Signal and connect it to an RxJS-based pipe:
this.store.rxLoad(this.id);
Here is the simple Signal Store implementing rxLoad
:
export const DessertDetailStore = signalStore(
{ providedIn: 'root' },
withState({
dessert: initDessert,
loading: false
}),
withMethods((
store,
dessertService = inject(DessertService)
) => ({
rxLoad: rxMethod<number>(pipe(
tap(() => patchState(store, { loading: true })),
switchMap((id) => dessertService.findById(id)),
tap((dessert) => patchState(store, { dessert, loading: false })),
)),
}))
);
For the sake of simplicity, this example does not contain error handling.
More on this: Angular Architecture Workshop (online, interactive, advanced)
Become an expert for enterprise-scale and maintainable Angular applications with our Angular Architecture workshop!
All Details (English Workshop) | All Details (German Workshop)
For and Against explicitEffect
Another often discussed helper that is also discussed by Alex Rickabough in the mentioned GitHub Issue is explicitEffect
. The library ngxtension provides an implementation. Basically, it's a combination of effect
and untracked
to restrict tracking to several explicitly mentioned Signals:
explicitEffect(this.id, (id) => {
this.store.load(id);
});
In this case, only the id
Signal is tracked. An explicit Effect is a simple way to mitigate the drawbacks of auto-tracking when using effects for something beyond rendering as discussed above. However, it does not compensate for other already discussed issues that come with using effects for such tasks. For instance, you can end up with cyclic updates and code that is hard to reason about. Also, code like this is imperative and, hence, not in the sense of the reactive paradigm, where we try to have declarative code and a reactive chain bridging inputs and outputs.
If you strongly favor the reactive paradigm, you will avoid explicitEffect
. Instead, you will very likely go with RxJS or helpers like rxMethod
and the many declarative ones in ngxtension. If this is not the case for you, you and your teammates should be aware of the consequences that come with using this kind of effect.
Summary
Effects are primarily for rending stuff that cannot be rendered via data binding. Examples are logging, painting on a canvas, or custom DOM behavior, such as displaying a SnackBar
with an imperative API like the one in Angular Material. To react to a Signal Change, you should favor computed
if the needed value can be synchronously derived from existing Signals. Also, use the events behind Signal changes, RxJS, to establish a reactive end-to-end chain or reactive helpers like rxMethod
and deriveAsync
.