Angular and The
Case for RxJS
Sandi K. Barr
Senior Software Engineer
Multiple values over time
Cancellable with unsubscribe
Synchronous or asynchronous
Declarative: what, not how or when
Nothing happens without an observer
Separate chaining and subscription
Can be reused or retried
(not a spicy Promise)Observable
!! ! !!
Observable(lazy)
Promise(eager)
Create: new Observable((observer) => observer.next(123)); new Promise((resolve, reject) => resolve(123));
Transform: obs$.pipe(map((value) => value * 2)); promise.then((value) => value * 2);
Subscribe: sub = obs$.subscribe((value) => console.log(value)); promise.then((value) => console.log(value));
Unsubscribe: sub.unsubscribe(); // implied by promise resolution
https://angular.io/guide/comparing-observables#cheat-sheet
Observable
Event
button.addEventListener('click', e => console.log('Clicked', e));
button.removeEventListener('click');
https://angular.io/guide/comparing-observables#observables-compared-to-events-api
const clicks$ = fromEvent(button, 'click');
const subscription = clicks$.subscribe(event => console.log('Clicked', event));
subscription.unsubscribe();
Observable
a function that takes
an Observer
const subscription = observable.subscribe(observer);
subscription.unsubscribe();
Observer
an object with next, error,
and complete methods
const observer = {
next: value => console.log('Next value:', value),
error: err => console.error('Error:', err),
complete: () => console.log('Complete')
};
const subscription = observable.subscribe(observer);
Subscription
an Observable only produces
values on subscribe()
A long time ago,
We used to be friends,
But I haven’t thought of
you lately at all...
Termination is not Guaranteed
https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339
Cold ObsERvables
The producer is created during the subscription
https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339
…
HOT ObsERvables
The producer is created outside the subscription
https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339
…
SubjecT: both an
observable and an observer
Make a cold Observable hot
https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339
…
Subject has state
Keeps a list of observers and sometimes also a number of the values that have been emitted
RxJS
Steep Learning Curve
@Injectable()
export class TodoStoreService {
private _todos: BehaviorSubject<Todo[]> = new BehaviorSubject([]);
constructor(private todoHTTP: TodoHttpService) {
this.loadData();
}
get todos(): Observable<Todo[]> {
return this._todos.asObservable();
}
loadData() {
this.todoAPI.getTodos()
.subscribe(
todos => this._todos.next(todos),
err => console.log('Error retrieving Todos’)
);
}
} https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/store/todo-store.service.ts
Store: Observable Data Service
Provide data to multiple parts of an application
BehaviorSubject
Just because you can access the value directly...
https://gist.github.com/sandikbarr/36bb0bf0b99a82a74be92aba1b1d0482#file-behavior-subject-getvalue-ts
const subject = new BehaviorSubject(initialValue);
// some time later…
const value = subject.getValue();
@Injectable()
export class TodoStoreService {
private _todo: BehaviorSubject<Todo[]> = new BehaviorSubject([]);
constructor(private todoAPI: TodoHttpService) {
this.loadData();
}
get todos(): Observable<Todo[]> {
return this._todos.asObservable();
}
}
https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/store/todo-store.service.ts
Protect the BehaviorSubject with
asObservable()
Components can subscribe after the data arrives
@Component({
selector: 'app-todo-list',
template: `
<ul>
<app-todo-list-item
*ngFor="let todo of todos"
[todo]="todo">
</app-todo-list-item>
</ul>
`
})
export class TodoListComponent implements OnInit, OnDestroy {
todos: Todo[];
todosSub: Subscription;
constructor (private todoStore: TodoStoreService) {}
ngOnInit() {
this.todosSub = this.todoStore.todos.subscribe(todos => {
this.todos = todos;
});
}
ngOnDestroy() {
this.todosSub.unsubscribe();
}
}
LET ME
HAVE THAT
TODO List
CLEAN UP
SUBSCRIPTIONS
@Component({
selector: 'app-todo-list',
template: `
<ul>
<app-todo-list-item
*ngFor="let todo of todos$ | async"
[todo]="todo">
</app-todo-list-item>
</ul>
`
})
export class TodoListComponent {
todos$: Observable<Todo[]> = this.todoStore.todos;
constructor (private todoStore: TodoStoreService) {}
}
USE THE ASYNC PIPE TO
AVOID MEMORY LEAKS
Subscriptions live until a stream is completed or until they are manually unsubscribed
https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/components/todo-list.component.ts
Decouple responsibilities
Observable Data Service
Provide data to multiple parts of an application
Not the same as centralized state management
Observable
Data SERVICE
HTTP SERVICE
COMPONENTS
!=
Action method updates the store on success
Store: Observable Data Service
https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/store/todo-store.service.ts
addTodo(newTodo: Todo): Observable<Todo> {
const observable = this.todoAPI.saveTodo(newTodo);
observable.pipe(
withLatestFrom(this.todos),
).subscribe(( [savedTodo, todos] ) => {
this._todos.next(todos.concat(savedTodo));
});
return observable;
}
Avoid duplicating HTTP requests!
https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/store/todo-store.service.ts
HTTP Observables are Cold
addTodo(newTodo: Todo): Observable<Todo> {
const observable = this.todoAPI.saveTodo(newTodo);
observable.pipe(
withLatestFrom(this.todos),
).subscribe(( [savedTodo, todos] ) => {
this._todos.next(todos.concat(savedTodo));
});
return observable;
}
@Injectable()
export class TodoHttpService {
base = 'http://localhost:3000/todos';
constructor(private http: HttpClient) { }
getTodos(): Observable<Todo[]> {
return this.http.get<Todo[]>(this.base);
}
saveTodo(newTodo: Todo): Observable<Todo> {
return this.http.post<Todo>(this.base, newTodo, {headers}).pipe(share());
}
}
share() makes a cold Observable hot
HTTP Service
https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/http/todo-http.service.ts
OPERATORS
Compose complex
asynchronous code in a
declarative manner
Pipeable Operators: take an input Observable and generate a resulting output Observable
Examples: filter, map, mergeMap
Creation Operators: standalone functions to create a new Observable
Examples: interval, of, fromEvent, concat
share() : refCount
share() also makes Observables retry-able
https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/http/todo-http.service.ts
@Injectable()
export class TodoHttpService {
base = 'http://localhost:3000/todos';
constructor(private http: HttpClient) { }
getTodos(): Observable<Todo[]> {
return this.http.get<Todo[]>(this.base);
}
saveTodo(newTodo: Todo): Observable<Todo> {
return this.http.post<Todo>(this.base, newTodo, {headers}).pipe(share());
}
}
" HOT: Share the execution
⛄ COLD: Invoke the execution
async : Unsubscribes automatically
https://gist.github.com/sandikbarr/36bb0bf0b99a82a74be92aba1b1d0482#file-duplicate-http-component-ts
But still be sure to avoid duplicating HTTP requests!
@Component({
selector: 'app-todo',
template: `
Total #: {{ total$ | async }}
<app-todo-list [todos]="todos$ | async"></app-todo-list>
`
})
export class DuplicateHttpTodoComponent {
todos$ = this.http.get<Todo[]>('http://localhost:3000/todos');
total$ = this.todos$.pipe(map(todos => todos.length));
constructor(private http: HttpClient) {}
}
ngIf with | async as
https://gist.github.com/sandikbarr/36bb0bf0b99a82a74be92aba1b1d0482#file-async-as-component-ts
Assign Observable values to a local variable
@Component({
selector: 'app-todo',
template: `
<ng-container *ngIf="todos$ | async as todos">
Total #: {{ todos.length }}
<app-todo-list [todos]="todos"></app-todo-list>
</ng-container>
`
})
export class TodoComponent {
todos$ = this.http.get<Todo[]>('http://localhost:3000/todos');
constructor(private http: HttpClient) {}
}
ngIf with| async as with ;ngElse
https://gist.github.com/sandikbarr/36bb0bf0b99a82a74be92aba1b1d0482#file-async-as-else-component-ts
Show alternate block when Observable has no value
@Component({
selector: 'app-todo',
template: `
<ng-container *ngIf="todos$ | async as todos; else loading">
Total #: {{ todos.length }}
<app-todo-list [todos]="todos"></app-todo-list>
</ng-container>
<ng-template #loading><p>Loading...</p></ng-template>
`
})
export class TodoComponent {
todos$ = this.http.get<Todo[]>('http://localhost:3000/todos');
constructor(private http: HttpClient) {}
}
Reactive Templates$ $
Compose a series of operators
Observable.prototype.pipe()
import { map, filter, scan } from 'rxjs/operators';
import { range } from 'rxjs';
const source$ = range(0, 10);
source$.pipe(
filter(x => x % 2 === 0),
map(x => x + x),
scan((acc, x) => acc + x, 0)
).subscribe(x => console.log(x));
https://gist.github.com/sandikbarr/36bb0bf0b99a82a74be92aba1b1d0482#file-pipe-ts
https://rxjs-dev.firebaseapp.com/assets/images/guide/marble-diagram-anatomy.svg
map()
A transformational operator
Applies a projection to each value
filter()
ONE OF MANY FILTERING operatorS
Filters items emitted from source
find()
A CONDITIONAL operator
Finds the first match then completes
reduce()
AN Aggregate operator
Emits aggregate result on completion
Emits accumulated result at each interval
scan()
A TRANSFORMATIONAL operator
@Component({
selector: 'app-todo-search',
template: `
<label for="search">Search: </label>
<input id="search" (keyup)="onSearch($event.target.value)"/>
`
})
export class TodoSearchComponent implements OnInit, OnDestroy {
@Output() search = new EventEmitter<string>();
changeSub: Subscription;
searchStream = new Subject<string>();
ngOnInit() {
this.changeSub = this.searchStream.pipe(
filter(searchText => searchText.length > 2), // min length
debounceTime(300), // wait for break in keystrokes
distinctUntilChanged() // only if value changes
).subscribe(searchText => this.search.emit(searchText));
}
ngOnDestroy() {
if (this.changeSub) { this.changeSub.unsubscribe(); }
}
onSearch(searchText: string) {
this.searchStream.next(searchText);
}
} https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/components/todo-search.component.ts
TYPE
AHEAD
SEARCH:
Rate-limit the input and delay the output
debounceTime()
A FILTERING operator
Emits values that are distinct from the previous value
distinctUntilChanged()
A FILTERING operator
AVOID NESTED SUBSCRIPTIONS
pyramid shaped callback hell% %
export class TodoEditComponent implements OnInit {
todo: Todo;
constructor(private todoStore: TodoStoreService,
private route: ActivatedRoute,
private router: Router) {}
ngOnInit() {
this.route.params.subscribe(params => {
const id = +params['id'];
this.todoStore.todos.subscribe((todos: Todo[]) => {
this.todo = todos.find(todo => todo.id === id);
});
});
}
}
Higher-Order Observables
Observables that emit other Observables
https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/components/edit/todo-edit.component.ts
export class TodoEditComponent {
todo$: Observable<Todo> = this.route.params.pipe(
map(params => +params['id']),
switchMap(id => this.todoStore.getTodoById(id))
);
constructor(private todoStore: TodoStoreService,
private route: ActivatedRoute,
private router: Router) {}
}
Cancels inner Observables
switch
Waits for inner Observables to complete
CONCAT
Subscribe to multiple inner Observables at a time
MERGE
// using map with nested subscribe
from([1, 2, 3, 4]).pipe(
map(param => getData(param))
).subscribe(val => val.subscribe(data => console.log(data)));
// using map and mergeAll
from([1, 2, 3, 4]).pipe(
map(param => getData(param)),
mergeAll()
).subscribe(val => console.log(val));
// using mergeMap
from([1, 2, 3, 4]).pipe(
mergeMap(param => getData(param))
).subscribe(val => console.log(val));
mergeMap()
Higher-order transformation operator
Luuk Gruijs: https://medium.com/@luukgruijs/understanding-rxjs-map-mergemap-switchmap-and-concatmap-833fc1fb09ff
Projects each source value into an Observable that is merged into the output Observable
Also provide the latest value from another Observable
withLatestFrom()
combineLatest()
Updates from the latest values of each input Observable
https://gist.github.com/sandikbarr/36bb0bf0b99a82a74be92aba1b1d0482#file-combinelatest-withlatestfrom-ts
combineLatest( [notifications$, otherPrimaryActiities$] )
.subscribe({
next: ( [notification, otherPrimaryActivity] ) => {
// fires whenever notifications$ _or_ otherPrimaryActivities$ updates
// but not until all sources have emitted at least one value
}
});
notifications$.pipe( withLatestFrom(mostRecentUpdates$) )
.subscribe({
next: ( [notification, mostRecentUpdate] ) => {
// fires only when notifications$ updates and includes latest from mostRecentUpdates$
}
});
@Component({
selector: 'app-many-subscriptions',
template: `<p>value 1: {{value1}}</p>
<p>value 2: {{value2}}</p>
<p>value 3: {{value3}}</p>`
})
export class SubscriberComponent implements OnInit, OnDestroy {
value1: number;
value2: number;
value3: number;
destroySubject$: Subject<void> = new Subject();
constructor(private service: MyService) {}
ngOnInit() {
this.service.value1.pipe(
takeUntil(this.destroySubject$)
).subscribe(value => {
this.value1 = value;
});
this.service.value2.pipe(
takeUntil(this.destroySubject$)
).subscribe(value => {
this.value2 = value;
});
this.service.value3.pipe(
takeUntil(this.destroySubject$)
).subscribe(value => {
this.value3 = value;
});
}
ngOnDestroy() {
this.destroySubject$.next();
}
}
Always
Bring
Backup
with this
one weird trick
USE A SUBJECT
TO Complete
STREAMS
https://gist.github.com/sandikbarr/36bb0bf0b99a82a74be92aba1b1d0482#file-weird-trick-takeuntil-ts-L1
tap()
a UTILITY operator
Perform side effects and return the stream unchanged
catchError()
an error handling operator
Catch errors in the stream and return a new Observable
catchError()
An error can terminate a stream and send the error to the error() callback,
or the stream can be allowed to continue if piped through catchError().
https://rxjs-dev.firebaseapp.com/guide/operators
Creation
fromEvent
interval
of
Aggregate
reduce
count
Conditional
find
every
isEmpty
Utility
tap
delay
toArray
Error Handling
catchError
retry
JOIN
mergeAll
withLatestFrom
Filtering
filter
debounceTime
distinctUntilChanged
Transformation
map
mergeMap
scan
Join Creation
merge
concat
combineLatest
Multicasting
share
publish
publishReplay
Aggregate
reduce
count
Conditional
find
every
isEmpty
Utility
tap
delay
toArray
Error Handling
catchError
retry
JOIN
mergeAll
withLatestFrom
Filtering
filter
debounceTime
distinctUntilChanged
Join Creation
merge
concat
combineLatest
Transformation
map
mergeMap
scan Creation
fromEvent
interval
of
Multicasting
share
publish
publishReplay
https://rxjs-dev.firebaseapp.com/guide/operators
Custom Functions to Add and Remove Event Handlers
generate
interval
fromEventPattern
fromEvent
Create a New
Observable
Sequence
That Works Like a for-Loop
That Never Does Anything
That Repeats a Value
That Throws an Error
That Completes
From an Event
From a Promise
That Iterates
That Emits Values on a Timer
Decided at Subscribe Time
Based on a Boolean Condition
Over Values in a Numeric Range
Over Values in an Iterable or Array-like Sequence
Over Arguments
With an Optional Delay
Using Custom Logic
Of Object Key/Values
repeat
throwError
EMPTY
NEVER
from
pairs
range
of
timer
iif
defer
usingThat Depends on a Resource
Troubleshooting
Has this Observable been
subscribed to?

How many Subscriptions
does this Observable have?

When does this Observable
complete? Does it complete?

Do I need to unsubscribe
from this Observable?
THANKS!
example code: https://github.com/sandikbarr/rxjs-todo
slides: https://www.slideshare.net/secret/FL6NONZJ7DDAkf

Angular and The Case for RxJS

  • 1.
    Angular and The Casefor RxJS Sandi K. Barr Senior Software Engineer
  • 2.
    Multiple values overtime Cancellable with unsubscribe Synchronous or asynchronous Declarative: what, not how or when Nothing happens without an observer Separate chaining and subscription Can be reused or retried (not a spicy Promise)Observable !! ! !!
  • 3.
    Observable(lazy) Promise(eager) Create: new Observable((observer)=> observer.next(123)); new Promise((resolve, reject) => resolve(123)); Transform: obs$.pipe(map((value) => value * 2)); promise.then((value) => value * 2); Subscribe: sub = obs$.subscribe((value) => console.log(value)); promise.then((value) => console.log(value)); Unsubscribe: sub.unsubscribe(); // implied by promise resolution https://angular.io/guide/comparing-observables#cheat-sheet
  • 4.
    Observable Event button.addEventListener('click', e =>console.log('Clicked', e)); button.removeEventListener('click'); https://angular.io/guide/comparing-observables#observables-compared-to-events-api const clicks$ = fromEvent(button, 'click'); const subscription = clicks$.subscribe(event => console.log('Clicked', event)); subscription.unsubscribe();
  • 6.
    Observable a function thattakes an Observer const subscription = observable.subscribe(observer); subscription.unsubscribe();
  • 7.
    Observer an object withnext, error, and complete methods const observer = { next: value => console.log('Next value:', value), error: err => console.error('Error:', err), complete: () => console.log('Complete') }; const subscription = observable.subscribe(observer);
  • 8.
    Subscription an Observable onlyproduces values on subscribe() A long time ago, We used to be friends, But I haven’t thought of you lately at all...
  • 9.
  • 10.
  • 11.
    Cold ObsERvables The produceris created during the subscription https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339 …
  • 12.
    HOT ObsERvables The produceris created outside the subscription https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339 …
  • 13.
    SubjecT: both an observableand an observer Make a cold Observable hot https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339 …
  • 14.
    Subject has state Keepsa list of observers and sometimes also a number of the values that have been emitted
  • 15.
  • 16.
    @Injectable() export class TodoStoreService{ private _todos: BehaviorSubject<Todo[]> = new BehaviorSubject([]); constructor(private todoHTTP: TodoHttpService) { this.loadData(); } get todos(): Observable<Todo[]> { return this._todos.asObservable(); } loadData() { this.todoAPI.getTodos() .subscribe( todos => this._todos.next(todos), err => console.log('Error retrieving Todos’) ); } } https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/store/todo-store.service.ts Store: Observable Data Service Provide data to multiple parts of an application
  • 17.
    BehaviorSubject Just because youcan access the value directly... https://gist.github.com/sandikbarr/36bb0bf0b99a82a74be92aba1b1d0482#file-behavior-subject-getvalue-ts const subject = new BehaviorSubject(initialValue); // some time later… const value = subject.getValue();
  • 18.
    @Injectable() export class TodoStoreService{ private _todo: BehaviorSubject<Todo[]> = new BehaviorSubject([]); constructor(private todoAPI: TodoHttpService) { this.loadData(); } get todos(): Observable<Todo[]> { return this._todos.asObservable(); } } https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/store/todo-store.service.ts Protect the BehaviorSubject with asObservable() Components can subscribe after the data arrives
  • 19.
    @Component({ selector: 'app-todo-list', template: ` <ul> <app-todo-list-item *ngFor="lettodo of todos" [todo]="todo"> </app-todo-list-item> </ul> ` }) export class TodoListComponent implements OnInit, OnDestroy { todos: Todo[]; todosSub: Subscription; constructor (private todoStore: TodoStoreService) {} ngOnInit() { this.todosSub = this.todoStore.todos.subscribe(todos => { this.todos = todos; }); } ngOnDestroy() { this.todosSub.unsubscribe(); } } LET ME HAVE THAT TODO List CLEAN UP SUBSCRIPTIONS
  • 20.
    @Component({ selector: 'app-todo-list', template: ` <ul> <app-todo-list-item *ngFor="lettodo of todos$ | async" [todo]="todo"> </app-todo-list-item> </ul> ` }) export class TodoListComponent { todos$: Observable<Todo[]> = this.todoStore.todos; constructor (private todoStore: TodoStoreService) {} } USE THE ASYNC PIPE TO AVOID MEMORY LEAKS Subscriptions live until a stream is completed or until they are manually unsubscribed https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/components/todo-list.component.ts
  • 21.
    Decouple responsibilities Observable DataService Provide data to multiple parts of an application Not the same as centralized state management Observable Data SERVICE HTTP SERVICE COMPONENTS !=
  • 22.
    Action method updatesthe store on success Store: Observable Data Service https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/store/todo-store.service.ts addTodo(newTodo: Todo): Observable<Todo> { const observable = this.todoAPI.saveTodo(newTodo); observable.pipe( withLatestFrom(this.todos), ).subscribe(( [savedTodo, todos] ) => { this._todos.next(todos.concat(savedTodo)); }); return observable; }
  • 23.
    Avoid duplicating HTTPrequests! https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/store/todo-store.service.ts HTTP Observables are Cold addTodo(newTodo: Todo): Observable<Todo> { const observable = this.todoAPI.saveTodo(newTodo); observable.pipe( withLatestFrom(this.todos), ).subscribe(( [savedTodo, todos] ) => { this._todos.next(todos.concat(savedTodo)); }); return observable; }
  • 24.
    @Injectable() export class TodoHttpService{ base = 'http://localhost:3000/todos'; constructor(private http: HttpClient) { } getTodos(): Observable<Todo[]> { return this.http.get<Todo[]>(this.base); } saveTodo(newTodo: Todo): Observable<Todo> { return this.http.post<Todo>(this.base, newTodo, {headers}).pipe(share()); } } share() makes a cold Observable hot HTTP Service https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/http/todo-http.service.ts
  • 25.
    OPERATORS Compose complex asynchronous codein a declarative manner Pipeable Operators: take an input Observable and generate a resulting output Observable Examples: filter, map, mergeMap Creation Operators: standalone functions to create a new Observable Examples: interval, of, fromEvent, concat
  • 26.
    share() : refCount share()also makes Observables retry-able https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/http/todo-http.service.ts @Injectable() export class TodoHttpService { base = 'http://localhost:3000/todos'; constructor(private http: HttpClient) { } getTodos(): Observable<Todo[]> { return this.http.get<Todo[]>(this.base); } saveTodo(newTodo: Todo): Observable<Todo> { return this.http.post<Todo>(this.base, newTodo, {headers}).pipe(share()); } }
  • 27.
    " HOT: Sharethe execution ⛄ COLD: Invoke the execution
  • 28.
    async : Unsubscribesautomatically https://gist.github.com/sandikbarr/36bb0bf0b99a82a74be92aba1b1d0482#file-duplicate-http-component-ts But still be sure to avoid duplicating HTTP requests! @Component({ selector: 'app-todo', template: ` Total #: {{ total$ | async }} <app-todo-list [todos]="todos$ | async"></app-todo-list> ` }) export class DuplicateHttpTodoComponent { todos$ = this.http.get<Todo[]>('http://localhost:3000/todos'); total$ = this.todos$.pipe(map(todos => todos.length)); constructor(private http: HttpClient) {} }
  • 29.
    ngIf with |async as https://gist.github.com/sandikbarr/36bb0bf0b99a82a74be92aba1b1d0482#file-async-as-component-ts Assign Observable values to a local variable @Component({ selector: 'app-todo', template: ` <ng-container *ngIf="todos$ | async as todos"> Total #: {{ todos.length }} <app-todo-list [todos]="todos"></app-todo-list> </ng-container> ` }) export class TodoComponent { todos$ = this.http.get<Todo[]>('http://localhost:3000/todos'); constructor(private http: HttpClient) {} }
  • 30.
    ngIf with| asyncas with ;ngElse https://gist.github.com/sandikbarr/36bb0bf0b99a82a74be92aba1b1d0482#file-async-as-else-component-ts Show alternate block when Observable has no value @Component({ selector: 'app-todo', template: ` <ng-container *ngIf="todos$ | async as todos; else loading"> Total #: {{ todos.length }} <app-todo-list [todos]="todos"></app-todo-list> </ng-container> <ng-template #loading><p>Loading...</p></ng-template> ` }) export class TodoComponent { todos$ = this.http.get<Todo[]>('http://localhost:3000/todos'); constructor(private http: HttpClient) {} }
  • 31.
  • 32.
    Compose a seriesof operators Observable.prototype.pipe() import { map, filter, scan } from 'rxjs/operators'; import { range } from 'rxjs'; const source$ = range(0, 10); source$.pipe( filter(x => x % 2 === 0), map(x => x + x), scan((acc, x) => acc + x, 0) ).subscribe(x => console.log(x)); https://gist.github.com/sandikbarr/36bb0bf0b99a82a74be92aba1b1d0482#file-pipe-ts
  • 33.
  • 34.
  • 35.
    filter() ONE OF MANYFILTERING operatorS Filters items emitted from source
  • 36.
    find() A CONDITIONAL operator Findsthe first match then completes
  • 37.
    reduce() AN Aggregate operator Emitsaggregate result on completion
  • 38.
    Emits accumulated resultat each interval scan() A TRANSFORMATIONAL operator
  • 39.
    @Component({ selector: 'app-todo-search', template: ` <labelfor="search">Search: </label> <input id="search" (keyup)="onSearch($event.target.value)"/> ` }) export class TodoSearchComponent implements OnInit, OnDestroy { @Output() search = new EventEmitter<string>(); changeSub: Subscription; searchStream = new Subject<string>(); ngOnInit() { this.changeSub = this.searchStream.pipe( filter(searchText => searchText.length > 2), // min length debounceTime(300), // wait for break in keystrokes distinctUntilChanged() // only if value changes ).subscribe(searchText => this.search.emit(searchText)); } ngOnDestroy() { if (this.changeSub) { this.changeSub.unsubscribe(); } } onSearch(searchText: string) { this.searchStream.next(searchText); } } https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/components/todo-search.component.ts TYPE AHEAD SEARCH:
  • 40.
    Rate-limit the inputand delay the output debounceTime() A FILTERING operator
  • 41.
    Emits values thatare distinct from the previous value distinctUntilChanged() A FILTERING operator
  • 42.
    AVOID NESTED SUBSCRIPTIONS pyramidshaped callback hell% % export class TodoEditComponent implements OnInit { todo: Todo; constructor(private todoStore: TodoStoreService, private route: ActivatedRoute, private router: Router) {} ngOnInit() { this.route.params.subscribe(params => { const id = +params['id']; this.todoStore.todos.subscribe((todos: Todo[]) => { this.todo = todos.find(todo => todo.id === id); }); }); } }
  • 43.
    Higher-Order Observables Observables thatemit other Observables https://github.com/sandikbarr/rxjs-todo/blob/master/src/app/todo/components/edit/todo-edit.component.ts export class TodoEditComponent { todo$: Observable<Todo> = this.route.params.pipe( map(params => +params['id']), switchMap(id => this.todoStore.getTodoById(id)) ); constructor(private todoStore: TodoStoreService, private route: ActivatedRoute, private router: Router) {} }
  • 44.
  • 45.
    Waits for innerObservables to complete CONCAT
  • 46.
    Subscribe to multipleinner Observables at a time MERGE
  • 47.
    // using mapwith nested subscribe from([1, 2, 3, 4]).pipe( map(param => getData(param)) ).subscribe(val => val.subscribe(data => console.log(data))); // using map and mergeAll from([1, 2, 3, 4]).pipe( map(param => getData(param)), mergeAll() ).subscribe(val => console.log(val)); // using mergeMap from([1, 2, 3, 4]).pipe( mergeMap(param => getData(param)) ).subscribe(val => console.log(val)); mergeMap() Higher-order transformation operator Luuk Gruijs: https://medium.com/@luukgruijs/understanding-rxjs-map-mergemap-switchmap-and-concatmap-833fc1fb09ff Projects each source value into an Observable that is merged into the output Observable
  • 48.
    Also provide thelatest value from another Observable withLatestFrom() combineLatest() Updates from the latest values of each input Observable https://gist.github.com/sandikbarr/36bb0bf0b99a82a74be92aba1b1d0482#file-combinelatest-withlatestfrom-ts combineLatest( [notifications$, otherPrimaryActiities$] ) .subscribe({ next: ( [notification, otherPrimaryActivity] ) => { // fires whenever notifications$ _or_ otherPrimaryActivities$ updates // but not until all sources have emitted at least one value } }); notifications$.pipe( withLatestFrom(mostRecentUpdates$) ) .subscribe({ next: ( [notification, mostRecentUpdate] ) => { // fires only when notifications$ updates and includes latest from mostRecentUpdates$ } });
  • 49.
    @Component({ selector: 'app-many-subscriptions', template: `<p>value1: {{value1}}</p> <p>value 2: {{value2}}</p> <p>value 3: {{value3}}</p>` }) export class SubscriberComponent implements OnInit, OnDestroy { value1: number; value2: number; value3: number; destroySubject$: Subject<void> = new Subject(); constructor(private service: MyService) {} ngOnInit() { this.service.value1.pipe( takeUntil(this.destroySubject$) ).subscribe(value => { this.value1 = value; }); this.service.value2.pipe( takeUntil(this.destroySubject$) ).subscribe(value => { this.value2 = value; }); this.service.value3.pipe( takeUntil(this.destroySubject$) ).subscribe(value => { this.value3 = value; }); } ngOnDestroy() { this.destroySubject$.next(); } } Always Bring Backup with this one weird trick USE A SUBJECT TO Complete STREAMS https://gist.github.com/sandikbarr/36bb0bf0b99a82a74be92aba1b1d0482#file-weird-trick-takeuntil-ts-L1
  • 50.
    tap() a UTILITY operator Performside effects and return the stream unchanged
  • 51.
    catchError() an error handlingoperator Catch errors in the stream and return a new Observable
  • 52.
    catchError() An error canterminate a stream and send the error to the error() callback, or the stream can be allowed to continue if piped through catchError().
  • 53.
  • 54.
  • 55.
    Custom Functions toAdd and Remove Event Handlers generate interval fromEventPattern fromEvent Create a New Observable Sequence That Works Like a for-Loop That Never Does Anything That Repeats a Value That Throws an Error That Completes From an Event From a Promise That Iterates That Emits Values on a Timer Decided at Subscribe Time Based on a Boolean Condition Over Values in a Numeric Range Over Values in an Iterable or Array-like Sequence Over Arguments With an Optional Delay Using Custom Logic Of Object Key/Values repeat throwError EMPTY NEVER from pairs range of timer iif defer usingThat Depends on a Resource
  • 56.
    Troubleshooting Has this Observablebeen subscribed to? How many Subscriptions does this Observable have? When does this Observable complete? Does it complete? Do I need to unsubscribe from this Observable?
  • 57.
    THANKS! example code: https://github.com/sandikbarr/rxjs-todo slides:https://www.slideshare.net/secret/FL6NONZJ7DDAkf