Programação Reativa para Web na Prática

William Grasel

@willgmbr

fb.me/wgrasel

define:

reactive programming 

"Reactive programming is a programming paradigm oriented around data flows and the propagation of change."

"Reactive programming is programming with asynchronous data streams"

- André Staltz

Data Streams?

O que mais pode ser um Stream?

  • Ações do usuário
  • Eventos do sistema
  • Estado da aplicação
  • Validações e regras de negócio
  • Qualquer fonte de dados, assíncrona ou não!

Qual a melhor forma de representar um fluxo de dados assíncrono?

síncrono

assíncrono

múltiplo

único

processamento

valor

Objeto

Array

Promise

Observable

Observable

RxJS

The ReactiveX library for JavaScript.


  [1, 2, 3]
    .map(i => i * 2) // [2, 4, 6]
    .filter(i => i > 5) // [6]
    .concat([10, 20, 30]) // [6, 10, 20, 30]

Operadores ❤️


  Observable.of(1, 2, 3)
    .map(i => i * 2)
    .filter(i => i > 5)
    .concat(
      Observable.of(10, 20, 30)
    )

  Observable.fromEvent(document, 'click')
    .map(i => i * 2)
    .filter(i => i > 5)
    .concat(
      Observable.fromEvent(document, 'blur')
    )

  Observable.fromEvent(document, 'click').pipe(
    map(i => i * 2),
    filter(i => i > 5),
    concat(
      Observable.fromEvent(document, 'blur')
    )
  ).subscribe(console.log)

  Array.of(1, 2, 3)
    .map(i => i * 2)
    .filter(i => i > 5)
    .concat(
      Array.of(10, 20, 30)
    )

  Observable.interval(500)
    .map(i => i * 2)
    .filter(i => i > 5)
    .concat(
      Observable.of(10, 20, 30)
    )

  Observable.fromEvent(document, 'click').pipe(
    map(i => i * 2),
    filter(i => i > 5),
    concat(
      Observable.fromEvent(document, 'blur')
    )
  )

Observable

❤️

Promise


  Observable.from(Promise.resolve(1))
    .concat(Promise.resolve(2))
    .concat(Promise.resolve(3))
    .subscribe(console.log); // 1, 2, 3

Mas e se eu quiser uma Promise?


  Observable.of(1, 2, 3)
    .toPromise()
    .then(console.log) // 3

  Observable.of(1, 2, 3)
    .first()
    .toPromise()
    .then(console.log) // 1

Tá, e o que eu ganho com isso?

Show me the code!

Typeahead Search


  <input type="text" [(ngModel)]="myInput">
  <ul>
    <li *ngFor="let r of results">{{r.name}}</li>
  </ul>

  <input type="text" [formControl]="myInput">
  <ul>
    <li *ngFor="let r of results">{{r.name}}</li>
  </ul>

  <input type="text" [formControl]="myInput">
  <ul>
    <li *ngFor="let r of results | async">{{r.name}}</li>
  </ul>

  @Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
  })
  export class AppComponent {
    myInput = new FormControl;
    results = of([]);
  }

  @Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
  })
  export class AppComponent {
    myInput = new FormControl;
    results = of([]).pipe(
      merge(this.myInput.valueChanges),
    );
  }

  @Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
  })
  export class AppComponent {
    myInput = new FormControl;
    results = of([]).pipe(
      merge(this.myInput.valueChanges),
      map(v => `https://swapi.co/api/people/?search=${v}`),
    );
  }

  @Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
  })
  export class AppComponent {
    myInput = new FormControl;
    results = of([]).pipe(
      merge(this.myInput.valueChanges),
      map(v => `https://swapi.co/api/people/?search=${v}`),
      mergeMap(url => this.http.get(url)),
    );

    constructor(private http: HttpClient) {}
  }

  @Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
  })
  export class AppComponent {
    myInput = new FormControl;
    results = of([]).pipe(
      merge(this.myInput.valueChanges),
      map(v => `https://swapi.co/api/people/?search=${v}`),
      switchMap(url => this.http.get(url)),
      map(json => json['results']),
    );

    constructor(private http: HttpClient) {}
  }

  @Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
  })
  export class AppComponent {
    myInput = new FormControl;
    results = of([]).pipe(
      merge(this.myInput.valueChanges),
      filter(v => v.length > 2),
      map(v => `https://swapi.co/api/people/?search=${v}`),
      switchMap(url => this.http.get(url)),
      map(json => json['results']),
    );

    constructor(private http: HttpClient) {}
  }

  @Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
  })
  export class AppComponent {
    myInput = new FormControl;
    results = of([]).pipe(
      merge(this.myInput.valueChanges),
      filter(v => v.length > 2),
      debounceTime(300),
      map(v => `https://swapi.co/api/people/?search=${v}`),
      switchMap(url => this.http.get(url)),
      map(json => json['results']),
    );

    constructor(private http: HttpClient) {}
  }

  @Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
  })
  export class AppComponent {
    myInput = new FormControl;
    results = of([]).pipe(
      merge(this.myInput.valueChanges),
      map(v => `https://swapi.co/api/people/?search=${v}`),
      mergeMap(url => this.http.get(url)),
      map(json => json['results']),
    );

    constructor(private http: HttpClient) {}
  }

  @Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
  })
  export class AppComponent {
    myInput = new FormControl;
    results = of([]).pipe(
      merge(this.myInput.valueChanges),
      filter(v => v.length > 2),
      debounceTime(300),
      map(v => `https://swapi.co/api/people/?search=${v}`),
      switchMap(url => this.http.get(url)),
      map(json => json['results']),
      retry(3),
    );

    constructor(private http: HttpClient) {}
  }

Router como Stream


  export class AppComponent {

    results = this.route.params.pipe(
      map(p => p.name),
      switchMap(name => this.user.search(name)),
    );

    constructor(
      private route: ActivatedRoute,
      private user: UserService,
    ) {}
  }

  export class AppComponent {

    myInput = new FormControl;

    results = this.route.params.pipe(
      map(p => p.name),
      merge(this.myInput.valueChanges),
      switchMap(name => this.user.search(name)),
    );

    constructor(
      private route: ActivatedRoute,
      private user: UserService,
    ) {}
  }

Firebase

Realtime Database

❤️

Observables

AngularFire


  @Component({
    template: `
    <div *ngFor="let user of users | async">
      {{ user.name }}
    </div>`
  })
  export class MyComponent {
    users = this.db.list('/users');

    constructor(private db: AngularFireDatabase) {}
  }

  @Component({
    template: `
    <div *ngFor="let user of users | async">
      {{ user.name }}
    </div>`
  })
  export class MyComponent {
    users = this.db.list('/users');

    constructor(private db: AngularFireDatabase) {}
  
    addItem() {
      this.users.push({ name: 'new item' });
    }
  }

Ridiculamente Simples

Bora juntar tudo!


  export class MyComponent {

    myInput = new FormControl;

    results = this.route.params.pipe(
      map(p => p.name),
      merge(this.myInput.valueChanges),
      startWith('joão'),
      switchMap(busca =>



      )),
    );

    constructor(
      private db: AngularFireDatabase,
      private route: ActivatedRoute,
    ) {}
  }

  export class MyComponent {

    myInput = new FormControl;

    results = this.route.params.pipe(
      map(p => p.name),
      merge(this.myInput.valueChanges),
      startWith('joão'),
      switchMap(busca =>
        this.db.list('/users', ref =>
          ref.orderByChild('name')
            .equalTo(busca)
      )),
    );

    constructor(
      private db: AngularFireDatabase,
      private route: ActivatedRoute,
    ) {}
  }

Como eu acho meu operador mágico?

Marbles Diagrams

Animando os Diagramas!

Imutabilidade!

Todo estado deve fazer parte do fluxo!

NUNCA faça um subscribe dentro de outro subscribe!


  @Component()
  export class UserAddressComponent {

    constructor (
      user: UserService,
      address: AddressService,
    ) {
      user.getCurrentUser().subscribe(user => {



      })
    }
    
  }

  @Component()
  export class UserAddressComponent {

    constructor (
      user: UserService,
      address: AddressService,
    ) {
      user.getCurrentUser().pipe(

      ).subscribe(user => {
        this.address = address;
      })
    }
    
  }

  @Component()
  export class UserAddressComponent {

    constructor (
      user: UserService,
      address: AddressService,
    ) {
      user.getCurrentUser().subscribe(user => {
        address.getAddressFromUser(user).subscribe(address => {
          this.address = address;
        })
      })
    }
    
  }

  @Component()
  export class UserAddressComponent {

    constructor (
      user: UserService,
      address: AddressService,
    ) {
      user.getCurrentUser().pipe(
        mergeMap(user => this.address.getAddressFrom(user))
      ).subscribe(address => {
        this.address = address;
      })
    }
    
  }

Não esqueça de fazer o unsubscribe!

Mas se puder, evite o unsubscribe!

Existem formas de automatizar o unsubscribe!

No Angular use o Pipe Async sempre que possível!

Limite a duração dos eventos reativamente usando operadores!


  @Component()
  export class MyComponent {

    ngOnInit() {
      interval(1000)
        .subscribe(i => console.log(i));
    }

  }

  @Component()
  export class MyComponent {

    unsubscribe = new Subject();

    ngOnInit() {
      interval(1000)

        .subscribe(i => console.log(i));
    }

    ngOnDestroy() {
      this.unsubscribe.next();
      this.unsubscribe.complete();
    }
    
  }

  @Component()
  export class MyComponent {

    unsubscribe = new Subject();

    ngOnInit() {
      interval(1000)
        .pipe(takeUntil(this.unsubscribe))
        .subscribe(i => console.log(i));
    }

    ngOnDestroy() {
      this.unsubscribe.next();
      this.unsubscribe.complete();
    }
    
  }

Referências

Perguntas?

Obrigado! =)