TL;DR Angular, previously known as Angular 2, incorporates RxJS and uses it internally. We can make use of some RxJS goodies and introduce FRP to write more robust code in our apps.
If you're new to RxJS, I recommend reading Understanding Reactive Programming and RxJS before proceeding.
RxJS is all about streams, operators to modify them, and observables.
Functional Reactive Programming (FRP)
FRP has recently become a buzzword. To give you a deeper understanding on that topic, there is an awesome post from Andre Stalz -- The introduction to Reactive Programming you've been missing. What is the key takeaway from the this comprehensive post? Reactive programming is actually programming with asynchronous data streams. But where does the word functional come into play? Functional is about how we can modify these streams to create new sets of data. A stream can be used as an input to another stream. We have a bunch of operators in RxJS to do things like this. So, can we do some of FRP with RxJS? The short answer is: yes! And we'll do so with Angular.
RxJS in Angular
To get started with RxJS in Angular, all we need to do is import the operators we want to use. TRxJS is itself an Angular dependency so it's ready to use out of the box.
Passing observables to the view
We are about to start with some observables created ad hoc. Let's create an observable from the JavaScript array:
const items = Observable.of([1, 2, 3])
Now, we can use the created observable as a component's property and pass it into the view. Angular introduced a new filter, which will be a perfect fit here. It's called
async
. Its purpose is to unwrap promises and observables. In the case of an observable it'll pass the last value of the observable:import { Component } from '@angular/core' import { Observable } from 'rxjs/Rx' @Component({ selector: 'my-app', template: ` <ul> <li *ngFor="let item of items | async"> {{ item }} </li> </ul> ` }) export class AppComponent { public items = Observable.of([1, 2, 3]) }
We should see a list of elements in the browser.
This is our hello world example to see how async works and how we can use it.
Http
Angular relies on RxJS for some of its internal features. One of the most well-known services is Http. In Angular 1.x, Http was a promise-based service. In Angular 2+, it's based on observables. This means that we can also make use of the
async
pipe here. Let's try to create a real-world example with a service. We want to fetch a list of repos authored by Auth0 on GitHub:import { Injectable } from '@angular/core' import { Http } from '@angular/http' import { Observable } from 'rxjs/Rx' import 'rxjs/add/operator/map' @Injectable() export class RepoService { constructor(private _http: Http) {} getReposForUser(user: string): Observable<any> { return this._http .get(`https://api.github.com/users/${user}/repos`) .map((res: any) => res.json()) } }
Here, we have the service, which exposes the
getReposForUser
method to make an http call. Note the return type of the method -- it's an Observable<any>
. Now, we can add it into the module and use it in the component:import { RepoService } from './repo.service.js' @Component({ selector: 'my-app', template: ` `, }) export class AppComponent { public repos: Observable<any> constructor(repoService: RepoService) { this.repos = repoService.getReposForUser('auth0') console.log(this.repos) } }
Something important has just happened. You can take a look into the Network tab of your developer tools in the browser. No call was made. Let's add the
for
loop with the async
pipe:@Component({ selector: 'my-app', template: ` <ul> <li *ngFor="let repo of repos | async"> {{ repo.name }} </li> </ul> `, }) export class AppComponent { public repos: Observable<any> constructor(repoService: RepoService) { this.repos = repoService.getReposForUser('auth0') } }
Now the call for repositories is fired, and we can see that the list of repos has been fetched correctly. Why is that?
Hot and Cold Observables
The
http.get
observable above is cold: that means each subscriber sees the same events from the beginning. It's independent of any other subscriber. It also means that if there's no subscriber, no value is emitted! See this one in action:export class AppComponent { public repos: Observable<any> constructor(repoService: RepoService) { this.repos = repoService.getReposForUser('auth0') this.repos.subscribe() this.repos.subscribe() } }
Now you'll be able to see three calls. You can now see one more thing --
async
makes a subscription under the hood.On the other hand, we have hot observables. The difference is, no matter how many subscribers there are, the observable starts just once. And we can make our observable hot, instead of cold, by using the
share
operator:// ... import 'rxjs/add/operator/share' @Injectable() export class RepoService { constructor(private http: Http) {} getReposForUser(user: string): Observable<any> { return this.http .get(`https://api.github.com/users/${user}/repos`) .map((res: any) => res.json()) .share() } }
Now you should see just one call. If you want to go deeper with the topic, here is a Hot vs Cold Observables article by Ben Lesh.
Programming the reactive way in Angular
Handling events
We've covered how you've probably used RxJS observables for Http in Angular, even if you weren't aware of it. However, there are many more things you can do with streams, even if Angular doesn't require you to do so. Now we move on to the on click events. The traditional, imperative way of handling click events in Angular is as follows:
@Component({ selector: 'my-app', template: ` <button (click)="handleButtonClick(1)"> Up Vote </button> `, }) export class AppComponent { handleButtonClick(value: number) { console.log(value) } }
We can create a stream of click events using RxJS
Subject
. Subject is both an observer and an observable at the same time. It means it can emit value (using .next()
), and you can subscribe to it (using subscribe
).Here, you can see the same case achieved with functional approach using RxJS:
@Component({ selector: 'my-app', template: ` <button (click)="counter$.next(1)"> Up Vote </button> `, }) export class AppComponent { public counter$: Observable<number> = new Subject<number>() constructor() { this.counter$.subscribe(console.log.bind(console)) } }
It's not much different than the previous one, though. Let's try to add some more logic there. Like making sum of clicks and printing some text instead of just number.
@Component({ selector: 'my-app', template: ` <button (click)="counter$.next(1)"> Up Vote </button> `, }) export class AppComponent { public counter$: Observable<string> = new Subject<number>() .scan((acc: number, current: number): number => acc + current) .map((value: number): string => `Sum of clicks: ${value}`) constructor() { this.counter$.subscribe(console.log.bind(console)) } }
The key point is that we define how the clicks stream will behave. We say that we don't really need clicks but only the sum of them with some prepended text. And this sum will be our stream, not the pure click events. And we subscribe to the stream of summed values. In other words, the key of functional programming is to make the code declarative, not imperative.
Communication between components
Let's briefly address communication between Angular components using an RxJS approach. It's actually about dumb components in the RxJS approach of an Angular world. Last time I described the change detection of Angular and what we can do with it to fine-tune the app. We'll add the component with
clicks$
stream as the input.import { ChangeDetectionStrategy, Component, Input } from '@angular/core' @Component({ selector: 'my-score', template: 'Summary: {{ score }}', changeDetection: ChangeDetectionStrategy.OnPush }) export class ScoreComponent { @Input() public score: number }
Note that the component has
ChangeDetectionStrategy.OnPush
turned on, so this means that we assume that the new reference will come as the input. The component accepts a numeric parameter, but there is no reference to streams. We can handle this with the async
pipe:@Component({ selector: 'my-app', template: ` <button (click)="counter$.next(1)"> Up Vote </button> <my-score [score]="counter$ | async"></my-score> `, }) export class AppComponent { public counter$: Observable<number> = new Subject<number>() .scan((acc: number, current: number): number => acc + current) }
Forms
Another place when you can use the power of RxJS is forms. We can use all of the knowledge that we have gained up to this point and see how we can create a reactive login form.
First, let's start with adding
ReactiveFormsModule
from @angular/forms
to the module. Then we can make use of the reactive forms introduced in Angular. Here's how it can look:import { FormBuilder, FormGroup } from '@angular/forms' @Component({ selector: 'my-app', template: ` <form [formGroup]="loginForm" > <label>Login:</label> <input formControlName="login" type="text" > <label>Password:</label> <input formControlName="password" type="password" > <button type="submit">Submit</button> </form> `, }) export class AppComponent implements OnInit { public loginForm: FormGroup constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.loginForm = this.formBuilder.group({ login: '', password: '', }) } }
We now have a few additional blocks: -
formControlName
-- added to match names from templates to the appropriate fields in the controller
- formBuilder.group
-- creates the form
- [formGroup]
-- connects the template and the controllerWe can now use the
valueChanges
observable:// ... this.loginForm.valueChanges.subscribe(console.log.bind(console)) // ...
Now, each changed field will emit an event and will be logged to the console. This offers many possibilities since we can take advantage of any operator that RxJS provides. In this example, let's focus on submitting the form in a reactive way. We can put
(submit)
on the form:@Component({ selector: 'my-app', template: ` <form [formGroup]="loginForm" (submit)="submit$.next()" > <!-- ... --> ` }) // ... export class AppComponent { public loginForm: FormGroup private submit$: Observable<any> = new Subject() // ...
We now have a stream of submit events and a stream of values. All that remains is to combine these streams. The resulting stream will emit the current state of the fields when the form is submitted. The desired behavior can be achieved by using the withLatestFrom operator of RxJS. The combined stream is as follows:
// ... this.submit$ .withLatestFrom(this.loginForm.valueChanges, (_, values) => values) .subscribe(values => { console.log('submitted values', values) }) // ...
We now have combined streams, and the logic is consolidated. It can be written in a single line. Just to recap, here is the final code for the form component:
@Component({ selector: 'my-app', template: ` <form [formGroup]="loginForm" (submit)="submit$.next()" > <label>Login:</label> <input formControlName="login" type="text" > <label>Password:</label> <input formControlName="password" type="password" > <button type="submit">Submit</button> </form> `, }) export class AppComponent { public loginForm: FormGroup private submit$: Observable<any> = new Subject() constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.loginForm = this.formBuilder.group({ login: '', password: '', }) this.submit$ .withLatestFrom(this.loginForm.valueChanges, (_, values) => values) .subscribe(values => { console.log('submitted values', values) }) } }
Aside: Authenticate an Angular App with Auth0
By integrating Auth0 in your Angular application, you will be able to manage user identities, including password resets, creating, provisioning, blocking, and deleting users. It requires just a few steps.
Set up an Auth0 application
First, sign up for a free account here. Then, set up an Auth0 application with the following steps:
- Go to your Applications section of the Auth0 Dashboard and click the "Create Application" button.
- Name your new app and select "Single Page Web Applications" as the application type.
- In the Settings for your new Auth0 app, add
to the Allowed Callback URLs, Allowed Web Origins, and Allowed Logout URLs. Click the "Save Changes" button.http://localhost:4200
- If you'd like, you can set up some social connections. You can then enable them for your app in the Application options under the Connections tab. The example shown in the screenshot above uses username/password database, Facebook, Google, and Twitter.
Note: Set up your own social keys and do not leave social connections set to use Auth0 dev keys, or you will encounter issues with token renewal.
Add dependencies and configure
In the root folder of your Angular project, install the
library by typing the following command in a terminal window:auth0-spa-js
npm install @auth0/auth0-spa-js
Then, edit the
environment.ts
file in the src/environments
folder and add the CLIENT_DOMAIN
and CLIENT_ID
keys as follows:// src/environments/environment.ts export const environment = { production: false, auth: { CLIENT_DOMAIN: 'YOUR_DOMAIN', CLIENT_ID: 'YOUR_CLIENT_ID', }, }; export const config = {};
Replace the
andYOUR_DOMAIN
placeholders with the actual values for the domain and client id you found in your Auth0 Dashboard.YOUR_CLIENT_ID
Add the authentication service
Authentication logic in your Angular application is handled with an
AuthService
authentication service. So, use Angular CLI to generate this new service by running the following command:ng generate service auth
Now, open the
src/app/auth.service.ts
file and replace its content with the following://src/app/auth.service.ts import { Injectable } from '@angular/core'; import createAuth0Client from '@auth0/auth0-spa-js'; import Auth0Client from '@auth0/auth0-spa-js/dist/typings/Auth0Client'; import { from, of, Observable, BehaviorSubject, combineLatest, throwError, } from 'rxjs'; import { tap, catchError, concatMap, shareReplay } from 'rxjs/operators'; import { Router } from '@angular/router'; import { environment } from './../environments/environment'; @Injectable({ providedIn: 'root', }) export class AuthService { // Create an observable of Auth0 instance of client auth0Client$ = (from( createAuth0Client({ domain: environment.auth.CLIENT_DOMAIN, client_id: environment.auth.CLIENT_ID, redirect_uri: `${window.location.origin}`, }), ) as Observable<Auth0Client>).pipe( shareReplay(1), // Every subscription receives the same shared value catchError((err) => throwError(err)), ); // Define observables for SDK methods that return promises by default // For each Auth0 SDK method, first ensure the client instance is ready // concatMap: Using the client instance, call SDK method; SDK returns a promise // from: Convert that resulting promise into an observable isAuthenticated$ = this.auth0Client$.pipe( concatMap((client: Auth0Client) => from(client.isAuthenticated())), tap((res) => (this.loggedIn = res)), ); handleRedirectCallback$ = this.auth0Client$.pipe( concatMap((client: Auth0Client) => from(client.handleRedirectCallback())), ); // Create subject and public observable of user profile data private userProfileSubject$ = new BehaviorSubject<any>(null); userProfile$ = this.userProfileSubject$.asObservable(); // Create a local property for login status loggedIn: boolean = null; constructor(private router: Router) { // On initial load, check authentication state with authorization server // Set up local auth streams if user is already authenticated this.localAuthSetup(); // Handle redirect from Auth0 login this.handleAuthCallback(); } // When calling, options can be passed if desired // https://auth0.github.io/auth0-spa-js/classes/auth0client.html#getuser getUser$(options?): Observable<any> { return this.auth0Client$.pipe( concatMap((client: Auth0Client) => from(client.getUser(options))), tap((user) => this.userProfileSubject$.next(user)), ); } private localAuthSetup() { // This should only be called on app initialization // Set up local authentication streams const checkAuth$ = this.isAuthenticated$.pipe( concatMap((loggedIn: boolean) => { if (loggedIn) { // If authenticated, get user and set in app // NOTE: you could pass options here if needed return this.getUser$(); } // If not authenticated, return stream that emits 'false' return of(loggedIn); }), ); checkAuth$.subscribe(); } login(redirectPath: string = '/') { // A desired redirect path can be passed to login method // (e.g., from a route guard) // Ensure Auth0 client instance exists this.auth0Client$.subscribe((client: Auth0Client) => { // Call method to log in client.loginWithRedirect({ redirect_uri: `${window.location.origin}`, appState: { target: redirectPath }, }); }); } private handleAuthCallback() { // Call when app reloads after user logs in with Auth0 const params = window.location.search; if (params.includes('code=') && params.includes('state=')) { let targetRoute: string; // Path to redirect to after login processed const authComplete$ = this.handleRedirectCallback$.pipe( // Have client, now call method to handle auth callback redirect tap((cbRes) => { // Get and set target redirect route from callback results targetRoute = cbRes.appState && cbRes.appState.target ? cbRes.appState.target : '/'; }), concatMap(() => { // Redirect callback complete; get user and login status return combineLatest([this.getUser$(), this.isAuthenticated$]); }), ); // Subscribe to authentication completion observable // Response will be an array of user and login status authComplete$.subscribe(([user, loggedIn]) => { // Redirect to target route after callback processing this.router.navigate([targetRoute]); }); } } logout() { // Ensure Auth0 client instance exists this.auth0Client$.subscribe((client: Auth0Client) => { // Call method to log out client.logout({ client_id: environment.auth.CLIENT_ID, returnTo: `${window.location.origin}`, }); }); } }
This service provides the properties and methods necessary to manage authentication across your Angular application.
Add the login and logout buttons
To add a new component that allows you to authenticate with Auth0, run the following command in a terminal window:
ng generate component login-button
Open the
src/app/login-button/login-button.component.ts
file and replace its content with the following://src/app/login-button/login-button.component.ts import { Component, OnInit } from '@angular/core'; import { AuthService } from '../auth.service'; @Component({ selector: 'app-login-button', templateUrl: './login-button.component.html', styleUrls: ['./login-button.component.css'], }) export class LoginButtonComponent implements OnInit { constructor(public auth: AuthService) {} ngOnInit() {} }
Next, define the component's UI by replacing the content of the
src/app/login-button/login-button.component.html
with the following markup:<!-- src/app/login-button/login-button.component.html --> <div> <button (click)="auth.login()" *ngIf="!auth.loggedIn">Log In</button> <button (click)="auth.logout()" *ngIf="auth.loggedIn">Log Out</button> </div>
Finally, put the
<app-login-button></app-login-button>
tag within the src/app/app.component.html
file, wherever you want the component to appear.Your Angular application is ready to authenticate with Auth0!
Check out the Angular Quickstart to learn more about integrating Auth0 with Angular applications.
Conclusion
Angular has a lot more features than meets the eye. RxJS is, in my personal opinion, one of the best of them. It can rocket the app to the next level in term of maintainability and clarity. The future is more declarative, less imperative code.
RxJS can appear intimidating at first, but once you’re familiar with its functionality and operators, it supplies many benefits (such as defining logic at the declaration time). All of this is to say, the code is easier to understand compared to imperative code. RxJS requires a different mode of thinking, but it is very worthwhile to learn and use.