Search code examples
angularrxjsbehaviorsubject

How to get previous value Subject?


I have the following Subject:

  private active = new Subject<P>();
  prev: Custom<any>;

  set(p: P): void {
    this.active.next(p);
  }

I want to extract data from active subject and set it to prev like this:

 set(p: P): void {
     this.prev = this.active;
     this.active.next(p);
 }

How to do that?


Solution

  • You're better off using BehaviorSubject here. It holds the current value and also has value getter to get it.

    private active = new BehaviorSubject<P>(null);  // <-- default value required
    prev: Custom<any>;
    
    set(p: P): void {
      this.prev = this.active.value;
      this.active.next(p);
    }
    

    But this doesn't actually look so clean. How exactly are you intending to use the prev value? Perhaps there's a cleaner way using ReplaySubject with buffer 2.

    Update: avoiding synchronous .value getter

    I could come up with 2 ways to use the previous value from a stream.

    1. A straight way with the pairwise operator. It'll emit the last value and current value as an array. One down side it doesn't emit the first value since there is no current previous state yet. It could be adjusted with the startWith operator.
    import { Subject } from "rxjs";
    import { startWith, pairwise} from "rxjs/operators";
    
    export class AppComponent {
      subSource = new Subject<any>();
      sub$ = this.subSource.pipe(
        startWith(null),
        pairwise()
      );
    
      constructor() {
        this.sub$.subscribe(val => console.log("pairwise:", val));
    
        this.subSource.next(1);
        this.subSource.next(2);
        this.subSource.next(3);
      }
    }
    
    // expected output:
    // pairwise: [null, 1]
    // pairwise: [1, 2]
    // pairwise: [2, 3]
    
    1. If you do not wish to have a default value at the beginning, you could use ReplaySubject with buffer 2 and use scan operator to emit only previous and current values.
    import { ReplaySubject } from "rxjs";
    import { scan } from "rxjs/operators";
    
    export class AppComponent {
      replaySubSource = new ReplaySubject<any>(2);
      replaySub$ = this.replaySubSource.pipe(
        scan((acc, curr) => {
          acc.push(curr);
          return acc.slice(-2);
        }, [])
      );
    
      constructor() {
        this.replaySub$.subscribe(val => console.log("replay subject:", val));
    
        this.replaySubSource.next(1);
        this.replaySubSource.next(2);
        this.replaySubSource.next(3);
      }
    }
    
    // replay subject: [1]
    // replay subject: [1, 2]
    // replay subject: [2, 3]
    

    Working example: Stackblitz

    As for the question about the default value using find. No I don't think it's dirty to use find there. In fact, find is there to fetch a specific element based on a predicate, so I don't see any wrong in your usage.

    Update: emit as {prev: .., active: ..}.

    You need only to pipe in a map operator to the end and emit as per the requirement.

    sub$ = this.subSource.pipe(
      startWith(null),
      pairwise(),
      map(([prev, active]) => ({ prev: prev, active: active }))
    );
    
    // expected output:
    // pairwise: {prev: null, active: 1}
    // pairwise: {prev: 1, active: 2}
    // pairwise: {prev: 2, active: 3}
    

    I've also adjusted the Stackblitz to reflect the changes.