Angular 2 et la programmation réactive

"Angular is a data-driven framework"

tout le monde

"$scope events in Angular are terrible!"

presque tout le monde

"Angular 2 is a reactive framework"

Victor Savkin

D'après le reactive manifesto, les applications réactives sont :

  • « Responsive » : capable d’offrir une expérience utilisateur optimale

  • « Resilient » : de mieux tolérer les erreurs et les pannes

  • « Elactic » : de mieux utiliser la puissance des machines

  • « Message-driven » : dirigée par les événements

On parle de modèle de développement :

  • « event driven » : dirigé par les événements

  • « push based application », les données sont poussées dès qu’elles sont disponibles

  • « Hollywood » : Don't call us, we'll call you

  • « réactif », qui réagit aux événements

 

Idéal pour les applications qui interagissent en temps réel avec les utilisateurs

Events vs Data

Il est facile de travailler avec des données

Il existe pleins d'outils pour manipuler des données

var numbers = [1, 2, 3, 4, 5];

function double(number) {
  return number * 2;
}

var doubledNumbers = numbers.map(double);
//[2, 4, 6, 8, 10]

.map

var numbers = [1, 2, 3, 4, 5];

function greaterThanThree(value) {
  return value > 3;
}

var numbersGreaterThanThree = 
  numbers.filter(greaterThanThree);
//[4, 5]

.filter

var numbers = [1, 2, 3, 4, 5];

function sum(total, value) {
  return total + value;
}

var total = numbers.reduce(sum, 0);
//15

.reduce

function(x) { return x + 1; }

(x) => { return x + 1; }

x => x + 1

ES6 arrow functions

var numbers = [1, 2, 3, 4, 5];

var doubledNumbers = numbers.map(n => n * 2);
//[2, 4, 6, 8, 10]

ES6 Iterables/Iterators

Les notions iterable/iterator permettent de créer une collection

Quelque chose est iterable si on peut parcourir ses valeurs

 

On peut demander (« pull ») la prochaine valeur

On utilise un iterator pour accéder à ces valeurs

var numbers = [1, 2, 3];

//.values() returns an iterator
var numIterator = numbers.values();

numIterator.next() //{value: 1, done: false }
numIterator.next() //{value: 2, done: false }
numIterator.next() //{value: 3, done: false }
numIterator.next() //{done: true}

Array as Iterables

var mySet = new Set();
mySet.add(1);
mySet.add(2);
mySet.add(3);

//.values() returns an iterator
var numIterator = mySet.values();

numIterator.next() //{value: 1, done: false }
numIterator.next() //{value: 2, done: false }
numIterator.next() //{value: 3, done: false }
numIterator.next() //{done: true}

Set as Iterables

var myMap = new Map();

myMap.set('a', 1);
myMap.set('b', 2);
myMap.set('c', 3);

//.values() returns an iterator
var valueIterator = myMap.values();

valueIterator.next() //{value: 1, done: false }
valueIterator.next() //{value: 2, done: false }
valueIterator.next() //{value: 3, done: false }
valueIterator.next() //{done: true}

//.keys() returns an iterator
var keyIterator = myMap.keys();

keyIterator.next() //{value: 'a', done: false }
keyIterator.next() //{value: 'b', done: false }
keyIterator.next() //{value: 'c', done: false }
keyIterator.next() //{done: true}

//.entries() returns an iterator
var entryIterator = myMap.entries();

entryIterator.next() //{value: ['a', 1], done: false }
entryIterator.next() //{value: ['b', 2], done: false }
entryIterator.next() //{value: ['c', 3], done: false }
entryIterator.next() //{done: true}

Map as Iterables

function* lazyNumbers(){
  yield 1;
  yield 2;
  yield 3;
}

//generator functions return an iterator
var valueIterator = lazyNumbers();

valueIterator.next() //{value: 1, done: false }
valueIterator.next() //{value: 2, done: false }
valueIterator.next() //{value: 3, done: false }
valueIterator.next() //{done: true}

Generators

C'est top !

Mais...

Cela fonctionne uniquement avec des objets synchrones :
 

Si on demande une valeur à partir d'une collection pull »), elle doit être disponible tout de suite

Async

Si synchronous as « pull »...

alors asynchronous as « push »

Avec Javascript, l'Async est géré par les événements : 

La boucle d'événement Javascript nous pousse (« push ») des valeurs

var myButton = document.getElementById('myButton');

function doSomethingOnClick(event){
  doSomething(event);

  //make sure you remember to tidy up after yourself!
  myButton.removeEventListener('click', doSomethingOnClick);
}

myButton.addEventListener('click', doSomethingOnClick);

Evénements du DOM

Les callbacks sont des événements !

getStuff(function(result){
  getMoreStuff(function(results){
    getSomeStuffForEachResult(results, function(moreStuff){
         // note my code running off the page...^
         doTheThingWeWantedToDo(moreStuff);
    });
  });
});

(ugh)

function getStuff(url, callback){
  var req = new XMLHttpRequest();
  function reqListener () {
    callback(JSON.parse(req.responseText));
  }
  req.addEventListener("load", reqListener);
  req.open("GET", url);
  req.send();
}

getStuff('foos.json', function(foos){
  getStuff('bars.json', function(bars){
    //do stuff with foos and bars
  });
});

XMLHttpRequest

Allô ? Et les Promises alors ?

fetch('foos.json')
  .then(function(res){
    return res.json();
  })
  .then(function(foos){
    return fetch('bars.json')
      .then(function(res){
        return res.json();
      })
      .then(function(bars){
        return [foos, bars];
      })
  })
  .then(function(results){
    //do stuff with foos and bars
  });

fetch

 

Les Promises, c'est top !

Mais...

Les Promises ne résolvent pas tout

On ne peut pas annuler une promise

Les Promises sont idéales pour gérer une valeur

Mais ce n'est pas le cas lorsqu'il faut gérer plusieurs valeurs

Et si on pouvait unifier la manipulation des événements de la même manière que les iterables/iterators unifient la manipulation des données ?

...gérer plusieurs valeurs facilement...

...libérer les ressources automatiquement...

...annuler ce qui n'est plus nécessaire...

et...

...vous vous rappelez des librairies gérant les données ? map/filter/reduce/etc ?

Et si on pouvait les utiliser également pour manipuler nos événements ?

Comment ?

"Reactive programming is programming with asynchronous data streams"

Un « stream » c'est simplement une collection qui se constitue dans le temps

Collection

Stream

[ 1, 2, 3]

1

2

3

Observables

Un Observable c'est comme une collection...

Sauf que les données arrivent dans le temps, de manière asynchrone

Un Observable c'est comme une Promise...

Sauf que l'on peut manipuler plusieurs valeurs

On peut les annuler

Et en plus, on peut utiliser
 .map, .filter, .reduce
(et bien plus encore !)

An API for asynchronous programming
with observable streams

 

The Observer pattern done right
ReactiveX is a combination of the best ideas from
the Observer pattern, the Iterator pattern, and functional programming

 

Available in Javascript with RxJS

Observable API

//Observable constructor
const myObservable = new Observable(observer => {

  //the observer lets us *push* values
  observer.next(1);
  observer.next(2);
  observer.next(3);

  //it lets us propagate errors
  observer.error('oops');

  //and lets us (optionally) complete the stream
  observer.complete();

});

Observable API

//Observable constructor
const myObservable = new Observable(observer => {
  observer.next(1);
  observer.next(2);
  observer.next(3);

  observer.complete();
});

myObservable.subscribe(
  val => console.log(val),
  err => console.log(err),
  _ => console.log('done')
);

Observable API

//Observable constructor
const myObservable = new Observable(observer => {
  let count = 0;
  const interval = setInterval(() => {
    observer.next(count++);
  }, 100);
  
  //disposal function
  return () => {
    clearInterval(interval);
  }
});

const subscriber = myObservable.subscribe(
  val => console.log(val),
  err => console.log(err),
  _ => console.log('done')
);

subscriber.unsubscribe();

Observable en image

(Marble diagram)

Observable en ASCII

--a---b-c---d---X---|->

a, b, c, d are emitted values
X is an error
| is the 'completed' signal
---> is the timeline

(Marble diagram)

Duality

function* values(){
  yield 1;
  yield 2;
  yield 3;
}
const i = values();

i.next() //{value: 1, done: false}
i.next() //{value: 2, done: false}
i.next() //{value: 3, done: false}
i.next() //{done: true}
const o = new Observable(observer => {

  observer.next(1);
  observer.next(2);
  observer.next(3);

  observer.complete();

});

o.subscribe(
  value => console.log(value),
  err => console.error(err),
  _ => console.log('done')
);
  

Pull

Push

Promise to not use Promise

Brian Holt (NetFlix)

Single return value Multiple return values
Pull/Synchronous/Interactive object Iterables
Push/Asynchronous/Reactive Promise Observables

Si on résume...

const myButton = document.getElementById('myButton');

const clicks$ = new Observable(observer => {
  const onClick = ev => observer.next(ev);

  myButton.addEventListener('click', onClick);

  return () => {
    myButton.removeEventListener('click', onClick);
  }
});


const clickListener = clicks$.subscribe(event => console.log(event));

//later...
clickListener.unsubscribe();

DOM events

const myButton = document.getElementById('myButton');

const clicks$ = Observable.fromEvent(myButton, 'click');

const clickListener = clicks$.subscribe(event => console.log(event));
//ClickEvent
//ClickEvent
//ClickEvent

//later...
clickListener.unsubscribe();

DOM Events

const myInput = document.getElementById('myInput');

//stream of keyup Events
const keyups$ = Observable.fromEvent(myInput, 'keyup');

//map to the values
const inputs$ = keyups$.map(ev => ev.target.value);

inputs$.subscribe(text => console.log(text));
//h
//he
//hel
//hell
//hello

.map

---1----8--3----2------6-->
vvvvv map(v => v + 1) vvvv
---2----9--4----3------7-->
const incrementButton = document.getElementById('increment');
const decrementButton = document.getElementById('decrement');
const counterOutput = document.getElementById('output');

const getValue = ev => parseInt(ev.target.value,10);

const increments$ = Observable.fromEvent(incrementButton, 'click');
const decrements$ = Observable.fromEvent(decrementButton, 'click');

//merge into a single stream
const changes$ = Observable.merge(increments$, decrements$);

//map to int values
const values$ = changes$.map(getValue);

//scan (reduce) to track the state
const total$ = values$.scan((total, value) => total + value, 0);

//set count
total$.subscribe(count => {
  counterOutput.innerText = count;
  console.log(count);
});

.scan/.reduce

.scan/.reduce en image

incrementStream(input): --(in)---(in)----------(in)--->
decrementStream(input): ---------------(de)----------->
  changesStream(merge): --(in)---(in)--(de)----(in)--->
   valuesStream  (map): --(+1)---(+1)--(-1)----(+1)--->
    totalStream (scan): --(+1)---(+2)--(+1)----(+2)--->

La démo en ASCII

Angular 2

import { Http } from '@angular/http'

const myInput = document.getElementById('myInput');

//stream of keyups Events
const keyups$ = Observable.fromEvent(myInput, 'keyup');

//map to the values
const inputs$ = keyups$.map(ev => ev.target.value);

//flatMap our inputs to *http responses*
const responses$ = inputs$
    .flatMap(text => Http.get(`foo.com/search/${text}`))
    .map(res => res.json());

responses$.subscribe(text => console.log(text));
//[{name: 'harry'},{name: 'hettie'}, {name: 'hellboy'}];
//[{name: 'harry'}, {name: 'hellboy'}];
//[{name: 'hellboy'}];

Angular 2 Http (with .flatMap)

.flatMap en image

 requestStream: --a-----b--c--------------|->
responseStream: -----A--------B-----C-----|->

(la requête en minuscule, la réponse en majuscules)
import { Http } from '@angular/http'

const myInput = document.getElementById('myInput');

//stream of click Events
const clicks$ = Observable.fromEvent(myInput, 'keyup');

//map to the values
const inputs$ = clicks$.map(ev => ev.target.value);

//flatMap our inputs to *http responses*
const responses$ = inputs$
    .switchMap(text => Http.get(`foo.com/search/${text}`))
    .map(res => res.json());

responses$.subscribe(text => console.log(text));
//[{name: 'harry'},{name: 'hettie'}, {name: 'hellboy'}];
//[{name: 'harry'}, {name: 'hellboy'}];
//[{name: 'hellboy'}];

.switchMap (.flatMapLatest)

const responses$ = 
  http.get('somebadconnection.json')
    .retry(3)
    .map(res => res.json());

responses$.subscribe(
  res => console.log(res),
  err => console.log('could not connect!')
);

Angular2 Http Retry

const ticks$ = Observable.interval(5000);
  
const responses$ = 
  ticks$
    .switchMap(() => http.get('stocks.json'))
    .map(res => res.json());

const stockPoller = responses$.subscribe(res => console.log(res));

//later
stockPoller.unsubscribe();

Angular2 Http Polling

class MyAutoCompleteComponent {
  constructor(http:Http, formBuilder:FormBuilder){
    this.searchForm = formBuilder.group({
      name: ['', Validators.required]
    });
    this.people = searchForm.controls.name.valueChanges
        .switchMap(text => Http.get(`foo.com/search/${text}`))
        .map(res => res.json());
  }
}

Angular2 Reactive Forms...

<form novalidate [formGroup]="searchForm">
  <input type="text" formControlName="name">
</form>
<div>
  <ul>
    <li *ngFor="let person of people | async">{{person.name}}</li>
  </ul>
</div>

...and Async Pipe

Angular 2 routing

import { Http } from '@angular/http';
import { ActivatedRoute } from '@angular/router';

export class MyComponent {

  constructor(
    private http: Http,
    private route: ActivatedRoute) {}

  ngOnInit() {
    this.data = this.route.params
      .map(params => params['id'])
      .switchMap(id => this.http.get('${id}/data.json'));
  }
}

Angular 2 & ngrx/store (Redux)

import { Store } from '@ngrx/store';
import { INCREMENT, DECREMENT, RESET } from './counter';

interface AppState {
  counter: number;
}

@Component({
    selector: 'my-app',
    template: `
        <button (click)="increment()">Increment</button>
        <div>Current Count: {{ counter | async }}</div>
        <button (click)="decrement()">Decrement</button>

        <button (click)="reset()">Reset Counter</button>
    `
})
class MyAppComponent {
    counter: Observable<number>;

    constructor(private store: Store<AppState>){
        this.counter = store.select('counter');
    }

    increment(){
        this.store.dispatch({ type: INCREMENT });
    }

    decrement(){
        this.store.dispatch({ type: DECREMENT });
    }

    reset(){
        this.store.dispatch({ type: RESET });
    }
}

Ressources

Questions ?

Angular 2 et la programmation réactive

By Bruno Baia

Angular 2 et la programmation réactive

Angular 2 adopte les Observables et la programmation Reactive. Apprenez ce qu'est un Observable et comment Angular 2 les utilise pour créer des applications avec une expérience utilisateur optimale.

  • 1,544