How do we manage application state in Angular? What are some of the good approaches to keep data in sync between different components and services that use the data?
The nature of JavaScript as a language doesn’t lend itself well to patterns from languages such as C# and Java. And despite Angular being the framework that seems to be an easy transition for developers who are already familiar with those languages (because of its use of TypeScript), the patterns and approaches from C# and Java and their associated frameworks can become baggage to efficiently building Angular applications.
The problem we are trying to solve in this post is sharing data between 2 unconnected components (i.e. no parent-child relationship) or services, such as data shared between 2 page components. React and its ecosystem have “communally” solved this problem by storing that data within a Redux store from which the listening components can received the updated data. And while one can import redux-like libraries into Angular, the framework comes included with the very thing to make this thing work: observables.
Imagine a scenario: Somewhere in the completely hypothetical and fictitious world, there is a succession battle of some monarchy, where there is a dispute regarding the heir to the throne. A developer out there has been tasked with creating a web-based application, that will be housed on a terminal within a heavily guarded room where the royal counsel will be voting on the successor. The developer decides to create a 2 page angular application: one page on which they can vote, and another which has the tally of the score. The glue between these 2 components will be a service that will hold the state that the components can retrieve the data from:
export class VotingService {
private voteList = [];
private votes$ = new BehaviorSubject<{ candidate: string }[]>([]);
get votes(): Observable<{ candidate: string }[]> {
return this.votes$;
}
castVote(vote) {
this.voteList.push(vote)
this.votes$.next(this.voteList)
}
}
The service exposes an observable to which interested parties can subscribe for changes. The reason why the type Observable
is given instead of exposing the BehaviorSubject
is to ensure that this is the only place from which a new event can be triggered for all the listeners. The service also exposes a method castVote
which takes in a new vote, and alerts all listeners. (Note: the private voteList
is not ideal, as it would be great if all state is managed within the observable. There are better ways of doing this, but are beyond the scope of this blog post.)
From the component tallying up the score, it can just subscribe to this observable, and be notified whenever there is a change in the votes.
@Component({
template: '<div *ngFor="let vote of votingService.votes | async"></div>'
})
export class VoteTallyComponent {
constructor(public votingService: VotingService) {
}
}
The async
pipe that Angular provides will automatically handle subscriptions, unsubscribes, etc. And whenever a new value is emitted, the UI will be updated to reflect this new value.
For those familiar with React & Redux, the service looks similar to a redux store, and the castVote
method looks similar to a reducer. And that’s because it is. The general pattern they are all following is the Observer pattern which has found similar implementations within different frameworks. The great thing about this specific approach within Angular is that one can abstract multiple “stores” of data, abstracted according to the business domain. And from there, compose multiple smaller abstracted “stores” into more complex services that handle business logic.
Following from the above example, imagine if there is a link between attendance at the royal council and the number of votes. We want to notify components if there are more votes than attendees. First of all, there can be an attendee service:
export class AttendeeService {
private attendeeList = [];
private attendees$ = new BehaviorSubject<{ name: string, clan: string }[]>([]);
get attendees(): Observable<{ name: string, clan: string }[]> {
return this.attendees$;
}
register(details) {
this.attendeeList.push(details)
this.attendees$.next(this.attendeeList)
}
}
Now to combine the two, we can create a new VoteMonitoringService
to let listeners know if the votes are valid or not:
export class VoteMonitoringService {
constructor(
private votingService: VotingService,
private attendeeService: AttendeeService) {}
get areVotesValid(): Observable<boolean> {
return combineLatest([
this.votingService.votes,
this.attendeeService.attendees
]).pipe(map(([votes, attendees]) => votes.length <= attendees.length))
}
}
In the above example, whenever a new value is emitted from the votes or the attendees, the areVotesValid
will emit a new value based on the latest values of votes or attendees. In this way, larger pieces of functionality can then be built up from smaller bits, building complexity in a maintainable way.
I was wrong 3 years ago when I wrote the previous blog post. And I may possibly change my mind about this upon learning better ways to do things. But this approach has served me well in the recent past.