Dive into RxJSJeremy Fairbank@elpapapollo / jfairbankObservables
View Slide
Software is broken.We are here to fix it.Say [email protected]
Async
Async APICallback
CallbackfetchUserById(1, function(err, user) {if (err) {console.error('Could not retrieve user');} else {console.log(user);}});
});});});});});});CallbackCallbackCallbackCallbackCallbackCallback
function fetchCustomerNameForOrder(orderId, done, fail) {fetchOrder(orderId, function(err, order) {if (err) {logError(err);fail(err);} else {fetchCustomer(order.customerId,function(err, customer) {if (err) {logError(err);fail(err);} else {done(customer.name);}});}});}
Async API ?
Promisefunction fetchCustomerNameForOrder(orderId) {return fetchOrder(orderId).then(order => fetchCustomer(order.customerId)).then(customer => customer.name);}
.then(...).then(...).then(...).then(...).then(...).then(...).then(...).then(...)
ErrorErrorErrorError
function fetchCustomerNameForOrder(orderId) {return fetchOrder(orderId).then(order => fetchCustomer(order.customerId)).then(customer => customer.name).catch(err => {logError(err);throw err;});}
Getting there…
• More readable andmaintainable async code• Better error handling• More declarative andversatile syntax• Capable of handling events,streams, and HTTP
RxJSObservables
Arrays[ 1, 2, 3, 4, 5 ]Sequences in space
ObservablesSequences in time1 2 3 4 5
Reactive
Reactive3
const { Observable } = require('rxjs');const source = Observable.of(1, 2, 3);source.subscribe(x => console.log(x));// 1// 2// 3
Observable.of(1, 2, 3).subscribe(x => console.log(x));console.logsubscribe
Observable.of(1, 2, 3).subscribe(x => console.log(x));console.logsubscribe1
Observable.of(1, 2, 3).subscribe(x => console.log(x));console.logsubscribe2
Observable.of(1, 2, 3).subscribe(x => console.log(x));console.logsubscribe3
DeclarativeTransformationOperate on events
Observable.of(1, 2, 3).map(n => n * 2).subscribe(x => console.log(x));// 2// 4// 6
Observable.of(1, 2, 3)console.logn * 2map subscribe
Observable.of(1, 2, 3)console.logn * 2map subscribe1
Observable.of(1, 2, 3)console.logn * 2map subscribe2
Observable.of(1, 2, 3)console.logn * 2map subscribe4
Observable.of(1, 2, 3)console.logn * 2map subscribe3
Observable.of(1, 2, 3)console.logn * 2map subscribe6
Lazy TransformationDo only as much work asneeded
Observable.range(1, 100).map(n => n * 2).filter(n => n > 4).take(2).subscribe(x => console.log(x));// 6// 8
console.logn * 2map subscriben > 4Observable.range(1, 100)filter2take
console.logn * 2map subscriben > 41Observable.range(1, 100)filter2take
console.logn * 2map subscriben > 42Observable.range(1, 100)filter2take
console.logn * 2map subscriben > 42Observable.range(1, 100)filter2take×
console.logn * 2map subscriben > 44Observable.range(1, 100)filter2take
console.logn * 2map subscriben > 4Observable.range(1, 100)filter2take4
console.logn * 2map subscriben > 4Observable.range(1, 100)filter2take4×
console.logn * 2map subscriben > 43Observable.range(1, 100)filter2take
console.logn * 2map subscriben > 46Observable.range(1, 100)filter2take
console.logn * 2map subscriben > 4Observable.range(1, 100)filter2take6
✓console.logn * 2map subscriben > 4Observable.range(1, 100)filter2take6
console.logn * 2map subscriben > 4Observable.range(1, 100)filter1take6
console.logn * 2map subscriben > 4Observable.range(1, 100)filter1take
console.logn * 2map subscriben > 44Observable.range(1, 100)filter1take
console.logn * 2map subscriben > 48Observable.range(1, 100)filter1take
console.logn * 2map subscriben > 4Observable.range(1, 100)filter1take8
✓console.logn * 2map subscriben > 4Observable.range(1, 100)filter1take8
console.logn * 2map subscriben > 4Observable.range(1, 100)filter0take8
DOM Events
let counter = 0;function updateCounter(n) {counter += n;counterEl.innerHTML = counter;}incrementBtn.addEventListener('click', () => {updateCounter(1);});decrementBtn.addEventListener('click', () => {updateCounter(-1);});
Observable.fromEvent(incrementBtn, 'click').mapTo(1).scan((acc, curr) => acc + curr, 0).subscribe((counter) => {counterEl.innerHTML = counter;});
.scan((acc, curr) => acc + curr, 0)Accumulatedcounter value
.scan((acc, curr) => acc + curr, 0)Current eventvalue (1)
.scan((acc, curr) => acc + curr, 0)Return new accumulatedcounter value
.scan((acc, curr) => acc + curr, 0)Initial countervalue
counterEl.innerHTML =counteracc + curr1mapTo subscribe0scan
acc + curr1mapTo subscribeescan0counterEl.innerHTML =counter
1mapTo subscribeescanacc + curr0counterEl.innerHTML =counter
1mapTo subscribe1scanacc + curr0counterEl.innerHTML =counter
1mapTo subscribescan1acc + curr0counterEl.innerHTML =counter
1mapTo subscribescan1acc + curr1Counter valuecounterEl.innerHTML =counter
1mapTo subscribescan1acc + curr1counterEl.innerHTML =counter
acc + curr1mapTo subscribescan1counterEl.innerHTML =counter
acc + curr1mapTo subscribeescan1counterEl.innerHTML =counter
1mapTo subscribeescanacc + curr1counterEl.innerHTML =counter
1mapTo subscribe1scanacc + curr1counterEl.innerHTML =counter
1mapTo subscribescan2acc + curr2Counter valuecounterEl.innerHTML =counter
1mapTo subscribescan2acc + curr2counterEl.innerHTML =counter
const source = Observable.merge(Observable.fromEvent(incrementBtn, 'click').mapTo(1),Observable.fromEvent(decrementBtn, 'click').mapTo(-1),);source.scan((acc, curr) => acc + curr, 0).subscribe((counter) => {counterEl.innerHTML = counter;});
acc + curr1mapTosubscribe0scan-1mapToIncrementDecrementcounterEl.innerHTML =counter
acc + curr1mapTosubscribe0scan-1mapToIncrementDecrementecounterEl.innerHTML =counter
acc + curr1mapTosubscribe0scan-1mapToIncrementDecrement1counterEl.innerHTML =counter
acc + curr1mapTosubscribe1scan-1mapToIncrementDecrement1counterEl.innerHTML =counter
acc + curr1mapTosubscribe1scan-1mapToIncrementDecrementcounterEl.innerHTML =counter
acc + curr1mapTosubscribe1scan-1mapToIncrementDecrementecounterEl.innerHTML =counter
acc + curr1mapTosubscribe1scan-1mapToIncrementDecrement-1counterEl.innerHTML =counter
acc + curr1mapTosubscribe0scan-1mapToIncrementDecrement0counterEl.innerHTML =counter
Async HTTP
PromisefetchOrders().then((orders) => {orders.forEach((order) => {console.log(order);});});
ObservablefetchOrders().subscribe((orders) => {orders.forEach((order) => {console.log(order);});});
Why Observables?
PromisefetchOrders().then(orders => orders.filter(order => order.customerName === 'Tucker')).then(orders => orders.map(order => order.id)).then((orderIds) => {orderIds.forEach(id => console.log(id));});
fetchOrders().mergeAll().filter(order => order.customerName === 'Tucker').map(order => order.id).subscribe(id => console.log(id));Observable
Cancellation
const promise = fetchOrders().then((orders) => {orders.forEach((order) => {console.log(order);});});promise.cancel();Promise
const promise = fetchOrders().then((orders) => {orders.forEach((order) => {console.log(order);});});promise.cancel();Promise×
const subscription = fetchOrders().subscribe((orders) => {orders.forEach((order) => {console.log(order);});});subscription.unsubscribe();Observable
const subscription = fetchOrders().subscribe((orders) => {orders.forEach((order) => {console.log(order);});});subscription.unsubscribe();ObservableCancel request
LazySubscriptions
const p1 = fetchOrders();const p2 = fetchOrders();const p3 = fetchOrders();Promises
const p1 = fetchOrders();const p2 = fetchOrders();const p3 = fetchOrders();PromisesImmediate
Observablesconst o1 = fetchOrders();const o2 = fetchOrders();const o3 = fetchOrders();o1.subscribe();o2.subscribe();o3.subscribe();
ObservablesLazyconst o1 = fetchOrders();const o2 = fetchOrders();const o3 = fetchOrders();o1.subscribe();o2.subscribe();o3.subscribe();
ObservablesLazyconst o1 = fetchOrders();const o2 = fetchOrders();const o3 = fetchOrders();o1.subscribe();o2.subscribe();o3.subscribe();Issue Request
const o1 = fetchOrders();o1.subscribe();o1.subscribe();o1.subscribe();
const o1 = fetchOrders();o1.subscribe();o1.subscribe();o1.subscribe();Pure,shareablevalue
const o1 = fetchOrders();o1.subscribe();o1.subscribe();o1.subscribe();Issue new requestwith sameobservablePure,shareablevalue
Create HTTPRequests
function fetchOrders() {return Observable.ajax.get('/orders');}Built-in AJAX
function fetchOrders() {return Observable.create((subscriber) => {fetchOrdersFromDb((orders) => {subscriber.next(orders);subscriber.complete();});});}Custom ObservableCreation
function fetchOrders() {return Observable.create((subscriber) => {fetchOrdersFromDb((orders) => {subscriber.next(orders);subscriber.complete();});});}Custom ObservableCreation?
Observable.of(1, 2, 3).subscribe(x => console.log(x));
Observable.of(1, 2, 3).subscribe(x => console.log(x));Subscriber(or Observer)
Observable.of(1, 2, 3).subscribe({next: x => console.log(x),});Subscriber Object
Observable.of(1, 2, 3).subscribe({next: x => console.log(x),complete: () => console.log('Done!'),});// 1// 2// 3// Done!
function fetchOrders() {return Observable.create((subscriber) => {fetchOrdersFromDb((orders) => {subscriber.next(orders);subscriber.complete();});});}fetchOrders().mergeAll().subscribe({next: x => console.log(x),complete: () => console.log('Done!'),});
Error Handling
const source = Observable.create((subscriber) => {subscriber.next(1);subscriber.error(new Error('Uh oh'));subscriber.next(2);});source.subscribe({next: x => console.log(x),complete: () => console.log('Done!'),error: e => console.error(e),});// 1// Error: Uh oh
const source = Observable.create((subscriber) => {subscriber.next(1);subscriber.error(new Error('Uh oh'));subscriber.next(2);});source.subscribe({next: x => console.log(x),complete: () => console.log('Done!'),error: e => console.error(e),});// 1// Error: Uh ohNever called
const source = Observable.create((subscriber) => {subscriber.next(1);subscriber.error(new Error('Uh oh'));subscriber.next(2);});source.subscribe(x => console.log(x));No error handler, sowhat happens?
ErrorErrorErrorErrorPromises
const source = Observable.create((subscriber) => {subscriber.next(1);subscriber.error(new Error('Uh oh'));subscriber.next(2);});source.subscribe(x => console.log(x));No swallowed errors!
fetchOrders().catch((e) => {logError(e);return legacyFetchOrders().catch((e2) => {logError(e2);return Observable.of([]);})}).subscribe(x => console.log(x));Catching
console.logcatch subscribelegacyFetchOrders()fetchOrders()catchObservable.of([])
console.logcatch subscribelegacyFetchOrders()fetchOrders()catchObservable.of([])✓
console.logcatch subscribelegacyFetchOrders()fetchOrders()catchObservable.of([])×
console.logcatch subscribelegacyFetchOrders()fetchOrders()catchObservable.of([])×✓
console.logcatch subscribelegacyFetchOrders()fetchOrders()catchObservable.of([])× ✓
console.logcatch subscribelegacyFetchOrders()fetchOrders()catchObservable.of([])××
console.logcatch subscribelegacyFetchOrders()fetchOrders()catchObservable.of([])××[]
Hot Coldvs.
Observablecreates the sourceCold
Best for one-offunique requests.Cold
function fetchOrders() {return Observable.create((subscriber) => {fetchOrdersFromDb((orders) => {subscriber.next(orders);subscriber.complete();});});}
function fetchOrders() {return Observable.create((subscriber) => {fetchOrdersFromDb((orders) => {subscriber.next(orders);subscriber.complete();});});}Resource requested/createdat subscription time
WebSockets
function ordersStream() {return Observable.create((subscriber) => {const url = 'ws://example.com/orders';const socket = new WebSocket(url);socket.addEventListener('message', (data) => {subscriber.next(data);});});}
const stream = ordersStream();stream.subscribe(x => console.log(x));
subscriber.next(data)WebSocketSubscribedObservableconsole.logstream.subscribe(x => console.log(x));
subscriber.next(data)...WebSocketSubscribedObservableconsole.logstream.subscribe(x => console.log(x));
.subscribe(...)
.subscribe(...) .subscribe(...)
.subscribe(...).subscribe(...).subscribe(...)
.subscribe(...).subscribe(...).subscribe(...).subscribe(...)
.subscribe(...).subscribe(...).subscribe(...).subscribe(...).subscribe(...)
.subscribe(...).subscribe(...).subscribe(...).subscribe(...).subscribe(...).subscribe(...)
.subscribe(...).subscribe(...).subscribe(...).subscribe(...).subscribe(...).subscribe(...).subscribe(...)
.subscribe(...).subscribe(...).subscribe(...).subscribe(...).subscribe(...).subscribe(...).subscribe(...).subscribe(...)
HotObservablecloses over the source
HotBest for multicastingand sharing resources.
const url = 'ws://example.com/orders';const socket = new WebSocket(url);function ordersStream() {return Observable.create((subscriber) => {socket.addEventListener('message', (data) => {subscriber.next(data);});});}
const url = 'ws://example.com/orders';const socket = new WebSocket(url);function ordersStream() {return Observable.create((subscriber) => {socket.addEventListener('message', (data) => {subscriber.next(data);});});}Resource createdoutside subscription
const url = 'ws://example.com/orders';const socket = new WebSocket(url);function ordersStream() {return Observable.create((subscriber) => {socket.addEventListener('message', (data) => {subscriber.next(data);});});} Close over existingresource when subscribing
const stream = ordersStream();const sub1 = stream.subscribe(x => console.log(x));const sub2 = stream.subscribe(x => console.log(x));
Shared stream of dataconst stream = ordersStream();const sub1 = stream.subscribe(x => console.log(x));const sub2 = stream.subscribe(x => console.log(x));
subscriber.next(event.data)WebSocketSubscribedObservableconsole.logstream.subscribe(x => console.log(x))
subscriber.next(event.data)...WebSocketSubscribedObservableconsole.logstream.subscribe(x => console.log(x))
subscriber.next(event.data)WebSocketSubscribedObservableconsole.logNew SubscribedObservableconsole.logstream.subscribe(...)stream.subscribe(...)
subscriber.next(event.data)WebSocketSubscribedObservableconsole.log...New SubscribedObservableconsole.logstream.subscribe(...)stream.subscribe(...)
subscriber.next(event.data)WebSocketSubscribedObservableconsole.log...New SubscribedObservableconsole.log...stream.subscribe(...)stream.subscribe(...)
subscriber.next(event.data)...WebSocketSubscribedObservableconsole.log...New SubscribedObservableconsole.logstream.subscribe(...)stream.subscribe(...)
function ordersStream() {return Observable.create((subscriber) => {const url = 'ws://example.com/orders';const socket = new WebSocket(url);socket.addEventListener('message', (data) => {subscriber.next(data);});}).share();}Share It
Shared stream of data tooconst stream = ordersStream();const sub1 = stream.subscribe(x => console.log(x));const sub2 = stream.subscribe(x => console.log(x));
Clean Up
function ordersStream() {return Observable.create((subscriber) => {const url = 'ws://example.com/orders';const socket = new WebSocket(url);socket.addEventListener('message', (data) => {subscriber.next(data);});return () => {socket.close();};}).share();}
function ordersStream() {return Observable.create((subscriber) => {const url = 'ws://example.com/orders';const socket = new WebSocket(url);socket.addEventListener('message', (data) => {subscriber.next(data);});return () => {socket.close();};}).share();}Called when all subscriptionsunsubscribe
function ordersStream() {return Observable.create((subscriber) => {const url = 'ws://example.com/orders';const socket = new WebSocket(url);socket.addEventListener('message', (data) => {subscriber.next(data);});return () => {socket.close();};}).share();}Close socket,deallocate resources,etc.
fetchOrders().mergeAll().filter(order => order.customerName === 'Tucker').map(order => order.id).subscribe(id => console.log(id));Recall
fetchOrders().mergeAll().filter(order => order.customerName === 'Tucker').map(order => order.id).subscribe(id => console.log(id));?Recall
Observable.of([1, 2, 3]).subscribe(x => console.log(x));// [ 1, 2, 3 ]
Observable.of([1, 2, 3]).mergeAll().subscribe(x => console.log(x));// 1// 2// 3
Observable.of([1, 2, 3]).mergeAll().subscribe(x => console.log(x));// 1// 2// 3Flatten
subscribemergeAllconsole.log[1, 2, 3]
subscribemergeAllconsole.log[2, 3] 1
subscribemergeAllconsole.log[3] 2
subscribemergeAllconsole.log3
Higher OrderObservables
Observable.of(1, 2, 3).map(n => n * 2).subscribe(x => console.log(x));// 2// 4// 6Delay?
Observable.of(1, 2, 3).map(n => Observable.of(n * 2)).subscribe(x => console.log(x));// ScalarObservable { value: 2 }// ScalarObservable { value: 4 }// ScalarObservable { value: 6 }
Observable.of(1, 2, 3).map(n => Observable.of(n * 2)).mergeAll().subscribe(x => console.log(x));// 2// 4// 6
Observable.of(1, 2, 3).mergeMap(n => Observable.of(n * 2)).subscribe(x => console.log(x));// 2// 4// 6
subscribemergeMapconsole.logObservable.of(n * 2)
subscribemergeMapconsole.logObservable.of(n * 2)1
subscribemergeMapconsole.logObservable.of(n * 2)Observable.of(2)subscribe2
Observable.of(1).delay(1000).subscribe(x => console.log(x))// // 1
Observable.of(1, 2, 3).mergeMap(n => (Observable.of(n * 2).delay(1000))).subscribe(x => console.log(x));// // 2// 4// 6
Observable.of(1, 2, 3).mergeMap(n => (Observable.of(n * 2).delay(1000))).subscribe(x => console.log(x));// // 2// 4// 6?
Concurrency
subscribemergeMapconsole.logObservable.of(n * 2)2
subscribemergeMapconsole.logObservable.of(n * 2)22
subscribemergeMapconsole.logObservable.of(n * 2)24
subscribemergeMapconsole.logObservable.of(n * 2)324
subscribemergeMapconsole.logObservable.of(n * 2)246
subscribemergeMapconsole.logObservable.of(n * 2)462
subscribemergeMapconsole.logObservable.of(n * 2)64
subscribemergeMapconsole.logObservable.of(n * 2)6
Observable.of(1, 2, 3).mergeMap(n => (Observable.of(n * 2).delay(1000)), 1).subscribe(x => console.log(x));// // 2// // 4// // 6
Observable.of(1, 2, 3).concatMap(n => (Observable.of(n * 2).delay(1000))).subscribe(x => console.log(x));// // 2// // 4// // 6
subscribeconcatMapconsole.logObservable.of(n * 2)
subscribeconsole.logObservable.of(n * 2)1concatMap
subscribeconsole.logObservable.of(n * 2)2concatMap
subscribeconsole.logObservable.of(n * 2)4concatMap
subscribeconsole.logObservable.of(n * 2)3concatMap
subscribeconsole.logObservable.of(n * 2)6concatMap
Rate Limiting
Observable.of(1, 2, 3).concatMap((id) => {const url = `/orders/${id}`;return Observable.ajax.get(url).delay(1000);}).pluck('response').bufferCount(3).subscribe(x => console.log(x));// [ { id: '1', name: 'Order 1' },// { id: '2', name: 'Order 2' },// { id: '3', name: 'Order 3' } ]
Observable.ajax.get(url)subscribeconsole.logpluck bufferCount'response' 3concatMap
Observable.ajax.get(url)subscribeconcatMapconsole.logpluck bufferCount'response' 31
Observable.ajax.get(url)subscribeconcatMapconsole.logpluck bufferCount'response' 3
Observable.ajax.get(url)subscribeconcatMapconsole.logpluck bufferCount'response' 3RRate limit next request
Observable.ajax.get(url)subscribeconcatMapconsole.logpluck bufferCount'response' 3R
Observable.ajax.get(url)subscribeconcatMapconsole.logpluck bufferCount'response' 3O1
Observable.ajax.get(url)subscribeconcatMapconsole.logpluck bufferCount'response' 2O1
Observable.ajax.get(url)subscribeconcatMapconsole.logpluck bufferCount'response' 2O1+
Observable.ajax.get(url)subscribeconcatMapconsole.logpluck bufferCount'response' 1O1O2
Observable.ajax.get(url)subscribeconcatMapconsole.logpluck bufferCount'response' 1O1O2+
Observable.ajax.get(url)subscribeconcatMapconsole.logpluck bufferCount'response' 0O1O2O3
Observable.ajax.get(url)subscribeconcatMapconsole.log[…]pluck bufferCount'response' 0
Sequential vs.Parallel
const promise = Promise.all([fetchOrder(1),fetchOrder(2),fetchOrder(3),]);promise.then(x => console.log(x));// [ { id: '1', name: 'Order 1' },// { id: '2', name: 'Order 2' },// { id: '3', name: 'Order 3' } ]
const source = Observable.forkJoin(fetchOrder(1),fetchOrder(2),fetchOrder(3));source.subscribe(x => console.log(x));// [ { response: { id: 1, name: 'Order 1' } },// { response: { id: 2, name: 'Order 2' } },// { response: { id: 3, name: 'Order 3' } } ]
const source = Observable.forkJoin(fetchOrder(1),fetchOrder(2),fetchOrder(3));source.subscribe(x => console.log(x));// [ { response: { id: 1, name: 'Order 1' } },// { response: { id: 2, name: 'Order 2' } },// { response: { id: 3, name: 'Order 3' } } ]Observable orPromise
fetchOrder(1)fetchOrder(2)fetchOrder(3)forkJoin subscribeconsole.log
fetchOrder(1)fetchOrder(2)fetchOrder(3)forkJoin subscribeconsole.logR1
fetchOrder(1)fetchOrder(2)fetchOrder(3)forkJoinR1subscribeconsole.log
fetchOrder(1)fetchOrder(2)fetchOrder(3)forkJoinR1subscribeconsole.logR3
fetchOrder(1)fetchOrder(2)fetchOrder(3)forkJoinR1R3subscribeconsole.log
fetchOrder(1)fetchOrder(2)fetchOrder(3)forkJoinR1R3subscribeconsole.logR2
fetchOrder(1)fetchOrder(2)fetchOrder(3)forkJoinR1R2R3subscribeconsole.log
fetchOrder(1)fetchOrder(2)fetchOrder(3)forkJoin subscribeconsole.logR1R2R3
Tip of the iceberg
• Declarative, lazyoperations• Expressive eventmanagement• No more error swallowing• Rate limiting andconcurrent processing
• Declarative, lazyoperations• Expressive eventmanagement• No more error swallowing• Rate limiting andconcurrent processing✓✓✓✓
github.com/ReactiveX/rxjsreactivex.io/rxjsRxJS
ObservablesECMAScript Proposal: github.com/tc39/proposal-observableAnother spec implementation: github.com/zenparsing/zen-observable
Thanks!Jeremy Fairbank@elpapapollo / jfairbankSlides: bit.ly/rxjs-connect