Reactive Manifesto
New systems have to be more flexible, loosely-coupled and scalable
More complex web systems produce
They became so important, they are part now of ES6
let promise = http.get('http://www.url.com/login');
// ...
promise.then(processLogin, errorLogin)
.then(getUserId, errorHandler)
.then(getPortfolio, errorHandler)
Benefits
Problems
Benefits
Resolves the same problem, different approach
x.then(valueFn, errorFn)
x.subscribe(valueFn, errorFn, completeFn)
Promise:
Observable:
let sub = x.subscribe(valueFn, errorFn, completeFn)
// later time ...
sub.unsubscribe()
The observer pattern is used to allow an object to publish changes to its state. Other objects subscribe to be immediately notified of any changes.
Design Patterns, GoF
Observables are functions that tie observers to a producer
Object.observe feature is obsolete. Use Proxy instead.
Babel does not support Proxies (cannot be transpiled or polyfilled).
Reactive Extensions (Rx) is a library for composing asynchronous and event-based programs using observable sequences.
Operators are methods on Observables that allow you to compose new observables
const startButton = document.querySelector('.start');
const stopButton = document.querySelector('.stop');
const startButton$ = Rx.Observable.fromEvent(startButton, 'click');
const stopButton$ = Rx.Observable.fromEvent(stopButton, 'click');
let subscription = startButton$
.switchMap(() => Rx.Observable.interval(1000) )
.subscribe((x) => console.log(x) );
stopButton$
.subscribe(() => subscription.unsubscribe() );
<button class="start">Start</button>
<button class="stop">Stop</button>
EventEmitter extends Subject, but it may change.
EventEmitter should be used only for @Output()s in components and directives.
@Component({
selector: 'start-stop',
template: `
<button (click)="start()">Start</button>
<button (click)="stop()">Stop</button>
`
})
export class StartStopComponent {
@Output() startEmitter: EventEmitter<any> = new EventEmitter();
@Output() stopEmitter: EventEmitter<any> = new EventEmitter();
start() {
this.startEmitter.emit(null);
}
stop() {
this.stopEmitter.emit(null);
}
}
<start-stop (startEmitter)="onStart()" (stopEmitter)="onStop()"></start-stop>
child.component.ts
parent.component.ts
Template-base forms
Reactive forms
Use ngModel to create two-way data bindings for reading and writing input-control values
Rather than update the data model directly, the component extracts user changes and forwards them to an external component or service.
<form>
<label for="name">Name:</label>
<input type="text" name="name" [(ngModel)]="model.name">
<label for="address">Address:</label>
<input type="text" name="address" [(ngModel)]="model.address">
</form>
import { FormsModule } from '@angular/forms';
app.module.ts
form.template.html
<form [formGroup]="registerForm">
<label for="name">Name:</label>
<input type="text" name="name" formControlName="name">
<label for="address">Address:</label>
<input type="text" name="address" formControlName="address">
</form>
import { ReactiveFormsModule } from '@angular/forms';
app.module.ts
register.template.html
import { FormGroup, FormControl } from '@angular/forms';
export class RegisterComponent {
registerForm = new FormGroup ({
name: new FormControl(),
address: new FormControl()
});
}
register.component.ts
export class RegisterComponent implements OnInit {
registerForm = new FormGroup ({
name: new FormControl(),
address: new FormControl()
});
ngOnInit() {
let form$ = this.registerForm.valueChanges
}
}
Calling valueChanges gets a RxJS Observable
register.component.ts
export class RegisterComponent implements OnInit {
registerForm = new FormGroup ({
name: new FormControl(),
address: new FormControl()
});
ngOnInit() {
this.registerForm.valueChanges
.subscribe(form => console.log(form));
}
}
Nothing happens until subscribe
register.component.ts
// ...
import 'rxjs/add/operator/debounceTime';
export class RegisterComponent {
// ...
this.registerForm.valueChanges
.debounceTime(500)
.subscribe(term => console.log(term))
}
Only execute the last request by buffering the events and handle only the last after some time of inactivity.
register.component.ts
// ...
import 'rxjs/add/operator/distinctUntilChanged';
export class RegisterComponent {
// ...
this.registerForm.valueChanges
.debounceTime(500)
.distinctUntilChange()
.subscribe(term => console.log(term))
}
register.component.ts
Only execute a request different from previous one.
// ...
import 'rxjs/add/operator/filter';
export class RegisterComponent {
// ...
this.registerForm.valueChanges
.filter(regForm => regForm.name.length > 2)
.debounceTime(500)
.distinctUntilChange()
.subscribe(term => console.log(term))
}
register.component.ts
Only search when more than two characters.
// ...
import 'rxjs/add/operator/map';
@Injectable()
export class BackendService {
// ...
search(term: string) {
return this.http.get('https://api.co/query=' + term )
.map(request => request.json().results)
}
}
register.service.ts
http.get returns also an observable
// ...
import 'rxjs/add/operator/flatMap';
export class RegisterComponent {
constructor(private backendService: BackendService) {}
// ...
this.registerForm.valueChanges
.filter(regForm => regForm.name.length > 2)
.debounceTime(500)
.distinctUntilChange()
.flatMap(term => this.backendService.search(term))
.subscribe(searchResult => this.items = searchResults)
}
register.component.ts
Merge observables from form and http.
// ...
import 'rxjs/add/operator/switchMap';
export class RegisterComponent {
// ...
this.registerForm.valueChanges
.filter(regForm => regForm.name.length > 2)
.debounceTime(500)
.distinctUntilChange()
.switchMap(term => this.service.search(term))
.subscribe(searchResult => this.items = searchResults)
}
register.component.ts
Automatically unsubscribe from the previous observables.
If a source Observable emits an error, resubscribe
// ...
import 'rxjs/add/operator/retry';
@Injectable()
export class BackendService {
// ...
search(term: string) {
return this.http.get('https://api.co/query=' + term )
.retry(4)
.map(request => request.json().results)
}
}
register.service.ts
@Injectable()
export class BackendService {
// ...
search(term: string) {
return this.http.get('https://api.co/query=' + term )
.retryWhen(attempts => {
return Observable
.range(1, 4).zip(attempts, (i) => i)
.do(i => console.log('Next attempt in ' +i*i+ ' seconds ...'))
.flatMap(i => Observable.timer(i*i * 1000));
})
.map(request => request.json().results)
}
}
register.service.ts
Next attempt in 1 seconds ...
Next attempt in 4 seconds ...
Next attempt in 9 seconds ...
Next attempt in 16 seconds ...
export class RegisterComponent {
// ...
this.items$ = this.registerForm.valueChanges
.filter(regForm => regForm.name.length > 2)
.debounceTime(500)
.distinctUntilChange()
.switchMap(term => this.service.search(term))
}
register.component.ts
Async pipe subscribes and unsubscribes automatically.
register.component.ts
<ul>
<li *ngFor="let item of items$ | async">{{item.name}}</li>
</ul>
Services
provide data to multiple parts of the application
Observable data
expose reactive data using Observables
Backend service
Store service
UI Component
UI Component
UI Component
Observable
A Subject is both an Observable (so we can subscribe() to it) and an Observer (so we can call next() on it to emit a new value).
let behaviourSubject = new BehaviorSubject(initialState);
// ...
let currentValue = behaviorSubject.getValue();
@Injectable()
export class PortfolioStoreService {
private _portfolio: BehaviorSubject<Portfolio> = new BehaviorSubject(emptyPortfolio);
// ...
portfolio-store.service.ts
BehaviorSubject is a variant of Subject.
It has the notion of "the current value"
@Injectable()
export class PortfolioStoreService {
private _portfolio: BehaviorSubject<Portfolio>;
portfolio$: Observable<Portfolio>
constructor(private portfolioBackendService: PortfolioBackendService) {
this.loadInitialData();
_portfolio = new BehaviorSubject(emptyPortfolio);
this.portfolio$ = this._portfolio.asObservable();
}
getPortfolio(): Observable<Portfolio> {
return this.portfolio$;
}
// ...
portfolio-store.service.ts
Never offer the internal Subject,
always return an Observable.
@Injectable()
export class PortfolioStoreService {
// ...
addPortfolio(newPortfolio:Portfolio):Observable {
let backendObs = this.portfolioBackendService.createPortfolio(newPortfolio);
backendObs.subscribe(
res => {
this._portfolios.next(this._portfolios.getValue().concat(newPortfolio));
});
return backendObs;
}
// ...
portfolio-store.service.ts
@Injectable()
export class PortfolioStoreService {
// ...
modifyPortfolio(newPortfolio:Portfolio): Observable {
return this.portfolioBackendService.modifyPortfolio(newPortfolio)
.subscribe(
res => {
let portfolios = this._portfolios.getValue();
portfolios = portfolios.map((portfolio: Portfolio) =>
(portfolio.id === newPortfolio.id) ? newPortfolio : portfolio);
this._portfolios.next(portfolios);
}
);
}
// ...
portfolio-store.service.ts
When building a Observable data services
Create a portfolio and send to backend server.
@Component({
selector:'app-new-portfolio',
template: `
<form>
...
</form>
<button (click)="create()">Create Portfolio</button>
`
})
export class NewPortfolioComponent {
constructor(private portfolioService: PortfolioService) {
}
create() {
this.portfolioService.createPortfolio(this.portfolio);
}
}
new-portfolio.component.ts
Send a POST a message to server.
@Injectable()
export class PortfolioService {
constructor(private http: Http) {
}
createPortfolio(portfolio) {
const headers = new Headers()
.append('Content-Type', 'application/json; charset=utf-8');
.append('X-Requested-With','XMLHttpRequest');
this.http.post('/portfolios', JSON.stringify({portfolio}), headers);
}
}
portfolio.service.ts
An observable is cold
if the producer isn't activated until subscription
Creating an observable is somewhat similar to declaring a function, the function itself is just a declaration.
export class PortfolioService {
// ...
return this.http.post('/portfolios', JSON.stringify({portfolio}), headers);
}
portfolio.service.ts
export class NewPortfolioComponent {
// ...
create() {
this.portfolioService.createPortfolio(this.portfolio)
.subscribe(
() => console.log('portolio saved successfully'),
console.error
);
}
}
new-portfolio.component.ts
ngOnInit() {
this.portfolio$ = this.portfolioService.loadPortfolio(1);
}
list-portfolio.component.ts
<div class="portfolio-detail" *ngIf="portfolio$ | async">
<div class="portfolio-field">
{{(portfolio$ | async).portfolioName}}
</div>
<div class="portfolio-field">
{{(portfolio$ | async).portfolioCurrency}}
</div>
<div class="portfolio-field">
{{( portfolio$ | async)?.longDescription}}
</div>
</div>
list-portfolio.component.html
... /portfolios GET 200 OK
... /portfolios GET 200 OK
... /portfolios GET 200 OK
... /portfolios GET 200 OK
ngOnInit() {
this.portfolio$ = this.portfolioService.loadPortfolio(1);
}
list-portfolio.component.ts
<div class="portfolio-detail" *ngIf="portfolio$ | async">
<div class="portfolio-field">
{{(portfolio$ | async).portfolioName}}
</div>
<div class="portfolio-field">
{{(portfolio$ | async).portfolioCurrency}}
</div>
<div class="portfolio-field">
{{( portfolio$ | async)?.longDescription}}
</div>
</div>
list-portfolio.component.html
Each async pipe
subscribes and unsubscribes automatically
ngOnInit() {
this.portfolioService.loadPortfolio(1)
.subscribe(result => this.portfolio = result);;
}
list-portfolio.component.ts
<div class="portfolio-detail" *ngIf="portfolio">
<div class="portfolio-field">
{{ portfolio.portfolioName}}
</div>
<div class="portfolio-field">
{{ portfolio.portfolioCurrency}}
</div>
<div class="portfolio-field">
{{ portfolio?.longDescription}}
</div>
</div>
list-portfolio.component.html
Organize components between
stateful vs stateless
export class ListPortfolioComponent implements OnInit {
portfolioObs: Observable<Portfolio>;
constructor(private portfolioService: portfolioService) {
}
ngOnInit() {
this.portfolioObs = this.portfolioService.loadPortfolio(1);
}
}
list-portfolio.component.ts
<portfolio-detail [portfolio]="portfolioObs | async"></portfolio-detail>
list-portfolio.component.html
export class PortfolioDetailComponent {
@Input() portfolio: Portfolio;
constructor() {
}
}
portfolio-detail.component.ts
<div class="portfolio-detail" *ngIf="portfolio">
<div class="portfolio-field">
{{ portfolio.portfolioName}}
</div>
<div class="portfolio-field">
{{ portfolio.portfolioCurrency}}
</div>
<div class="portfolio-field">
{{ portfolio?.longDescription}}
</div>
</div>
portfolio-detail.component.html
Build loosely-coupled systems:
An observable is cold
if the producer isn't activated until subscription
An observable is hot
if the producer is activated outside of subscription
@Injectable()
export class PortfolioService {
// ...
constructor(private http: Http) { }
loadPortfolio(portfolioId: string) {
return this.http.get('https://api.co/portfolio?id=' + portfolioId )
.map(request => request.json().results)
.publish()
.refCount();
}
}
portfolio.service.ts
As long as there is at least one Subscriber
this Observable will be emitting data.
@Injectable()
export class PortfolioService {
// ...
constructor(private http: Http) { }
loadPortfolio(portfolioId: string) {
return this.http.get('https://api.co/portfolio?id=' + portfolioId )
.map(request => request.json().results)
.share();
}
}
<div class="portfolio-detail" *ngIf="portfolio$ | async; else loading; let portfolio">
<div class="portfolio-field">
{{ portfolio.portfolioName}}
</div>
<div class="portfolio-field">
{{ portfolio.portfolioCurrency}}
</div>
<div class="portfolio-field">
{{ portfolio?.longDescription}}
</div>
</div>
list-portfolio.component.html
Discussion is still open in github.com/angular/angular/issues/15020
Only in Angular v4 Release Candidate