Asynchronous, Reactive Angular Components

Creating a Reactive Login/Register Component

Paul Bailey
12 min readJan 17, 2023

Reactive programming is talked about a lot in Angular. Although Angular is reactive by default, what I am referring to specifically is reactive programming, with the use of RXJS and working with data streams.

Many of the blog posts I have read explain the benefits of coding in this way, by taking some imperative code, which often has nested observable subscriptions and moving towards a more declarative/reactive approach with the help of RXJS operators.

Here I am taking a relatively simple block of purely synchronous code and converting it into asynchronous, reactive code.

This will hopefully be useful to others as it’s based on a simple login/register dialog component which many web applications will feature.

Let’s begin!

This is the current setup of the component:

export class LoginRegisterDialogComponent implements OnInit {

userIsRegistering = false;
form: UntypedFormGroup;

constructor(
public accountService: AccountService,
private fb: UntypedFormBuilder
) { }

ngOnInit(): void {
this.buildForm();
}

buildForm() {
this.form = this.fb.group({
'username': ['', [Validators.required]],
'password': ['', [Validators.required, Validators.minLength(6)]]
});
}

onSubmitForm() {
if(this.userIsRegistering) {
this.accountService.register(this.form.value);
} else {
this.accountService.login(this.form.value);
}
}

toggleRegisterUser() {
this.userIsRegistering = !this.userIsRegistering;
this.userIsRegistering ? this.form.addControl(
'email',
new FormControl(
'',
[Validators.required, Validators.email]
)
) : this.form.removeControl('email');
}
}

In the preceding code:

1. When the component is initialized within the ngOnInit lifecycle hook, a basic form is built and assigned to the form property

2. onSubmitForm method is created which when invoked calls either the accountService register() or login() method, depending on the value of the userIsRegistering property

3. toggleRegisterUser() method is created which toggles the userIsRegistering property and either adds or removes an email form control, depending on the userIsRegistering property value.

Looks good to me!

What’s the issue with this?

Whilst the above approach is valid, there are a few issues to point out:

1. Nothing is stopping a developer from directly mutating the properties from within the component itself

2. There is a fair amount of logic in the component. Components should be as lean as possible, only responsible for providing data to the template to display. Where possible and logical, components should contain high level of abstractions of low level logic that is provided by an external service

3. A developer could inject this component into another and inadvertently mutate one of the public properties

4. The code is quite imperative here. There has been no effort made to make use of the reactive programming paradigm. Imperative programming is defining what we want to happen and when to do it. Reactive programming is defining what we want to achieve and letting the Observable stream decide when to do it.

Before moving on, let’s quickly look at how the template consumes the data provided by the component:

<div *ngIf="form">
<form [formGroup]="form">
<h1 mat-dialog-title>{{userIsRegistering ? 'Register' : 'Login'}}</h1>
<div mat-dialog-content>
<div class="row mb-2">
<mat-form-field appearance="fill">
<mat-label>Username</mat-label>
<input matInput type="text" formControlName="username">
</mat-form-field>
</div>
<div class="row mb-2">
<mat-form-field appearance="fill">
<mat-label>Password</mat-label>
<input
matInput
type="password"
placeholder="Enter Password.."
formControlName="password">
</mat-form-field>
</div>
<div class="row mb-3" *ngIf="userIsRegistering">
<mat-form-field appearance="fill">
<mat-label>Email</mat-label>
<input matInput type="email" formControlName="email">
</mat-form-field>
</div>
</div>
<div class="row mt-2">
<p class="d-flex justify-content-center">
{{userIsRegistering ? 'Already have an account?' : 'No account?'}}
<span
(click)="toggleRegisterUser()">{{userIsRegistering ? 'Login' : 'Register'}}
</span>
</p>
</div>
<div mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button
mat-button
(click)="onSubmitForm()"
type="submit">
{{userIsRegistering ? 'Register' : 'Login'}}
</button>
</div>
</form>
</div>

One issue here is the violation of the DRY principle; the ternary operator is repeated four times within the interpolation throughout the template. We’ll come back to this later.

Let’s fix this!

A lot of the logic in both the template and typescript class is dependent on the value of the userIsRegistering property:

1. The ternary operators in the template

<h1 mat-dialog-title>{{register ? 'Register' : 'Login'}}</h1>

2. The adding/removing of the email form control

toggleRegisterUser() {
this.userIsRegistering = !this.userIsRegistering;
this.userIsRegistering ? this.form.addControl(
'email',
new FormControl(
'',
[Validators.required, Validators.email]
)
) : this.form.removeControl('email');
}

3. The submitting of the form

onSubmitForm() {
if(this.userIsRegistering) {
this.accountService.register(this.form.value);
} else {
this.accountService.login(this.form.value);
}
}

Rather than programming in such an imperative way, wouldn’t it be great if we could switch to a more reactive approach and react to changes to this state?

Reactive Approach

First, let’s refactor the userIsRegistering property:

export class LoginRegisterDialogComponent implements OnInit {

private userIsRegisteringToggle = new Subject<void>();
userIsRegistering$: Observable<boolean> = this.userIsRegisteringToggle.pipe(
scan(previous => !previous, false),
startWith(false)
);

In the preceding code:

1. A userIsRegisteringToggle property has been created which is a Subject. The private access modifier prevents other developers from directly mutating it from outside of the component

2. A userIsRegistering$ property has been created. This property’s state is set when the userIsRegisteringToggle Subject is emitted, using the RXJS scan() operator. This operator takes an accumulator function and a seed value to initialize the state. The startWith() operator emits the Observable with an initial value.

With the above configured, whenever the userIsRegisteringToggle is emitted, the state of the userIsRegistering$ Observable is toggled.

Next, I want to start deriving state from the userIsRegistering$ Observable:

export class LoginRegisterDialogComponent implements OnInit {

private userIsRegisteringToggle = new Subject<void>();
userIsRegistering$: Observable<boolean> = this.userIsRegisteringToggle.pipe(
scan(previous => !previous, false),
startWith(false)
);

form$: Observable<UntypedFormGroup> = this.userIsRegistering$.pipe(
map((userIsRegistering) => {
const form = this.fb.group({
'username': ['', [Validators.required]],
'password': ['', [Validators.required, Validators.pattern("^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$")]]
});
if(userIsRegistering) {
form.addControl('email', new FormControl('', [Validators.required, Validators.email]));
}
return form;
})
);

The old form property has been deleted and a new form$ Observable (which wraps an UntypedFormGroup) has been created.

The value of this Observable is dependent on the value of the userIsRegistering$ Observable. I.e. when the user is registering, the form should contain the email form control. If they’re not registering, the form should only contain the username and password form controls.

One way to handle this dependency in a reactive way is to derive the state of the form object from the state of the userIsRegistering$ Observable. To do this, the RXJS pipe() and Map() operators are used on the userIsRegistering$ Observable to build the form based on the Observable’s value.

The Map() operator then returns a new derived Observable which contains the UntypedFormGroup object.

Now, every time theuserIsRegistering$ Observable receives a new value (i.e. when the userIsRegisteringToggle Subject is emitted), the form$ Observable will be updated with the correct UntypedFormGroup controls.

Nice! Next, let’s update the template to make use of the new form$ and userIsRegistering$ Observables:

<div *ngIf="(form$ | async) as form">
<form [formGroup]="form">
<h1 mat-dialog-title>{{(userIsRegistering$ | async) ? 'Register' : 'Login'}}</h1>
<div mat-dialog-content>
<div class="row mb-2">
<mat-form-field appearance="fill">
<mat-label>Username</mat-label>
<input matInput type="text" formControlName="username">
</mat-form-field>
</div>
<div class="row mb-2">
<mat-form-field appearance="fill">
<mat-label>Password</mat-label>
<input
matInput
type="password"
placeholder="Enter Password.."
formControlName="password">
</mat-form-field>
</div>
<div class="row mb-3" *ngIf="(userIsRegistering$ | async)">
<mat-form-field appearance="fill">
<mat-label>Email</mat-label>
<input matInput type="email" formControlName="email">
</mat-form-field>
</div>
</div>
<div class="row mt-2">
<p class="d-flex justify-content-center">
{{(register$ | async) ? 'Already have an account?' : 'No account?'}}
<span
(click)="toggleRegisterUser()">{{(userIsRegistering$ | async) ? 'Login' : 'Register'}}
</span>
</p>
</div>
<div mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button
mat-button
(click)="onSubmitForm()"
type="submit">
{{(userIsRegistering$ | async) ? 'Register' : 'Login'}}
</button>
</div>
</form>
</div>

All the references to the old userIsRegistering property have been replaced with the new userIsRegistering$ Observable. The reference to the old form property has also been replaced with the new form$ Observable.

In both instances, the async pipe has been used. Fortunately, this handles the subscribing/unsubscribing to the Observables for us!

Don’t Repeat Yourself (DRY)

Some of you eagle eyed readers will notice that we’re still left with the DRY principle violations in the template. More derived state is needed…

State is already being derived within the template using the ternary operator:

<h1 mat-dialog-title>{{(userIsRegistering$ | async) ? 'Register' : 'Login'}}</h1>

In the preceding code, the state of the h1 tag is derived from the userIsRegister$ Observable. Unfortunately, this is repeated multiple times within the template.

Personally, I prefer to keep business logic within the typescript code, so let’s also move the ternary operator out of the template.

Let’s derive this state in the component’s typescript file:

export class LoginRegisterDialogComponent implements OnInit {

private userIsRegisteringToggle = new Subject<void>();
userIsRegistering$: Observable<boolean> = this.userIsRegisteringToggle.pipe(
scan(previous => !previous, false),
startWith(false)
);

form$: Observable<UntypedFormGroup> = this.userIsRegistering$.pipe(
map((userIsRegistering) => {
const form = this.fb.group({
'username': ['', [Validators.required]],
'password': ['', [Validators.required, Validators.pattern("^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$")]]
});
if(userIsRegistering) {
form.addControl('email', new FormControl('', [Validators.required, Validators.email]));
}
return form;
})
);

accountAction$: Observable<{type: string, linkLabel: string, linkText: string, buttonAction: string}> = this.userIsRegistering$.pipe(
map(userIsRegistering => {
return {
type: userIsRegistering ? 'Register' : 'Login',
linkLabel: userIsRegistering ? 'Already have an account?' : 'No account?',
linkText: userIsRegistering ? 'Login' : 'Register',
buttonAction: userIsRegistering ? 'Register' : 'Login',
}
})
)

In the preceding code, an accountAction$ Observable has been added. This wraps an Object with several properties.

The value of this Observable is dependent on the value of the userIsRegister$ Observable. Therefore, the state of this new property can be derived from the state of the userIsRegister$ Observable.

The pipe() and map() RXJS operators are used again to derive a new Observable. This contains an Object with the following properties:

1. type: The type of action (i.e. register or login)

2. linkLabel: The text of the link label

3. linkText: The link placeholder text

4. buttonAction : The text that will be displayed on the submit button.

With this new state derived, let’s refactor the template:

<div *ngIf="(form$ | async) as form">
<form [formGroup]="form">
<ng-container *ngIf="(accountAction$ | async) as accountAction">
<h1 mat-dialog-title>{{accountAction.type}}</h1>
<div mat-dialog-content>
<div class="row mb-2">
<mat-form-field appearance="fill">
<mat-label>Username</mat-label>
<input matInput type="text" formControlName="username">
</mat-form-field>
</div>
<div class="row mb-2">
<mat-form-field appearance="fill">
<mat-label>Password</mat-label>
<input
matInput
type="password"
placeholder="Enter Password.."
formControlName="password">
</mat-form-field>
</div>
<div class="row mb-3" *ngIf="register">
<mat-form-field appearance="fill">
<mat-label>Email</mat-label>
<input matInput type="email" formControlName="email">
</mat-form-field>
</div>
</div>
<div class="row mt-2">
<p class="d-flex justify-content-center">
{{accountAction.linkLabel}}
<span
(click)="toggleRegisterUser()">{{accountAction.linkText}}
</span>
</p>
</div>
<div mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button
mat-button
(click)="onSubmitForm()"
type="submit">
{{accountAction.buttonAction}}
</button>
</div>
</ng-container>
</form>
</div>

Now, the async operator is only used once within the template.

Another improvement can be made within the typescript code.

Interfaces = Easier Maintainability + Increased Re-usability

First, add an AccountAction interface above the class declaration:

export interface AccountAction {
type: string,
linkLabel: string,
linkText: string,
buttonAction: string
}

This interface can now be used within the type parameter of the accountAction$ Observable:

accountAction$: Observable<AccountAction> = this.userIsRegistering$.pipe(
map(userIsRegistering => {
return {
type: userIsRegistering ? 'Register' : 'Login',
linkLabel: userIsRegistering ? 'Already have an account?' : 'No account?',
linkText: userIsRegistering ? 'Login' : 'Register',
buttonAction: userIsRegistering ? 'Register' : 'Login',
}
})
);

Now, if this type parameter was used multiple times and needed to be refactored, it would only need to be refactored in one place.

Another improvement to be made is reducing the amount of logic within the component.

Components Are For Presentation

A component’s responsibility is to prepare and pass data to the template. Where possible, lower level implementation details should be abstracted into a service. The relevant data should then be passed to the component in a declarative way.

First, move the logic out of the component and into the AccountService:

export class AccountService {

constructor(private fb: UntypedFormBuilder) { }

private userIsRegisteringToggle = new Subject<void>();
userIsRegistering$: Observable<boolean> = this.userIsRegisteringToggle.pipe(
scan(previous => !previous, false),
startWith(false)
);

form$: Observable<UntypedFormGroup> = this.userIsRegistering$.pipe(
map((userIsRegistering) => {
const form = this.fb.group({
'username': ['', [Validators.required]],
'password': ['', [Validators.required, Validators.pattern("^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$")]]
});
if(userIsRegistering) {
form.addControl('email', new FormControl('', [Validators.required, Validators.email]));
}
return form;
})
);

accountAction$: Observable<AccountAction> = this.userIsRegistering$.pipe(
map(userIsRegistering => {
return {
type: userIsRegistering ? 'Register' : 'Login',
linkLabel: userIsRegistering ? 'Already have an account?' : 'No account?',
linkText: userIsRegistering ? 'Login' : 'Register',
buttonAction: userIsRegistering ? 'Register' : 'Login',
}
})
);
}

Great, now let’s add a toggleUserIsRegistering() method to the service:

toggleUserIsRegistering() {
this.userIsRegisteringToggle.next();
}

Next, refactor the toggleUserIsRegistering() method in the component:

toggleUserIsRegistering() {
this.accountService.toggleUserIsRegistering();
}

To make the component more reactive, the form submission logic needs refactoring.

Making Reactive Forms, well...More Reactive

First, refactor the onSubmitForm() component method:

onFormSubmit(form: UntypedFormGroup) {
this.accountService.OnAccountAccessFormSubmitted(form.value);
}

Then add the following code to the service:

export interface LoginUser {
username: string;
password: string;
}

export interface RegisterUser {
username: string;
password: string;
email: string;
}

export class AccountService {

private accountAccessFormSubmitted: Subject<RegisterUser | LoginUser> = new Subject();

private loginRegisterUrl$ = this.userIsRegistering$.pipe(
map(userIsRegistering => {
const loginRegisterUrl = userIsRegistering ? `${this.baseUrl}/Register` : `${this.baseUrl}/Login`;
return loginRegisterUrl;
})
);

handleAccountAccessFormSubmitted$ = this.accountAccessFormSubmitted.pipe(
withLatestFrom(this.loginRegisterUrl$),
switchMap(([formValue, loginRegisterUrl]) => this.loginOrRegister(formValue, loginRegisterUrl))
).subscribe();

private loginOrRegister(user: RegisterUser | LoginUser, url: string) {
return this.http.post<User>(url, user).pipe(
tap(user => {
if(user) {
this.setLoggedOnUser(user),
this.dialog.closeAll();
this.router.navigate(['/home']);
}
})
);
}

onAccountAccessFormSubmitted(formValue: RegisterUser | LoginUser) {
this.accountAccessFormSubmitted.next(formValue);
}

In the preceding code:

  1. An accountAccessFormSubmitted Subject has been added which takes either the RegisterUser or LoginUser interface types which are defined above the class declaration
  2. An accountAccessFormSubmitted$ Observable has been created which is emitted when the accountAccessFormSubmitted Subject is emitted
  3. A private loginRegisterUrl$ Observable is derived from the userIsRegistering$ Observable which generates the correct url depending on whether the user is registering or logging in
  4. The handleAccountAccessFormSubmitted$ is created and emits whenever the accountAccessFormSubmitted$ Observables is emitted. The value of the loginRegisterUrl$ Observable is extracted using the RXJS withLatestFrom() operator. The value of this Observable is passed to the loginOrRegister() method (along with the form value) via the switchMap() operator. This operator takes the value from the outer Observable and passes it to an inner Observable. It also automatically unsubscribes from the outer Observable. Subscribing to the handleAccountAccessFormSubmitted$ Observable immediately, ensures it gets triggered
  5. onAccountAccessFormSubmitted method is created to emit accountAccessFormSubmitted$ Observable.

The above approach does increase the amount of logic however, it is written in a more reactive/declarative way.

Next, bring the Observables needed back into the component:

export class LoginRegisterDialogComponent {

form$: Observable<UntypedFormGroup> = this.accountService.form$;

accountAction$: Observable<AccountAction> = this.accountService.accountAction$;Now all the low level implementation details are abstracted and the component’s only job is to receive the relevant data from the service and pass it to the template.

Okay, almost there.

Combining Observables

There is still a nested subscription in the template:

<div *ngIf="(form$ | async) as form">
<form [formGroup]="form">
<ng-container *ngIf="(accountAction$ | async) as accountAction">
<h1 mat-dialog-title>{{accountAction.type}}</h1>

This could be improved by refactoring the Observable assignment in the component:

accountAccess$: Observable<{form: UntypedFormGroup, accountAction: AccountAction}> = combineLatest(([
this.accountService.form$,
this.accountService.accountAction$,
]), (accountAccessForm, accountAction) => {
return {
form: accountAccessForm,
accountAction: accountAction
}
});

The RXJS combineLatest() operator is used to combine the form$ and accountAction$ Observables. This operator combines the values from the multiple Observables and will only emit once all inner Observables have emitted at least their first value.

Now the Observable can be subscribed to within the template:

<div *ngIf="(accountAccess$ | async) as accountAccess">
<form [formGroup]="accountAccess.form">
<h1 mat-dialog-title>{{accountAccess.accountAction.type}}</h1>
<div mat-dialog-content>
<div class="row mb-2">
<mat-form-field appearance="fill">
<mat-label>Username</mat-label>
<input matInput type="text" formControlName="username">
</mat-form-field>
</div>
<div class="row mb-2">
<mat-form-field appearance="fill">
<mat-label>Password</mat-label>
<input
matInput
type="password"
placeholder="Enter Password.."
formControlName="password">
</mat-form-field>
</div>
<div class="row mb-3" *ngIf="accountAccess.form.contains('email')">
<mat-form-field appearance="fill">
<mat-label>Email</mat-label>
<input matInput type="email" formControlName="email">
</mat-form-field>
</div>
</div>
<div class="row mt-2">
<p class="d-flex justify-content-center">
{{accountAccess.accountAction.linkLabel}}
<span
(click)="toggleRegisterUser()">{{accountAccess.accountAction.linkText}}
</span>
</p>
</div>
<div mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button
mat-button
(click)="onSubmitForm(accountAccess.form)"
type="submit">
{{accountAccess.accountAction.buttonAction}}
</button>
</div>
</form>
</div>

Summary

There you have it, an uninspiring, imperative block of code has been refactored into reactive, declarative code.

Now, whenever the user switches between logging in and registering, the userIsRegistering Behavior Subject is triggered. This then triggers the form$ and accountAction$ Observables used within the template.

Previously, the code was somewhat imperative. I.e. describing what will happen and how/when.

The new logic describes the results to be achieved. I.e. the form object to be recreated whenever the userIsRegistering$ Observable is toggled:

accountAccessForm$: Observable<UntypedFormGroup> = this.userIsRegistering$.pipe(
map((userIsRegistering) => {
const form = this.fb.group({
'username': ['', [Validators.required], [this.validationService.uniqueUsernameValidatorFn(userIsRegistering)]],
'password': ['', [Validators.required, Validators.pattern("^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$")]]
});
if(userIsRegistering) {
form.addControl('email', new FormControl('', [Validators.required, Validators.email], [this.validationService.uniqueEmailValidatorFn()]));
}
return form;
})
);

It is not necessarily describing when/how this will happen.

With the help of some RXJS magic, abstraction away from these lower level implementation details has been accomplished.

Deriving state in this way is a powerful concept. It enables easy reaction to changes in state and is a useful tool when there are dependencies between states.

As the Subject is a private property, it can only be triggered from within the AccountService. Only the Observables (i.e. the listeners) are exposed to the rest of the code base:

  private userIsRegisteringToggle = new Subject<void>();
userIsRegistering$: Observable<boolean> = this.userIsRegisteringToggle.pipe(
scan(previous => !previous, false),
startWith(false)
);

API’s can be created to trigger the Subjects within the Service. Similar to what has been done here:

 toggleUserIsRegistering() {
this.userIsRegisteringToggle.next();
}

If required, extra logic can be added before updating the value. E.g. transforming data or checking the validity of data.

Moving the code out into an external service made the component leaner. Some may argue that the algorithm to rebuild the form Object and AccountAction Object should belong in the component, as it relates specifically to what is rendered on the template. Personally I value a leaner component over this.

The repetition of ternary operators/async pipes in the template has also been removed. Now, just one subscription to one Observable exists, supplying all the data the template needs.

<ng-container *ngIf="(accountAccessObs$ | async) as accountAccess">
<div *ngIf="(accountAccess.form) as form">
<form [formGroup]="accountAccess.form">
<h1 mat-dialog-title>{{accountAccess.action.type}}</h1>

--

--

Paul Bailey
0 Followers

A Full Stack Developer working within the Financial Services industry