by Gion Kunz
Founder, UI Developer, UX Enthusiast at syncrea GmbH
Item
Error
Completed
import {from} from 'rxjs';
import {filter, map} from 'rxjs/operators';
from([1, 2, 3])
.pipe(
map(num => num * num),
filter(num => num > 3)
)
.subscribe(item => console.log(item));
Items are emitted through a stream of items
Each item flows through the stream individually
The subscription will cause the stream start flowing
import {from, timer} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';
from([1, 2, 3])
.pipe(
switchMap(num => timer(1000).pipe(map(() => num * num)))
)
.subscribe(item => console.log(item));
Route Parameters
HttpClient Service
Reactive Forms
Component Output (EventEmitter)
@Component({
selector: 'my-component',
template: '{{data}}'
})
export class MyComponent {
asyncSubscription: Subscription;
data: number;
constructor() {
this.asyncSubscription = of(1)
.pipe(delay(2000))
.subscribe(data => this.data = data);
}
ngOnDestroy() {
this.asyncSubscription.unsubscribe();
}
}
Subscriptions should be cleaned up using unsubscribe()
Within components, the ngOnDestroy lifecycle hook is a good spot for this task
Feels cumbersome and inconvenient
@Component({
selector: 'my-component',
template: '{{asyncData | async}}'
})
export class MyComponent {
asyncData: Observable<number>;
constructor() {
this.asyncData = of(1)
.pipe(delay(2000));
}
}
The async Pipe is creating the subscription in the view
Always the latest item in the stream will be rendered
When the component view is destroyed, the subscriptions is automatically unsubscribed
Client
Server
update todo
update todo
reload data
show updates
Delayed Feedback
Project
Server
Todos
fetch todos
create todo
Out of Sync
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@Injectable()
export class TodoService {
todos = new BehaviorSubject<Todos[]>([]);
constructor(private http: HttpClient) {
this.loadTodos();
}
loadTodos() {
this.http
.get<Todos[]>('http://localhost/api/todos')
.subscribe(todos => this.todos.next(todos));
}
}
addTodo(title: string) {
this.todos.next([{
title,
done: false,
order: 1
}, ...this.todos.getValue()]);
this.http.post('http://localhost/api/todos', {
title,
done: false,
order: 1
})
.subscribe((todos) => this.todos.next(todos));
}
Optimistic Update
Server Sync
client
server
persistent
persistent
transient
transient
synchronization
database, storage ...
optimistic updates, performance ...
session data ...
interaction state, UI state, URL state ...
Highly Persistent
Highly Transient
Database
Storage
UI State
URL State
Persistence State
Session
Cookies
Local storage
Interaction State
Action
Dispatcher
Store
View
Elm
2012
Flux
2014
Redux
2015
ngrx store
2016
RxJS
2010
Action
Reducer
Store
View
1
Dispatch action
2
Current state and action
3
New state
4
Update View
export class AddTodoAction implements Action {
readonly type = 'AddTodoAction';
constructor(public readonly todo: ServerTodoItem) {}
}
export class UpdateTodoAction implements Action {
readonly type = 'UpdateTodoAction';
constructor(public readonly todoId: number,
public readonly data: any) {}
}
store.dispatch(
new AddTodoAction({
title: 'Todo nr 1',
done: false,
order: 1
})
);
export interface Reducer<S, A extends Action> {
(state: S, action: A): S
}
export interface CounterState {
count: number;
}
const initialState: CounterState = {count: 0};
export function reducer(state: CounterState = initialState,
action: Action): CounterState {
switch (action.type) {
case 'IncrementCountAction':
return {
...state,
count: state.count + 1
};
default: return state;
}
}
const count: Observable<number> =
store.select(state => state.count);
export interface CounterState {
count: number;
}
const initialState: CounterState = {count: 0};
export function reducer(state: CounterState = initialState,
action: Action): CounterState {
switch (action.type) {
case 'IncrementCountAction':
return {
...state,
count: state.count + 1
};
default: return state;
}
}
export const store = createStore(reducer, initialState);
import { Component } from '@angular/core';
import { store } from '../counter.state';
@Component({
selector: 'app',
template: `
<div class="count">{{count | async}}</div>
<button (click)="increment()">
Increment
</button>
`
})
export class CounterComponent {
count: Observable<number> =
store.select(state => state.count);
increment() {
store.dispatch(new IncrementCounterAction());
}
}
export class TodoService {
loadTodos() {
this.http
.get<Todos[]>('http://localhost/api/todos')
.subscribe(todos =>
this.store.dispatch(new SetTodosAction(todos))
);
}
}
Action
Reducer
Effect
1
Dispatch action
Action
2
Map to new action
import { Actions, Effect, ofType } from '@ngrx/effects';
@Injectable()
export class TodoEffects {
@Effect() loadTodos = this.actions.pipe(
ofType('LoadTodosAction'),
mergeMap(action =>
this.http.get('http://localhost/api/todos')
.pipe(
map(todos => new SetTodosAction(todos))
)
)
);
constructor(private http: HttpClient, private actions: Actions) {}
}
@Component({
selector: 'app-todos',
template: `
<h2>Todos</h2>
<ul class="todo-list">
<li *ngFor="let todo of todos | async">
{{todo.title}}
</li>
</ul>`
})
export class TodosComponent {
todos: Observable<Todo[]>;
constructor() {
this.todos = store
.select(state => state.todos);
store.dispatch(new LoadTodosAction());
}
}
Effect
Reducer
Component
LoadItemsAction
LoadSuccessAction
LoadFailedAction
export interface State {
loading: boolean;
errors: string[];
items: Item[];
}
@Effect() loadItems = this.actions.pipe(
ofType('LoadItemsAction'),
mergeMap(action =>
this.http.get('http://localhost/api/items')
.pipe(
map(items => new LoadSuccessAction(items)),
catchError(() => of(new LoadFailedAction('failed!'))
)
)
);
case 'LoadItemsAction':
return {
...state,
loading: true
};
case 'LoadSuccessAction':
return {
...state,
loading: false,
items: action.items
};
case 'LoadFailedAction':
return {
...state,
loading: false,
errors: [action.error, ...state.errors]
};
Effect
Reducer
Component
OptimisticUpdateAction
ServerUpdateAction
export interface State {
updating: boolean;
item: Item;
}
@Effect() updateItem = this.actions.pipe(
ofType('OptimisticUpdateAction'),
mergeMap(action =>
this.http.post(`/api/items/${action.item.id}`, action.data)
.pipe(
map(item => new ServerUpdateAction(item))
)
)
);
case 'OptimisticUpdateAction':
return {
...state,
updating: true,
item: {
...state.item,
...action.optimisticData
}
};
case 'ServerUpdateAction':
return {
...state,
updating: false,
item: action.item
};
Effect
Reducer
Component
LoadMoreAction
AppendMoreItemsAction
export interface State {
items: Item[];
}
@Effect() loadMoreItems = this.actions.pipe(
ofType('LoadMoreAction'),
combineLatest(this.state.select(state => state.items.length)),
mergeMap(([action, start]) =>
this.http.get(`/api/items/?start=${start}&limit=10`)
.pipe(
map(items => new AppendMoreItemsAction(items))
)
)
);
case 'AppendMoreItemsAction':
return {
...state,
items: [...state.items, action.items]
};
Reducer
Component
NextStepAction
NextStepAction
export interface EnterName {
readonly kind: 'EnterName';
readonly name: string;
}
export interface EnterEmail {
readonly kind: 'EnterEmail';
readonly email: string;
}
export interface Done {
readonly kind: 'Done';
}
export type Step = EnterName | EnterEmail | Done;
export interface State {
readonly step: Step;
readonly data: {name: string; email: string};
}
case 'NextStepAction':
switch (action.step.kind) {
case 'EnterName':
return {
...state,
data: {name: action.name},
step: {
kind: 'EnterEmail',
email: ''
}
};
case 'EnterEmail':
return {
...state,
data: {...state.data, email: action.email},
step: {kind: 'Done'}
};
}
Let's implement state management with ngrx store.