Rx in Angular
Chris Trześniewski
Developer 🥑 Advocate
& Senior Frontend Developer @Scalac
Introduction
- ... Promise on steroids
- ... request ==> response ==> done
More than ...
- ... work with multiple data
- ... model complex interactions declaratively
It allows to ...
Agenda
RxJS - crash course
Reactivity in @Components
Handling @Outputs with RxJS
Reactive @Inputs
RxJS
Crash course
Producer & Observer
Button
Component
?
Observable
Button
Component
Observable
Observable
Observable
Producer <=> Observer
- source of data arriving over time
- ie. DOM events, web socket, HTTP request...
Producer
- function or object that reacts to data
- ie. component, event handler ...
Observer
Observable
Producer <=> Observer
- function that ties Producer and Observer
- lazy - doesn't evaluate nor attaches listeners unless subscribed
- need to be unsubscribed
Observable
Observable
example
const myButton = document.querySelector('#myButton');
const click$ = Rx.Observable.fromEvent(myButton, 'click');
click$.subscribe(event => console.log(event));
const mouseMove$ = fromEvent(window, 'mousemove');
mouseMove$.subscribe((event: MouseEvent) => {
console.log(`Mouse: (${event.clientX}, ${event.clientY})`)
});
Observable
example
const mouseSub = mouseMove$.subscribe((event: MouseEvent) => {
console.log(`Mouse: (${event.clientX}, ${event.clientY})`)
});
const clicksSub = click$.subscribe(() => {
mouseSub.unsubscribe();
clicksSub.unsubscribe();
});
! But we must remember to unsubscribe !
Observable
operators
-
operators are what gives "super powers" to Observables
-
they allow us to manipulate the stream of data
-
some are similar to what you know from arrays - map, filter, reduce
-
they can be chained
Observable
operators
interval(1000).pipe(
map(x => x * x)
).subscribe(x => console.log(x)); // 1 4 9 ..
interval(1000).pipe(
filter(x => x % 2 === 0)
).subscribe(x => console.log(x)); // 2 4 6 ...
interval(1000).pipe(
filter(x => x % 2 === 0),
map(x => x * x)
).subscribe(x => console.log(x)); // 4 16 36 ...
Tasks
-
create a stream of clicks on a window
-
map it to x coordinate of a click
-
emit only clicks that have x coordinate greater than 100 and lower than 500
-
create an Observable of triple clicks that returns the coordinates of the click in following format: (x, y) *
* UIEvent.detail
Observable
higher order operators
- allow us to change / switch to another observable
- there are different strategies of handling inner observables
- mergeMap, concatMap, switchMap, exhaustMap
Observable
higher order operators
const myButton = document.querySelector('#myButton');
const click$ = Rx.Observable.fromEvent(myButton, 'click');
const mergeMapped$ = click$.pipe(
mergeMap(() => interval(1000))
);
mergeMapped$.subscribe(value => console.log(value));
Tasks
-
create a timer (counting 1 to 5) and display its value in a div
-
timer, take operators
-
on button click start a timer created above
-
switchMap vs exhaustMap - play with it to see what's a difference
Reactive components
Reactive components
- We can use Observables in component's properties
- There are two approaches to displaying values from an Observable
- subscribe vs async pipe
Reactive components
.subscribe()
Then we can just use `componentValue` inside our component's template
this.ourObservable$.subscribe(value => {
this.componentValue = value;
});
@Component({
template: `
<div>{{ componentValue }}</div>
`
})
Reactive components
.subscribe()
! But we must remember to unsubscribe !
subscription = new Subscription()
ngOnInit() {
this.subscription.add(
this.ourObservable$.subscribe(value => {
this.componentValue = value;
});
)
}
ngOnDestroy() {
this.subsctiption.unsubscribe();
}
Reactive components
.subscribe()
! But we must remember to unsubscribe !
destroy$ = new Subject()
ngOnInit() {
this.ourObservable$
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.componentValue = value;
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
Reactive components
async pipe
Then we can use async pipe to unwrap the Observable
ourObservable$ = interval(1000);
@Component({
template: `
<div>{{ ourObservable$ | async }}</div>
`
})
Reactive components
.subscribe()
No need for manual subscription management
-
async pipe “unwraps” the observable and returns a latest value
-
it takes care of unsubscribing by itself when component is destroyed
Tasks
-
create an Observable which emit a current time (ie. every 500ms)
-
display then current time provided by this Observable using both methods (subscribe & async pipe)
Handling events with RxJS
Events as Observable
- Events in Angular can be easily transformed into Observables
- Allows for more control of the application flow
Events as Observable
@Component({
template: `
<input #myInput>
<div>{{ inputChanges$ | async }}</div>
`
})
inputChanges$: Observable<string>;
@ViewChild('myInput', { static: true }) myInput: ElementRef;
ngOnInit(): void {
this.inputChanges$ =
fromEvent(this.myInput.nativeElement, 'input').pipe(
map((event: any) => event.target.value)
);
}
Events as Observable
Subject
- Special type of Observable which shares single execution among multiple observers (multicasts)
- It's both an Observable (you can subscribe to it)
- and an Observer (provides next, error, complete methods)
Events as Observable
Subject
@Component({
template: `
<input
#myInput
(change)=“inputValuesSubject.next(myInput.value)"
>
<div>{{ inputValuesSubject | async }}</div>
`
})
class MyComponent {
inputValuesSubject = new Subject<string>();
}
Events as Observable
- Now we can use our event streams anyway we want
- Ie. we could combine to streams to calculate a new value
Events as Observable
@Component({
template: `
<input
#firstArgument
(change)="firstArgSub.next(firstArgument.value)">
<input
#secondArgument
(change)="secondArgSub.next(secondArgument.value)">
<div>Sum {{ sum$ | async }}</div>
`
})
export class MySimpleCalculatorComponent {
firstArgSub = new Subject<string>();
firstArgSub = new Subject<string>();
sum$ = combineLatest(
[this.firstArgumentSubject, this.secondArgumentSubject]
).pipe(
map(([first, second]) => +first + +second)
);
}
Task
- Create a search input
- Using Rick and Morty API* load characters filtered by name (max 10)
- Display found character's names in a list below the search
Bonus tasks:
- Prevent calling API to often
- Add second filter parameter ie. status
Reactive Inputs
Reactive Inputs
- So far we've got reactive properties and events
- But what about @Inputs?
Reactive Inputs
- via ngOnChanges method
- via property setters
Getting input changes:
// the only way to get notified
// of changes to this prop is `ngOnChanges`
@Input() firstProperty: string;
@Input() set secondProperty(secondProperty: string) {
// set some internal properties
// or do some calculation based on the property
}
Reactive Inputs
- via ngOnChanges method
- via property setters
The latter is a good candidate for our reactive inputs
// we use BehaviorSubject
// to always get the current value in new subscribers
reactiveProperty$ = new BehaviorSubject<string>(null);
@Input() set reactiveProperty(value: string) {
reactiveProperty$.next(value);
}
Reactive Inputs
- now we can use our reactive property as we did with all Observables before
But how does it benefit us?
- all the power of Observables and operators
- more declarative dependencies
Reactive Inputs
reactivePropertyCharacters$ = this.reactiveProperty$.pipe(
map(value => value.split('')),
map(characters => characters
.filter(char => !!char.match(/[a-zA-Z]/))
.map(char => char.toUpperCase())
)
)
computedProperty$ = combineLatest(
[propertyA$, propertyB$]
).pipe(
map(([a, b]) => a * b)
)
Property dependencies visible right away
Task
-
inside a component create an input text
-
use value typed inside the filter input to display only the recipes that contain given ingredients
Bonus tasks:
-
allow for entering multiple ingredients (comma separated)
-
add a checkbox that determines if a recipe should contain all ingredients or just any of them
-
create a component with an array of recipes as an input
interface Recipe {
name: string;
ingredients: string[]
}
Thank you!
Any questions?
Rx in Angular - DevMeeting 2020.09.12
By Chris Trześniewski
Rx in Angular - DevMeeting 2020.09.12
- 292