Comment obtenir la valeur actuelle du sujet RxJS ou observable?

207

J'ai un service Angular 2:

import {Storage} from './storage';
import {Injectable} from 'angular2/core';
import {Subject}    from 'rxjs/Subject';

@Injectable()
export class SessionStorage extends Storage {
  private _isLoggedInSource = new Subject<boolean>();
  isLoggedIn = this._isLoggedInSource.asObservable();
  constructor() {
    super('session');
  }
  setIsLoggedIn(value: boolean) {
    this.setItem('_isLoggedIn', value, () => {
      this._isLoggedInSource.next(value);
    });
  }
}

Tout fonctionne très bien. Mais j'ai un autre composant qui n'a pas besoin de s'abonner, il a juste besoin d'obtenir la valeur actuelle d'isLoggedIn à un certain moment. Comment puis-je faire ceci?

Baconbeastnz
la source

Réponses:

341

A Subjectou Observablen'a pas de valeur actuelle. Lorsqu'une valeur est émise, elle est transmise aux abonnés et le Observablefait avec elle.

Si vous souhaitez avoir une valeur actuelle, utilisez celle BehaviorSubjectqui est conçue exactement à cette fin. BehaviorSubjectconserve la dernière valeur émise et la transmet immédiatement aux nouveaux abonnés.

Il dispose également d'une méthode getValue()pour obtenir la valeur actuelle.

Günter Zöchbauer
la source
1
Salut, c'est ma mauvaise, c'est la même chose maintenant dans le rxjs 5. Le lien du code source ci-dessus faisait référence à rxjs4.
le code de schrodinger
84
Note importante de l'auteur de RxJS 5: L'utilisation d' getValue()un drapeau rouge ÉNORME vous fait quelque chose de mal. Il est là comme une trappe d'évacuation. Généralement, tout ce que vous faites avec RxJS doit être déclaratif. getValue()est impératif. Si vous utilisez getValue(), il y a 99,9% de chances que vous fassiez quelque chose de mal ou de bizarre.
Ben Lesh
1
@BenLesh et si j'en ai un BehaviorSubject<boolean>(false)et que je souhaite le basculer?
bob
3
@BenLesh getValue () est très utile pour, disons, faire une action instantanée, comme onClick-> dispatchToServer (x.getValue ()) mais ne l'utilisez pas dans des chaînes observables.
FlavorScape
2
@ IngoBürk La seule façon dont je peux justifier votre suggestion est si vous ne saviez pas que foo$c'était un BehaviorSubject(c'est-à-dire qu'il est défini comme Observableet peut-être qu'il fait chaud ou froid à partir d'une source différée). Cependant , puisque vous allez ensuite à utiliser .nextsur $foolui - même qui signifie que votre mise en œuvre repose sur elle étant un BehaviorSubjectdonc il n'y a aucune raison pour ne pas utiliser simplement .valuepour obtenir la valeur actuelle en premier lieu.
Simon_Weaver
145

La seule façon que vous devriez être obtenir des valeurs « sur » un Observable / sujet est subscribe!

Si vous utilisez, getValue()vous faites quelque chose d'impératif dans le paradigme déclaratif. Il est là comme une trappe d'évacuation, mais 99,9% du temps, vous ne devriez PAS l'utiliser getValue(). Il y a quelques choses intéressantes quigetValue() faire: cela générera une erreur si le sujet a été désabonné, il vous empêchera d'obtenir une valeur si le sujet est mort parce qu'il est erroné, etc. Mais, encore une fois, il est là comme une échappatoire trappe pour de rares circonstances.

Il existe plusieurs façons d'obtenir la dernière valeur d'un sujet ou observable de manière "Rx-y":

  1. En utilisant BehaviorSubject: mais en y souscrivant . Lorsque vous vous abonnez pour la première fois àBehaviorSubject il envoie de manière synchrone la valeur précédente qu'il a reçue ou a été initialisée avec.
  2. Utilisation d'un ReplaySubject(N): cela mettra en cacheN valeurs et les rejouera aux nouveaux abonnés.
  3. A.withLatestFrom(B): Utilisez cet opérateur pour obtenir la valeur la plus récente de l'observable Blors de son Aémission. Vous donnera les deux valeurs dans un tableau [a, b].
  4. A.combineLatest(B): Utilisez cet opérateur pour obtenir les valeurs les plus récentes de Aet Bchaque fois que ce soit Aou Bémet. Vous donnera les deux valeurs dans un tableau.
  5. shareReplay(): Effectue une multidiffusion observable via un ReplaySubject, mais vous permet de réessayer l'observable en cas d'erreur. (Fondamentalement, cela vous donne ce comportement de mise en cache prometteur).
  6. publishReplay(), publishBehavior(initialValue), multicast(subject: BehaviorSubject | ReplaySubject), Etc: D' autres opérateurs qui tirent parti BehaviorSubjectet ReplaySubject. Différentes saveurs de la même chose, elles diffusent essentiellement la source observable en canalisant toutes les notifications via un sujet. Vous devez appeler connect()pour vous abonner à la source avec le sujet.
Ben Lesh
la source
19
pouvez-vous expliquer pourquoi l'utilisation de getValue () est un drapeau rouge? Que faire si j'ai besoin de la valeur actuelle de l'observable une seule fois, au moment où l'utilisateur clique sur un bouton? Dois-je m'abonner à la valeur et me désinscrire immédiatement?
Flame
5
Ça va parfois. Mais souvent, c'est un signe que l'auteur du code fait quelque chose de très impératif (généralement avec des effets secondaires) où ils ne devraient pas être. Vous pouvez également click$.mergeMap(() => behaviorSubject.take(1))résoudre votre problème.
Ben Lesh
1
Grande explication! En fait, si vous pouvez garder Observable et utiliser les méthodes, c'est une bien meilleure façon. Dans un contexte de dactylographie avec Observable utilisé partout mais seulement quelques uns définis comme BehaviourSubject, alors ce serait du code moins cohérent. Les méthodes que vous avez proposées m'ont permis de garder des types observables partout.
JLavoie du
2
c'est bien si vous voulez simplement envoyer la valeur actuelle (comme l'envoyer au serveur en un clic), faire une getValue () est très pratique. mais ne l'utilisez pas pour chaîner des opérateurs observables.
FlavorScape
1
J'ai un AuthenticationServicequi utilise un BehaviourSubjectpour stocker l'état actuellement connecté ( boolean trueou false). Il expose un isLoggedIn$observable pour les abonnés qui veulent savoir quand l'état change. Il expose également une get isLoggedIn()propriété, qui renvoie l'état actuellement connecté en appelant getValue()le sous BehaviourSubject- jacent - cela est utilisé par mon garde d'authentification pour vérifier l'état actuel. Cela me semble une utilisation sensée de getValue()...?
Dan King
13

J'ai eu une situation similaire où les abonnés tardifs s'abonnent au sujet après que sa valeur soit arrivée.

J'ai trouvé ReplaySubject qui est similaire à BehaviorSubject fonctionne comme un charme dans ce cas. Et voici un lien pour une meilleure explication: http://reactivex.io/rxjs/manual/overview.html#replaysubject

Kfir Erez
la source
1
cela m'a aidé dans mon application Angular4 - j'ai dû déplacer l'abonnement du constructeur du composant vers le ngOnInit () (ce composant est partagé entre les routes), juste en partant ici au cas où quelqu'un aurait un problème similaire
Luca
1
J'ai eu un problème avec une application Angular 5 où j'utilisais un service pour obtenir des valeurs à partir d'une API et définir des variables dans différents composants. J'utilisais des sujets / observables mais cela ne poussait pas les valeurs après un changement d'itinéraire. ReplaySubject était une goutte en remplacement de Subject et a tout résolu.
jafaircl
1
Assurez-vous que vous utilisez ReplaySubject (1) sinon les nouveaux abonnés recevront chaque valeur précédemment émise dans l'ordre - ce n'est pas toujours évident au moment de l'exécution
Drenai
@Drenai pour autant que je comprends ReplaySubject (1) se comportent de la même manière que BehaviorSubject ()
Kfir Erez
Pas tout à fait la même chose, la grande différence étant que ReplaySubject n'émet pas immédiatement une valeur par défaut lorsqu'il est abonné si sa next()fonction n'a pas encore été invoquée, contrairement à BehaviourSubject. L'émission immédiate est très pratique pour appliquer des valeurs par défaut à une vue, lorsque le BehaviourSubject est utilisé comme source de données observable dans un service par exemple
Drenai
5
const observable = of('response')

function hasValue(value: any) {
  return value !== null && value !== undefined;
}

function getValue<T>(observable: Observable<T>): Promise<T> {
  return observable
    .pipe(
      filter(hasValue),
      first()
    )
    .toPromise();
}

const result = await getValue(observable)
// Do the logic with the result
// .................
// .................
// .................

Vous pouvez consulter l'article complet sur la façon de l'implémenter à partir d'ici. https://www.imkrish.com/how-to-get-current-value-of-observable-in-a-clean-way/

Keerati Limkulphong
la source
3

J'ai rencontré le même problème dans les composants enfants où initialement il devrait avoir la valeur actuelle du sujet, puis s'abonner au sujet pour écouter les modifications. Je conserve simplement la valeur actuelle du service afin qu'elle soit accessible aux composants, par exemple:

import {Storage} from './storage';
import {Injectable} from 'angular2/core';
import {Subject}    from 'rxjs/Subject';

@Injectable()
export class SessionStorage extends Storage {

  isLoggedIn: boolean;

  private _isLoggedInSource = new Subject<boolean>();
  isLoggedIn = this._isLoggedInSource.asObservable();
  constructor() {
    super('session');
    this.currIsLoggedIn = false;
  }
  setIsLoggedIn(value: boolean) {
    this.setItem('_isLoggedIn', value, () => {
      this._isLoggedInSource.next(value);
    });
    this.isLoggedIn = value;
  }
}

Un composant qui a besoin de la valeur actuelle pourrait simplement y accéder à partir du service, c'est-à-dire:

sessionStorage.isLoggedIn

Je ne sais pas si c'est la bonne pratique :)

Molp Burnbright
la source
2
Si vous avez besoin de la valeur d'un observable dans votre vue de composant, vous pouvez simplement utiliser le asynctuyau.
Ingo Bürk,
2

Une semblable recherche réponse a été downvoted. Mais je pense que je peux justifier ce que je propose ici pour des cas limités.


S'il est vrai qu'un observable n'a pas de valeur actuelle , il aura très souvent une valeur immédiatement disponible . Par exemple, avec les magasins redux / flux / akita, vous pouvez demander des données à un magasin central, sur la base d'un certain nombre d'observables et cette valeur sera généralement immédiatement disponible.

Si tel est le cas, lorsque vous subscribe, la valeur reviendra immédiatement.

Supposons donc que vous ayez appelé un service et, une fois terminé, vous souhaitez obtenir la dernière valeur de quelque chose de votre magasin, qui pourrait ne pas émettre :

Vous pourriez essayer de le faire (et vous devriez autant que possible garder les choses «à l'intérieur des tuyaux»):

 serviceCallResponse$.pipe(withLatestFrom(store$.select(x => x.customer)))
                     .subscribe(([ serviceCallResponse, customer] => {

                        // we have serviceCallResponse and customer 
                     });

Le problème avec cela est qu'il se bloquera jusqu'à ce que l'observable secondaire émette une valeur, qui pourrait potentiellement ne jamais l'être.

Je me suis retrouvé récemment à avoir besoin d'évaluer un observable uniquement si une valeur était immédiatement disponible , et plus important encore, j'avais besoin de pouvoir détecter si ce n'était pas le cas. J'ai fini par faire ça:

 serviceCallResponse$.pipe()
                     .subscribe(serviceCallResponse => {

                        // immediately try to subscribe to get the 'available' value
                        // note: immediately unsubscribe afterward to 'cancel' if needed
                        let customer = undefined;

                        // whatever the secondary observable is
                        const secondary$ = store$.select(x => x.customer);

                        // subscribe to it, and assign to closure scope
                        sub = secondary$.pipe(take(1)).subscribe(_customer => customer = _customer);
                        sub.unsubscribe();

                        // if there's a delay or customer isn't available the value won't have been set before we get here
                        if (customer === undefined) 
                        {
                           // handle, or ignore as needed
                           return throwError('Customer was not immediately available');
                        }
                     });

Notez que pour tout ce qui précède, j'utilise subscribepour obtenir la valeur (comme @Ben en parle). Ne pas utiliser de .valuepropriété, même si j'en avais BehaviorSubject.

Simon_Weaver
la source
1
Btw cela fonctionne parce que par défaut 'planning' utilise le thread actuel.
Simon_Weaver
1

Bien que cela puisse sembler exagéré, ce n'est qu'une autre solution "possible" pour garder le type observable et réduire le passe-partout ...

Vous pouvez toujours créer un getter d'extension pour obtenir la valeur actuelle d'un observable.

Pour ce faire, vous devez étendre l' Observable<T>interface dans un global.d.tsfichier de déclaration de typage. Ensuite, implémentez l' extension getter dans un observable.extension.tsfichier et incluez enfin à la fois les saisies et le fichier d'extension dans votre application.

Vous pouvez vous référer à cette réponse StackOverflow pour savoir comment inclure les extensions dans votre application angulaire.

// global.d.ts
declare module 'rxjs' {
  interface Observable<T> {
    /**
     * _Extension Method_ - Returns current value of an Observable.
     * Value is retrieved using _first()_ operator to avoid the need to unsubscribe.
     */
    value: Observable<T>;
  }
}

// observable.extension.ts
Object.defineProperty(Observable.prototype, 'value', {
  get <T>(this: Observable<T>): Observable<T> {
    return this.pipe(
      filter(value => value !== null && value !== undefined),
      first());
  },
});

// using the extension getter example
this.myObservable$.value
  .subscribe(value => {
    // whatever code you need...
  });
j3ff
la source
0

Vous pouvez stocker la dernière valeur émise séparément de l'Observable. Lisez-le ensuite au besoin.

let lastValue: number;

const subscription = new Service().start();
subscription
    .subscribe((data) => {
        lastValue = data;
    }
);
Slawa
la source
1
Ce n'est pas une approche réactive pour stocker des choses en dehors de l'observable. Au lieu de cela, vous devriez avoir autant de données que possible circulant à l'intérieur des flux observables.
ganqqwerty
0

La meilleure façon de le faire est d'utiliser Behaviur Subject, voici un exemple:

var sub = new rxjs.BehaviorSubject([0, 1])
sub.next([2, 3])
setTimeout(() => {sub.next([4, 5])}, 1500)
sub.subscribe(a => console.log(a)) //2, 3 (current value) -> wait 2 sec -> 4, 5
yaya
la source
0

Un abonnement peut être créé et après avoir pris le premier élément émis détruit. Pipe est une fonction qui utilise un observable comme entrée et renvoie un autre observable comme sortie, sans modifier le premier observable. Angulaire 8.1.0. Forfaits: "rxjs": "6.5.3","rxjs-observable": "0.0.7"

  ngOnInit() {

    ...

    // If loading with previously saved value
    if (this.controlValue) {

      // Take says once you have 1, then close the subscription
      this.selectList.pipe(take(1)).subscribe(x => {
        let opt = x.find(y => y.value === this.controlValue);
        this.updateValue(opt);
      });

    }
  }
SushiGuy
la source