Wojciech Trawiński
Gdańsk, 2019
JavaScript developer working at 7N for Roche company.
Passionate of Angular, RxJS, TypeScript and functional programming.
JavaScript developer working at 7N for Roche company.
Passionate of Angular, RxJS, TypeScript and functional programming.
Owner of JavaScript everyday blog
Writer for Angular In Depth and Angular Love blogs
Powerful overloads
Errors handling
Multicasting
Flattening operators
Miscellaneous stuff
first operator
import { interval } from 'rxjs';
import { first } from 'rxjs/operators';
const source$ = interval(1000);
const result$ = source$.pipe(
first()
);
result$.subscribe(console.log);
//console output: 0
first operator
without arguments
import { interval } from 'rxjs';
import { first } from 'rxjs/operators';
const source$ = interval(1000);
const result$ = source$.pipe(
first()
);
result$.subscribe(console.log);
//console output: 0
first operator
without arguments
import { interval } from 'rxjs';
import { first } from 'rxjs/operators';
const source$ = interval(1000);
const greaterThan = (threshold: number) => (
(val: number) => val > threshold
);
const result$ = source$.pipe(
first(greaterThan(5))
);
result$.subscribe(console.log);
//console output: 6
with predicate fn
filter operator
filter operator
with predicate fn
import { of } from 'rxjs';
import { filter } from 'rxjs/operators';
const source$ = of('foo', 69);
const havingType = (type: string) => (
(val: any) => typeof val === type
);
const result$ = source$.pipe(
filter(havingType('string'))
);
result$.subscribe(console.log);
//console output: 'foo'
filter operator
import { of } from 'rxjs';
import { filter, map } from 'rxjs/operators';
const source$ = of('foo', 69);
const havingType = (type: string) => (
(val: any) => typeof val === type
);
const result$ = source$.pipe(
filter(havingType('string')),
map(({length})=> length)
);
result$.subscribe(console.log);
Fail!
with predicate fn
filter operator
with predicate fn
with type predicate
import { of } from 'rxjs';
import { filter, map } from 'rxjs/operators';
const source$ = of('foo', 69);
const result$ = source$.pipe(
filter((item): item is string => (
typeof item === 'string'
)),
map(({length})=> length)
);
result$.subscribe(console.log);
//console output: 3
import { of } from 'rxjs';
import { filter } from 'rxjs/operators';
const source$ = of('foo', 69);
const havingType = (type: string) => (
(val: any) => typeof val === type
);
const result$ = source$.pipe(
filter(havingType('string'))
);
result$.subscribe(console.log);
//console output: 'foo'
distinctUntilChanged operator
distinctUntilChanged operator
without arguments
import { of } from 'rxjs';
import { distinctUntilChanged } from ...;
const foo = {a: 1};
const bar = {a: 1};
const source$ = of(foo, bar);
const result$ = source$.pipe(
distinctUntilChanged()
);
result$.subscribe(console.log);
//console output: {a: 1}, {a: 1}
distinctUntilChanged operator
without arguments
import { of } from 'rxjs';
import { distinctUntilChanged } from ...;
const foo = {a: 1};
const bar = {a: 1};
const source$ = of(foo, bar);
const result$ = source$.pipe(
distinctUntilChanged()
);
result$.subscribe(console.log);
//console output: {a: 1}, {a: 1}
with compare fn
import { of } from 'rxjs';
import { distinctUntilChanged } from ...;
const foo = {a: 1};
const bar = {a: 1};
const source$ = of(foo, bar);
const result$ = source$.pipe(
distinctUntilChanged((prev, next) => (
prev.a === next.a
))
);
result$.subscribe(console.log);
//console output: {a: 1}
import { interval } from 'rxjs';
import { tap } from 'rxjs/operators';
const source$ = interval(1000);
const result$ = source$.pipe(
tap(val => {
if(val === 1) {
throw new Error('Bazinga!');
}
})
);
do nothing
result$.subscribe(console.log);
//console output: 0, 'Error: Bazinga!'
import { interval } from 'rxjs';
import { tap } from 'rxjs/operators';
const source$ = interval(1000);
const result$ = source$.pipe(
tap(val => {
if(val === 1) {
throw new Error('Bazinga!');
}
})
);
do nothing
result$.subscribe(console.log);
//console output: 0, 'Error: Bazinga!'
provide error notification cb
result$.subscribe({
next: console.log,
error: err => console.log('Handling error'),
complete: () => console.log('Stream completed!')
});
//console output: 0, 'Handling error'
import { interval } from 'rxjs';
import { tap } from 'rxjs/operators';
const source$ = interval(1000);
const result$ = source$.pipe(
tap(val => {
if(val === 1) {
throw new Error('Bazinga!');
}
})
);
import { interval, of } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
const source$ = interval(1000);
const result$ = source$.pipe(
tap(val => {
if(val === 1) {
throw new Error('Bazinga!');
}
}),
catchError(err => {
console.log('Handling error locally');
return of('Fallback value')
})
);
catch error and provide fallback value
result$.subscribe({
next: console.log,
error: err => console.log('Handling error'),
complete: () => console.log('Stream completed!')
});
//console output: 0, 'Handling error locally', 'Fallback value', 'Stream completed!'
catch error and provide fallback value
import { interval, of } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
const source$ = interval(1000);
const result$ = source$.pipe(
tap(val => {
if(val === 1) {
throw new Error('Bazinga!');
}
}),
catchError(err => {
console.log('Handling error locally');
return of('Fallback value')
})
);
retry observable at most n times
import { interval, of } from 'rxjs';
import { tap, retry, catchError } from 'rxjs/operators';
const source$ = interval(1000);
const result$ = source$.pipe(
tap(val => {
if(val === 1) {
throw new Error('Bazinga!');
}
}),
retry(2),
catchError(err => {
console.log('Handling error locally');
return of('Fallback value')
})
);
import { interval, of } from 'rxjs';
import { tap, retry, catchError } from 'rxjs/operators';
const source$ = interval(1000);
const result$ = source$.pipe(
tap(val => {
if(val === 1) {
throw new Error('Bazinga!');
}
}),
retry(2),
catchError(err => {
console.log('Handling error locally');
return of('Fallback value')
})
);
result$.subscribe({
next: console.log,
error: err => console.log('Handling error'),
complete: () => console.log('Stream completed!')
});
//console output: 0, 0, 0 'Handling error locally', 'Fallback value', 'Stream completed!'
retry observable at most n times
import { interval, of } from 'rxjs';
import { tap, retryWhen, delay, catchError } from 'rxjs/operators';
const source$ = interval(1000);
const result$ = source$.pipe(
tap(val => {
if (val === 1) {
throw new Error('Bazinga!');
}
}),
retryWhen(errors => errors.pipe(
delay(2000),
tap(() => console.warn('Retrying...')),
)),
catchError(err => {
console.log('Handling error locally');
return of('Fallback value')
})
);
retry under certain circumstances
retry when the notifier observable emits
import { interval, of } from 'rxjs';
import { tap, retryWhen, delay, take, catchError } from 'rxjs/operators';
const source$ = interval(1000);
const result$ = source$.pipe(
tap(val => {
if (val === 1) {
throw new Error('Bazinga!');
}
}),
retryWhen(errors => errors.pipe(
delay(2000),
tap(() => console.warn('Retrying...')),
take(2)
)),
catchError(err => {
console.log('Handling error locally');
return of('Fallback value')
})
);
retry under certain circumstances
result$ completes without receiving the error
const source$ = interval(1000);
const result$ = source$.pipe(
tap(val => {
if (val === 1) {
throw new Error('Bazinga!');
}
}),
retryWhen(errors => errors.pipe(
delay(2000),
tap(error => {
if (error.message === 'Bazinga!') {
throw error;
}
console.warn('Retrying...')
}),
take(2)
)),
catchError(err => {
console.log('Handling error locally');
return of('Fallback value')
})
);
retry under certain circumstances
catchError handles
the rethrown error
...making cold observable hot...
Hot
Observable closes over notifications producer
…like watching a movie at the cinema:
Hot
Observable closes over notifications producer
…like watching a movie at the cinema:
Cold
Notifications producer created at subscription
…like watching a movie on Netflix:
import { ajax } from 'rxjs/ajax';
import { tap, map } from 'rxjs/operators';
const source$ = ajax('https://jsonplaceholder.typicode.com/users/1').pipe(
tap(() => console.log('Making http request...')),
map(({ response }) => response)
);
const address$ = source$.pipe(
map(({ address }) => address)
);
const company$ = source$.pipe(
map(({ company }) => company)
);
import { ajax } from 'rxjs/ajax';
import { tap, map } from 'rxjs/operators';
const source$ = ajax('https://jsonplaceholder.typicode.com/users/1').pipe(
tap(() => console.log('Making http request...')),
map(({ response }) => response)
);
const address$ = source$.pipe(
map(({ address }) => address)
);
const company$ = source$.pipe(
map(({ company }) => company)
);
address$.subscribe(console.log)
company$.subscribe(console.log)
Two http requests!
import { ajax } from 'rxjs/ajax';
import { tap, map, share } from 'rxjs/operators';
const source$ = ajax('https://jsonplaceholder.typicode.com/users/1').pipe(
tap(() => console.log('Making http request...')),
map(({ response }) => response),
share()
);
const address$ = source$.pipe(
map(({ address }) => address)
);
const company$ = source$.pipe(
map(({ company }) => company)
);
address$.subscribe(console.log)
company$.subscribe(console.log)
Single http request!
share operator
address$.subscribe(console.log)
setTimeout(() => {
company$.subscribe(console.log)
}, 5000)
Two http requests!
import { ajax } from 'rxjs/ajax';
import { tap, map, share } from 'rxjs/operators';
const source$ = ajax('https://jsonplaceholder.typicode.com/users/1').pipe(
tap(() => console.log('Making http request...')),
map(({ response }) => response),
share()
);
const address$ = source$.pipe(
map(({ address }) => address)
);
const company$ = source$.pipe(
map(({ company }) => company)
);
import { ajax } from 'rxjs/ajax';
import { tap, map, shareReplay } from 'rxjs/operators';
const source$ = ajax('https://jsonplaceholder.typicode.com/users/1').pipe(
tap(() => console.log('Making http request...')),
map(({ response }) => response),
shareReplay(1)
);
const address$ = source$.pipe(
map(({ address }) => address)
);
const company$ = source$.pipe(
map(({ company }) => company)
);
address$.subscribe(console.log)
setTimeout(() => {
company$.subscribe(console.log)
}, 5000)
Single http request!
shareReplay operator
switchMap
mergeMap
concatMap
exhaustMap
observable input
observable input
return observable
import { of, from } from 'rxjs';
import { switchMap } from 'rxjs/operators';
const actions$ = of('Login Success Action');
const newActions = ['Hide Spinner Action','Save Token Action'];
const effect$ = actions$.pipe(
switchMap(() => from(newActions))
);
effect$.subscribe(console.log);
//console output:
'Hide Spinner Action',
'Save Token Action'
observable input
return observable
import { of, from } from 'rxjs';
import { switchMap } from 'rxjs/operators';
const actions$ = of('Login Success Action');
const newActions = ['Hide Spinner Action','Save Token Action'];
const effect$ = actions$.pipe(
switchMap(() => from(newActions))
);
effect$.subscribe(console.log);
//console output:
'Hide Spinner Action',
'Save Token Action'
return array
import { of, from } from 'rxjs';
import { switchMap } from 'rxjs/operators';
const actions$ = of('Login Success Action');
const newActions = ['Hide Spinner Action','Save Token Action'];
const effect$ = actions$.pipe(
switchMap(() => newActions)
);
effect$.subscribe(console.log);
//console output:
'Hide Spinner Action',
'Save Token Action'
errors handling
errors handling
import { of, interval } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { switchMap, map, take, catchError } from 'rxjs/operators';
const source$ = interval(2000).pipe(
map(val => val === 1 ? 1000000 : val + 1),
take(5)
);
const result$ = source$.pipe(
switchMap(val => ajax(`https://jsonplaceholder.typicode.com/users/${val}`).pipe(
map(({ response }) => response)
)),
catchError(err => of('Fallback value'))
);
result$.subscribe({
next: console.log,
error: console.error,
complete: () => console.log('Completed!')
});
errors handling
import { of, interval } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { switchMap, map, take, catchError } from 'rxjs/operators';
const source$ = interval(2000).pipe(
map(val => val === 1 ? 1000000 : val + 1),
take(5)
);
const result$ = source$.pipe(
switchMap(val => ajax(`https://jsonplaceholder.typicode.com/users/${val}`).pipe(
map(({ response }) => response)
)),
catchError(err => of('Fallback value'))
);
result$.subscribe({
next: console.log,
error: console.error,
complete: () => console.log('Completed!')
});
errors handling
import { of, interval } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { switchMap, map, take, catchError } from 'rxjs/operators';
const source$ = interval(2000).pipe(
map(val => val === 1 ? 1000000 : val + 1),
take(5)
);
const result$ = source$.pipe(
switchMap(val => ajax(`https://jsonplaceholder.typicode.com/users/${val}`).pipe(
map(({ response }) => response),
catchError(err => of('Fallback value'))
))
);
result$.subscribe({
next: console.log,
error: console.error,
complete: () => console.log('Completed!')
});
...when order matters...
Emits the values emitted by the source Observable
until a notifier Observable emits a value
Emits the values emitted by the source Observable
until a notifier Observable emits a value
import { interval, timer } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
const source$ = interval(1000);
const notifier$ = timer(2500);
const result$ = source$.pipe(
takeUntil(notifier$)
);
result$.subscribe({
next: console.log,
complete: () => console.log('Completed!')
});
//console output: 0, 1, 'Completed!'
with flattening operators
import { interval, timer } from 'rxjs';
import { takeUntil, map, switchMapTo} from 'rxjs/operators';
const source$ = interval(1000);
const notifier$ = timer(2500);
const innerSource$ = interval(300).pipe(
map(idx => `Inner ${idx}`)
);
const result$ = source$.pipe(
takeUntil(notifier$),
switchMapTo(innerSource$)
);
result$.subscribe({
next: console.log,
complete: () => console.log('Completed!')
});
//console output: 'Inner 0', 'Inner 1', 'Inner 2', 'Inner 0' ...
import { interval, timer } from 'rxjs';
import { takeUntil, map, switchMapTo} from 'rxjs/operators';
const source$ = interval(1000);
const notifier$ = timer(2500);
const innerSource$ = interval(300).pipe(
map(idx => `Inner ${idx}`)
);
const result$ = source$.pipe(
takeUntil(notifier$),
switchMapTo(innerSource$)
);
result$.subscribe({
next: console.log,
complete: () => console.log('Completed!')
});
//console output: 'Inner 0', 'Inner 1', 'Inner 2', 'Inner 0' ...
with flattening operators
Memory leak!
Don't do that at home!
import { interval, timer } from 'rxjs';
import { takeUntil, map, switchMapTo} from 'rxjs/operators';
const source$ = interval(1000);
const notifier$ = timer(2500);
const innerSource$ = interval(300).pipe(
map(idx => `Inner ${idx}`)
);
const result$ = source$.pipe(
switchMapTo(innerSource$),
takeUntil(notifier$)
);
result$.subscribe({
next: console.log,
complete: () => console.log('Completed!')
});
//console output: 'Inner 0', 'Inner 1', 'Inner 2', 'Inner 0', 'Completed!'
with flattening operators
Order matters!
...just because you have not subscribed doesn't mean that nothing happens...
Promise recap
const fetchMessage = () => Promise.resolve('Angular rocks!').then(message => {
console.log('Some internal stuff when promise is resolved');
return message;
});
fetchMessage().then(message => console.log( `Received ${message}`));
//console output: 'Some internal stuff...', 'Received Angular rocks!'
Promise recap
const fetchMessage = () => Promise.resolve('Angular rocks!').then(message => {
console.log('Some internal stuff when promise is resolved');
return message;
});
fetchMessage().then(message => console.log( `Received ${message}`));
//console output: 'Some internal stuff...', 'Received Angular rocks!'
const fetchMessage = () => Promise.resolve('Angular rocks!').then(message => {
console.log('Some internal stuff when promise is resolved');
return message;
});
fetchMessage();
//console output: 'Some internal stuff...'
Promise is eager!
Observable from Promise
import { from } from 'rxjs';
const fetchMessage = () => Promise.resolve('Angular rocks!').then(message => {
console.log('Some internal stuff when promise is resolved');
return message;
});
const source$ = from(fetchMessage());
window.requestIdleCallback(() => {
console.log('Have not subscribed yet!')
source$.subscribe(console.log)
});
//console output: 'Some internal stuff...', 'Have not subscribed yet!', 'Angular rocks!'
Promise resolved at the moment of creation
- before calling subscribe on Observable
Wait until micro tasks queue is empty
Creates an Observable that, on subscribe, calls an Observable factory to make an Observable for each new Observer
import { defer, from } from 'rxjs';
const fetchMessage = () => Promise.resolve('Angular rocks!').then(message => {
console.log('Some internal stuff when promise is resolved');
return message;
});
const source$ = defer(() => from(fetchMessage()));
window.requestIdleCallback(() => {
console.log('Have not subscribed yet!')
source$.subscribe(console.log)
});
//console output: 'Have not subscribed yet!', 'Some internal stuff...', 'Angular rocks!'
Promise resolved at the moment of creation,
but it's created when calling subscribe on Observable
Decides at subscription time
which Observable will actually be subscribed
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
const messagesCache = {
'angular': 'Angular rocks!',
'react': 'No comments'
};
function getMessageFromBE(query: string): Observable<string> {
return of(`Message from BE for ${query} query`).pipe(
delay(2000)
);
}
Call BE or return cached value if present
import { Observable, of, asyncScheduler, iif } from 'rxjs';
import { delay } from 'rxjs/operators';
const messagesCache = {
'angular': 'Angular rocks!',
'react': 'No comments'
};
function getMessageFromBE(query: string): Observable<string> {
return of(`Message from BE for ${query} query`).pipe(
delay(2000)
);
}
function fetchMessage(query: string): Observable<string> {
const cachedMessage = messagesCache[query];
return iif(
() => cachedMessage !== undefined,
of(cachedMessage, asyncScheduler),
getMessageFromBE(query)
);
}
fetchMessage('vue').subscribe(console.log);
fetchMessage('angular').subscribe(console.log);
//console output: 'Angular rocks!', 'Message from BE for vue query'
Complete immediately
import { iif, interval } from 'rxjs';
import { map } from 'rxjs/operators';
let hasAccess = true;
const messages$ = interval(1000).pipe(
map(idx => `Message ${idx}`)
);
function getMessagesStream() {
return iif(() => hasAccess, messages$);
}
Default value for streams provided to iif function is EMPTY
import { iif, interval } from 'rxjs';
import { map } from 'rxjs/operators';
let hasAccess = true;
const messages$ = interval(1000).pipe(
map(idx => `Message ${idx}`)
);
function getMessagesStream() {
return iif(() => hasAccess, messages$);
}
getMessagesStream().subscribe({
next: m => console.log(`User#1: ${m}`)
})
hasAccess = false;
getMessagesStream().subscribe({
next: m => console.log(`User#2: ${m}`),
complete: () => console.log('User#2: Completed')
});
console output: 'User#2: Completed', 'User#1: Message 0', ...
const todosServiceStub = {
get() {
return of([{id: 1}]);
}
};
describe('TodosComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ TodosComponent ],
providers: [{provide: TodosService, useValue: todosServiceStub}]
})
});
...
});
That’s what I call cheating
Synchronous version
import { of } from 'rxjs';
const mockResponse = ['foo', 'bar', 'baz'];
const response$ = of(mockResponse);
response$.subscribe(console.log);
console.log('I am sync');
//console output: ['foo', 'bar', 'baz'],
'I am sync'
Synchronous version
import { of } from 'rxjs';
const mockResponse = ['foo', 'bar', 'baz'];
const response$ = of(mockResponse);
response$.subscribe(console.log);
console.log('I am sync');
//console output: ['foo', 'bar', 'baz'],
'I am sync'
Asynchronous version
import { of, asyncScheduler } from 'rxjs';
const mockResponse = ['foo', 'bar', 'baz'];
const response$ = of(mockResponse, asyncScheduler);
response$.subscribe(console.log);
console.log('I am sync');
//console output: 'I am sync',
['foo', 'bar', 'baz']
No more cheating with async scheduler!
import { Subscription } from 'rxjs';
class SubsManager {
private subs: Subscription[] = [];
add(sub: Subscription) {
this.subs.push(sub);
}
cleanup() {
this.subs.forEach(sub => sub.unsubscribe());
}
}
import { Subscription } from 'rxjs';
class SubsManager {
private subs: Subscription[] = [];
add(sub: Subscription) {
this.subs.push(sub);
}
cleanup() {
this.subs.forEach(sub => sub.unsubscribe());
}
}
Object Oriented Programming
Single Responsibility Principle
import { interval, Subscription } from 'rxjs';
class SubsManager {
private subs: Subscription[] = [];
add(sub: Subscription) {
this.subs.push(sub);
}
cleanup() {
this.subs.forEach(sub => sub.unsubscribe());
}
}
const subsManager = new SubsManager();
const source$ = interval(1000);
subsManager.add(source$.subscribe());
subsManager.add(source$.subscribe());
subsManager.add(source$.subscribe());
setTimeout(() => {
subsManager.cleanup();
}, 5000);
You can do better!
import { interval, Subscription } from 'rxjs';
class SubsManager {
private subs: Subscription[] = [];
add(sub: Subscription) {
this.subs.push(sub);
}
cleanup() {
this.subs.forEach(sub => sub.unsubscribe());
}
}
const subsManager = new SubsManager();
const source$ = interval(1000);
subsManager.add(source$.subscribe());
subsManager.add(source$.subscribe());
subsManager.add(source$.subscribe());
setTimeout(() => {
subsManager.cleanup();
}, 5000);
import { interval, Subscription } from 'rxjs';
const subs = new Subscription();
const source$ = interval(1000);
subs.add(source$.subscribe(console.log));
subs.add(source$.subscribe(console.log));
subs.add(source$.subscribe(console.log));
setTimeout(() => {
subs.unsubscribe();
}, 5000);
Subscription instance has add method
EMPTY
NEVER
throwError
EMPTY
Observable that emits no items to the Observer
and immediately emits a complete notification
EMPTY
Observable that emits no items to the Observer
and immediately emits a complete notification
interface Action<T =any> {
type: string;
payload?: T;
}
interface Notification {
message: string;
timeout?: number;
}
enum NotificationActionTypes {
Show = '[Notification] Show',
Dispose = '[Notification] Dispose'
}
EMPTY
interface Action<T =any> {
type: string;
payload?: T;
}
interface Notification {
message: string;
timeout?: number;
}
enum NotificationActionTypes {
Show = '[Notification] Show',
Dispose = '[Notification] Dispose'
}
const getDelayedDisposeAction = timeout => (
of({ type: NotificationActionTypes.Dispose }).pipe(
delay(timeout)
)
);
const effects$ = actions$.pipe(
switchMap(({ payload: { timeout } }: Action<Notification>) => timeout
? getDelayedDisposeAction(timeout)
: EMPTY)
);
Creates an Observable that emits no items to the Observer and immediately emits an error notification
throwError
Creates an Observable that emits no items to the Observer and immediately emits an error notification
throwError
const source$ = interval(1000);
const result$ = source$.pipe(
tap(val => {
if(val === 1) {
throw new Error('Bazinga!');
}
}),
catchError(err => {
console.log('Handling error locally');
return throwError(err);
})
);
Rethrow error after local handling
Observable that emits no items to the Observer
and never completes
NEVER
Observable that emits no items to the Observer
and never completes
NEVER
Place for your example :)
wojtrawi@gmail.com