Perhaps the most widely used state management approach in Angular is a âservice with a subjectâ. In almost any debate about state management libraries you will almost always see comments like âjust use a service with a subjectâ. It might not actually always be the best approach, but there is a reason this approach is so popular and itâs because it is simple and it works.
This article is adapted from the following video:
Understanding âService with a Subjectâ
If you need to share some state among multiple components, you can create a service and add a subject to that service to hold the state.
You will generally have some method in the service to update that subject with new data, and then any component that wants to use the data in that subject can do so either by directly accessing the subject, or to make things a bit more safe we can expose the subject as an observable instead. This means code from outside of this service can access the values, but it has no power to modify those values directly by having access to that subject.
But now signals are coming in like a wrecking ball and challenging all of our beloved patterns.
I am almost certain this pattern will remain popular, and the signal-ified version of it is just going to make things a little bit easier in some ways⊠and perhaps harder in others.
Enter the âService with a Signalâ
Iâve taken the service with a subject idea and created the equivalent set up with signals. These end up being quite similar.
Perhaps an interesting aspect to this signals version is that rather than just having other components access the settable signal directly, we provide a computed
version of that signal which will give components the ability to read the signal, but not modify it. This is the same idea as returning an observable of a BehaviorSubject
rather than the BehaviorSubject
directly.
So, what benefit do signals offer here? Mostly the same benefits they usually offer - we donât have to deal with observables and subscriptions here, we can just access signal values directly, and we donât need to understand the more complex aspects and gotchas of RxJS. I talked about the benefits of signals over a subject in one of my previous videos.
A Fair Comparison: Handling Asynchronous Data
This is clearly a very simple example. It wouldnât be fair to just say signals are better for services.
So, letâs take a look at a situation involving async that I often run into. Right now, this service allows us to start from a blank state and add or remove data. But, often I want to sync this data either to local storage or perhaps some external API. This means that I want to initially seed this service with data from the API, and every time a change is made I want to replicate that change back to the API to make sure it is saved.
Now, I could just manually subscribe to a request to load the data from the API and next
the subject in this service to initialise the data. This leads to a simpler mental model, but doesnât really make use of RxJSâs async power.
Instead, I can bust out some RxJS operators and do something like this instead.
Now rather than having a subject that contains all of our values, we instead have a subject that just emits when we want to add or remove a value - in this case names of employees.
This employees$
observable will listen to all of the emissions from this subject, and collect them into an array using the scan
operator.
But, to get our initial data we first call the .load
method from our API service, and then we pass that data when we are switching to our subject that emits to add or remove a value, and use it as our initial value for the scan. That means our scan will initially be an array of the initial set of data, and then every time our subject emits we will either add or remove names from that array.
To save the data back to the API every time it changes in the app, Iâve added this tap
operator which allows me to run a side effect whenever this stream emits.
This is probably a reasonably fair example of RxJS - there are some non obvious concepts here that arenât easy to learn, but this also provides a lot of power and flexibility and can be completely reactive and declarative.
Check out this video if you want more info about what that means exactly.
Now, here is what the signals version might look like:
We have this init effect that triggers loadEmployees
. The initialEmployees
just uses async await
to load the data from the service and then we update
the employees signal. To save the data every time it changes, we have this save
effect.
This signals example looks quite a bit simpler, and it is in a sense, but there is also some awkwardness here. This code is much more imperative. We need to do things like setting this hasLoaded
flag to make sure we donât save our data back to the API before any data has loaded, and to actually understand what values employees will hold and how it behaves we need to search around in multiple places in this service.
With a declarative approach, like with RxJS, we only need to look in one place to understand what something is.
Blending Signals and RxJS
An important thing to remember is that we can use signals and RxJS together, so if you wanted to go with the RxJS approach you can just wrap your employees$
observable with toSignal
or whatever we end up getting in the API to convert back to a signal. You get all of the RxJS goodness and power, but you still have a signal in end that you can use to access the value directly without any subscriptions.