0% found this document useful (0 votes)
274 views60 pages

Theory of RXJS Slides

The document provides an overview of RxJS (Reactive Extensions for JavaScript) and Observables. It begins by explaining the mental model for Observables, describing them as lazy and asynchronous representations of values that may be single or multiple over time. Various operators for Observables are then discussed, including mapping, filtering, projection, combination, and result selection operators. The document concludes by emphasizing best practices like using concatMap as the default projection operator choice.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
274 views60 pages

Theory of RXJS Slides

The document provides an overview of RxJS (Reactive Extensions for JavaScript) and Observables. It begins by explaining the mental model for Observables, describing them as lazy and asynchronous representations of values that may be single or multiple over time. Various operators for Observables are then discussed, including mapping, filtering, projection, combination, and result selection operators. The document concludes by emphasizing best practices like using concatMap as the default projection operator choice.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 60

Theory of RxJS

Or, “yet another Observables talk”

Alex Rickabaugh @synalx


These look similar, but aren’t.

class ApiService {

getData(): Promise<Data>;

getData(): Observable<Data>;

@synalx
1 2 3

Mental Model Operators Best Practices

Mental Model for Observables

@synalx
In the beginning: Callbacks

http.get(‘/api/data’, (result) => {


console.log(result);
});

@synalx
In the beginning: Callbacks

http.get(‘/api/data’, (result) => {


console.log(result);
});

@synalx
In the beginning: Callbacks

http.get(‘/api/data’, (result) => {


console.log(result);
});

@synalx
Callback Hell

http.get(‘/api/data’, result => {


model.processUpdate(result, newModel => {
ui.update(newModel, ref => {
ref.animations.run(() => {
// And so on.
});
});
});
});

@synalx
Promise

const result = fetch(‘/api/data’); // Promise<Response>

// HTTP does its thing in the background.

// Some time later


result.then(data => { … });

@synalx
Promises
Disconnect the action from what comes next

Callbacks can be added afterwards

The expectation of a value as a first class object

@synalx
Beyond Promises
Promises have their own problems

Only one response

No way to cancel the in-progress action

@synalx
Observables
Are similar to Promises:

Async primitive

Can be used for same use case

But are also different:

Multiple values, cancellation

Lazy (represent actions not yet executed)


@synalx
Eager vs. Lazy
Eager

Computation of the result begins immediately

Lazy

Computation doesn’t start until the result is used

@synalx
Eager vs. Lazy

// Here, the result is eagerly computed.


const eagerResult = 1 + 2;

// Here, it’s only computed on demand.


const lazyResult = () => 1 + 2;

// To get the result, we need to execute it.


console.log(lazyResult());

@synalx
Mental Model
Sync Async

Eager
Lazy

@synalx
Mental Model
Sync Async

Eager
Scalar
Lazy

@synalx
Mental Model
Sync Async

Eager
Scalar
Lazy

() => Scalar

@synalx
Mental Model
Sync Async

Eager
Scalar Promise
Lazy

() => Scalar

@synalx
Mental Model
Sync Async

Eager
Scalar Promise
Lazy

() => Scalar () => Promise

@synalx
Lazy + Async

const getData = () => fetch(‘/api/data’);

getData().then(data => console.log(data));

// Later on...
getData().then(freshData => console.log(freshData));

@synalx
Services as a container for async functions

class ApiService {

getData(): Promise<Data> {
return fetch(‘/api/data’).then(r => r.json());
}

@synalx
Mental Model
Sync Async

Eager
Scalar Promise
Lazy

() => Scalar () => Promise

@synalx
Mental Model
Sync Async

Eager
Scalar Promise

() => Promise
Lazy

() => Scalar
Observable

@synalx
Observables as async functions

An Observable represents an operation which could run


(like a function).

It’s also asynchronous, like a Promise.

@synalx
Basic example

const obs = new Observable(subscriber => {


http.get(‘/api/data’, result => {
subscriber.next(result);
});
});

@synalx
Basic example

// To “call” an Observable, we subscribe to it.


obs.subscribe(result => console.log(result));

// Subscribing again runs the computation again.


obs.subscribe(result => console.log(‘again: ‘, result));

@synalx
Long running example

const timer$ = new Observable(subscriber => {


// Emit every second indefinitely.
const timer = setInterval(() => subscriber.next(), 1000);

// Called when the subscriber unsubscribes.


return () => {
clearInterval(timer);
};
});

@synalx
Back to ApiService

class ApiService {

getData(): Promise<Data>;

getData(): Observable<Data>;

@synalx
Back to ApiService

class ApiService {

getData(): Promise<Data>;

data$: Observable<Data>;

@synalx
1 2 3

Mental Model Operators Best Practices

Operators

@synalx
Operators

Operators are the equivalent of composition

Observable → Observable

100+ operators!

@synalx
Array-like operators
map

transform one value to another

(Value → Value)

filter

exclude values not matching a predicate


@synalx
map

const source$ = Observable.from([1, 2, 3]);

// Double every value from source$.


const mapped$ = source$.pipe(map(value => value * 2));

mapped$.subscribe(v => console.log(v));


// 2, 4, 6, ...

@synalx
map

const source$ = Observable.from([1, 2, 3]);

// Double every value from source$.


const mapped$ = source$.pipe(map(value => value * 2));

source$.subscribe(v => console.log(v));


// 1, 2, 3, ...

@synalx
Projection
Sometimes we want to asynchonously transform values

Value → Observable

@synalx
Projections

const source$ = Observable.from([1, 2, 3]);

const mapped$ = source$.pipe(


map(value => http.get<Data>(`/data?id=${value}`))
);

// What type is mapped$?


// Observable<Observable<Data>>

@synalx
Higher-order Observable

Observable<Observable<Data>>

@synalx
Higher order Observable
Observable<Observable<Data>>

Need some way to flatten into Observable<Data>

Different ways of doing this

Subscribe to every inner Observable simultaneously

Subscribe to one at a time…

etc.
@synalx
Projection operators
Transform Observable<Observable<T>> to Observable<T>

mergeAll: all events from inner observables in one stream

concatAll: subscribe to one inner observable at a time

switchAll: unsubscribe previous inner when next arrives

And a fourth one...

@synalx
Projection operators
Transform Observable<Observable<T>> to Observable<T>

mergeAll: all events from inner observables in one stream

concatAll: subscribe to one inner observable at a time

switchAll: unsubscribe previous inner when next arrives

exhaust: ignore new inners while previous is still emitting

@synalx
map → concat

const source$ = Observable.from([1, 2, 3]);

const inOrder$ = source$.pipe(


map(id => http.get<Data>(`/data?id=${id}`)),
concatAll()
);

@synalx
map → concat

const source$ = Observable.from([1, 2, 3]);

const inOrder$ = source$.pipe(


concatMap(value => http.get<Data>(`/data/${value}`))
);

@synalx
When to use each operator?

concatMap: should be your default choice

switchMap: when only the latest source value is important

mergeMap: to combine multiple sources (rare)

exhaustMap: very, very rare

@synalx
Result selectors

const second$ = http


.get(‘/first’)
.pipe(
concatMap(first => http.get(first.url))
);

// But, no access to first result!

@synalx
Result selectors

const both$ = http


.get(‘/first’)
.pipe(
concatMap(
first => http.get(first.url),
(first, second) => ({first, second})
)
);

@synalx
Combination operators

These are for joining the values of multiple input streams

combineLatest

withLatestFrom

@synalx
combineLatest

const combined$ = combineLatest(


stream1$,
stream2$,
(value1, value2) => ({value1, value2})
);

@synalx
withLatestFrom

const data$: firebase.object(‘/fb-data’).valueChanges();

// Want to auto-save the form, but the id isn’t known.


form
.valueChanges
.pipe(
withLatestFrom(data$, (value, data) => ({...value, data}))
)
.subscribe(...);

@synalx
1 2 3

Mental Model Operators Best Practices

Patterns and Anti-patterns in Real Apps

@synalx
Anti-pattern: nested subscriptions
subscribe() should happen as close to the UI as possible

And only once.

Use projection operators instead of nesting subscriptions

@synalx
Anti-pattern: nested subscriptions

http.get(‘/first’).subscribe(first => {
this.first = first;

http.get(first.url).subscribe(second => {
this.second = second;
});
});

@synalx
Instead, use projection operators

http.get(‘/first’).pipe(
concatMap(
first => http.get(first.url),
(first, second) => ({first, second})
)
).subscribe(({first, second}) => {
this.first = first;
this.second = second;
});

@synalx
Pattern: subscription management
You should always unsubscribe

Memory leaks, correctness

Don’t manually track subscriptions

Make an onDestroy$ Observable, use takeUntil operator

@synalx
First, create an onDestroy$ stream

class MyComponent implements OnDestroy {


onDestroy$ = new Subject<void>();

ngOnDestroy(): void {
this.onDestroy$.next();
}
}

@synalx
As the last step in every sequence, use takeUntil

class MyComponent implements OnDestroy {


random(): void {
this.http.post(‘/data’, data)
.pipe(
map(...),
switchMap(...),
takeUntil(this.onDestroy$)
)
.subscribe();
}
} @synalx
Anti-pattern: Avoid side effects in operators

class ApiService {
data: Data;

getData(url: string): Observable<Data> {


return this.http.get(url).pipe(map(data => {
this.data = data;
return data;
}));
}
}
@synalx
Anti-pattern: Avoid side effects in operators
Introduces unexpected statefulness

Unwanted cross-talk between multiple subscriptions

Harder to reason about

Instead:

Stateless services

NGRX
@synalx
Pattern: custom operators
Operator is a function which transforms a given
Observable → Observable

Write your own!

@synalx
Pattern: custom operators

type Operator<I, O> = (in: Observable<I>) => Observable<O>;

function countChars(): Operator<string, number> {


return (obs: Observable<string>) => {
return obs.pipe(map(str => str.length));
};
}

const charCount$ = strings$.pipe(countChars());

@synalx
1 2 3

Mental Model Operators Best Practices

Observables as async functions

Explored common operators

Looked at best practices

@synalx
Thank you!
Follow me on twitter: @synalx

@synalx

You might also like