Simple Observable-based Store in Angular
Since Flux and Redux gained popularity, the approach to front-end architecture has changed drastically. The idea of a store with global application state, unidirectional data flow, immutability and changes using actions, largely increased the popularity of Redux-based frameworks.
According to the results presented in annual State of JS survey, React and Vue, highly driven by reactivity and streams of data, are two of the most beloved frameworks. Even though on slow decline, Angular stays strong as one of the most popular front-end solutions. Not to fall behind, third party developers have created multiple redux-based tools to handle reactivity in Angular web applications.
In general, the simplest state management consists of:
- Store – a place to contain the data;
- Reduers – to specify how the application’s state changes in response to actions;
- Selectors – used to obtain data from the store as Observables;
- Actions – launched to modify the store.
There’s plethora of tools to handle such behavior. While most of them are third-party and need to be installed in our project, Angular enables the developer to leverage existing capabilities to create a state management system without any plugins.
State management in Angular – solutions
Browsing through the state management libraries, an interesting relationship emerges.
Existing tools can be sorted by difficulty:
- NgRx – the most advanced and robust, familiar to Redux store users, lots of boilerplate code and steep learning curve for beginners;
- NGXS – simplified version of NgRx, mostly due to high usage of TypeScript features, contains some additional features like Cancellation or Angular Error Handlers as compared to NgRX;
- Akita – not as highly advanced as the tools above, but quite easy to pick up and with little boiler plate code, used by many for larger projects, features lots of Angular plugins;
- Simple RxJS store – smallest in size (built-in), based on BehaviorSubject for state and pipe operators for modifications, easy too set up, limited in multiple aspects.
RxJS store pros and cons
All of the proposed solutions helps an Angular developer to get rid of excessive usage of input/emitter way of component interaction, often ridden with too much complexity when multiple components are communicating with each other.
Focusing on the RxJS store itself, the good parts are:
- Easy to kickstart – a beginner can start with own store after reading just one article (like this!);
- Simplicity – as much as one service might prove enough for some use cases;
- Scalable to some extent – if approached with proper attention, such store can be scaled even for medium-sized projects;
- Small size – there’s no extra dependencies due too RxJS being a part of Angular stack;
What’s not that good?
- No debugging tools – all kind of goodness coming from Redux DevTools is to be missed in this case
- No selectors – reaching out for store data requires the usage of transform functions
- No persistence – there are no plugins to keep store data in Web Storage
- Other features like WebSocket integration, lazy loading or web workers are missing
RxJS store implementation – multiple user creation
You can check out this example on StackBlitz.
The repository can be found on GitHub.
The Problem: one of the use-cases for a simple store I’ve ran into recently was multiple user creation. Single user creation dialog consisted mostly of a form with data and save button.
An extension would need to have the abilities to:
- Add new users to list;
- Remove any of the users;
- Define each user data;
- Switch between users;
- Save all defined users with one button click.
Solution
The working example is shown on the gif below. As a side note, for the sake of presentation, save button simply resets the store, but could be easily changed to POST request. Styling is not perfect – the main focus is on the store and component interaction.
Each component has a dashed border and description below it, hierarchically it looks like this:
- user-create is the top component that contains:
- user-create-form with the form;
- user-create-toolbar with add button and:
- user-create-tab that contains specific users with selection and remove options;
The most important part of the solution is the service that connects all components together, allowing the developer to get rid of Input/Outputs for component interaction and gain access to simple, action-like interface.
... // imports
export interface User {
id: string;
form: Partial<formgroup>;
isSelected: boolean;
}
@Injectable({
providedIn: 'root'
})
export class UserCreateStoreService {
private readonly _users = new BehaviorSubject<array<user>>([]);
readonly users$ = this._users.asObservable();
public get users(): User[] {
return this._users.getValue();
}
public set users(val: User[]) {
this._users.next(val);
}
...
user-create-store.service
The basic setup is extremely simple and consists of four straightforward ideas:
- users – BehaviorSubject with array of users – thanks to that solution, any new subscriber (e.g. using async pipe) will retrieve the last, existing users upon subscription (BehaviorSubject emits the last value);
- users$ – simple observable that gives access to store’s _users values, limiting the user from changing internal data;
- get users – outputs the last _users value; it can be used to modify a copy of current users;
- set users – emits a new users array, which then are distributed to all subscribers (e.g. components hooked up to store using async pipe).
<pre><code>... (continuation from previous snippet)
add() {
const user = this.createUser();
// Immutability as the users are remapped and reemitted
this.users = [...this.users, user];
this.select(this.users[this.users.length - 1]);
}
select(selectedUser: User) {
const foundUser = this.users.find(user => user.id === selectedUser.id);
if(foundUser) {
this.users = this.users.map(user => ({
... user,
// Selects only the provided user
isSelected: user.id === selectedUser.id
}));
}
}
remove(userToRemove: User) {
const usersWithoutRemoved = this.users.filter(
user => user.id !== userToRemove.id);
this.users = [ ... usersWithoutRemoved];
this.select(this.users[0]);
}
// Simplified save for the sake of presentation.
save() {
this.users = [];
this.add();
}
private createUser() {
return {
id: new Date().valueOf().toString(),
form: new FormGroup({
firstName: new FormControl('New', [Validators.required]),
lastName: new FormControl('User', [Validators.required]),
}),
isSelected: false,
} as User;
}
</code></pre>
user-create-store.service
“Actions” are quite simply service methods that make the entire flow easily extendable and simple enough for smaller projects. Most of them work on the copy of current users, modifying them somehow and then remapping and re-emitting the values.
The usage in components is even less complicated. It’s mostly two things:
- Subscribing to the users in store using async pipe.
- One-line methods launching the store “actions”, providing the user if required.
<srs-user-create-toolbar></srs-user-create-toolbar>
<srs-user-create-form></srs-user-create-form>
<button mat-raised-button="" class="save" (click)="save()">Save</button>
<span class="description">user-create</span>
user-create.component.html
export class UserCreateComponent {
constructor(private userStore: UserCreateStoreService) { }
save() {
this.userStore.save();
}
}
user-create.component.ts
User Create Tab
<section class="user__tab" *ngfor="let user of users$ | async; let amount = count" [class.user__tab--selected]="user.isSelected">
<span class="user__name" (click)="select(user)">
{{ userLabel(user) }}</span>
<button class="user__remove" [class.user__remove--disabled]="amount == 1" (click)="remove(user)">X</button>
</section>
user-create-tab.component.html
...
export class UserCreateTabComponent {
users$ = this.userStore.users$;
constructor(private userStore: UserCreateStoreService) { }
userLabel(user: User) {
return `${user.form.getRawValue().firstName} ${user.form.getRawValue().lastName}`;
}
remove(user: User) {
this.userStore.remove(user);
}
select(user: User) {
this.userStore.select(user);
}
}
user-create-tab.component.ts
Finally, it’s also worth noting how the form component leverages the store:
<ng-container *ngfor="let user of users$ | async">
<form class="form" *ngif="user.isSelected" [formgroup]="user.form">
<mat-form-field>
<input matinput="" placeholder="First Name" formcontrolname="firstName">
</mat-form-field>
<mat-form-field>
<input matinput="" placeholder="Last Name" formcontrolname="lastName">
</mat-form-field>
</form>
</ng-container>
...
user-create-form.component.html
The form loops through available users and shows the form for the currently selected user, binding to user.form as the formGroup.
You can check out this example on StackBlitz.
Wrapping up
Simple RxJS store might be viewed as an extremely stripped down approach in comparison with other available options, developed by large teams or even companies. Some may say it’s a solution for very small projects, mostly non-commercial.
It turns out, however, that properly managed and well designed RxJS store using observables not only works wonders in larger projects, but also enables them to be kickstarted quicker, keeping it simple and with very little boilerplate code.
Whenever you start a new project, it’s a great idea to run through the features of all state management frameworks to see, which one suits you best. Who knows – maybe RxJS store, despite its obvious drawbacks, might turn out to be the optimal one?