Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ngrx: current value not received in Subject upon subscription

Tags:

In a component, I subscribe a Subject to some part of my state using the store. From then on, when that part of the state changes, I will start receiving updates and my component will reflect this state. So far so good.

However, I also want my component to initialize correctly based on the current value of this state. So as soon as I subscribe, I also want the current value to get emitted - so, e.g., my view can initialize correctly.

Example with an Angular component:

my.component.ts

export class MyComponent implements OnInit {
  constructor(private store: Store<State>) { }

  // As soon as I subscribe, I want this to also emit the current
  // value, e.g. so my view can correctly reflect the current state.
  public mode$ = new Subject<ApplicationMode>();

  ngOnInit() {
    this.store.select(getApplicationState)
       .select(s => s.applicationMode).subscribe(this.mode$);
  }
}

my.component.html

{{ mode$ | async }}

I believe that what I would like/expect is that this part of my store would return a BehaviorSubject or a ReplaySubject(1). But selecting anything from the store always returns a Store<T> which is obviously some sort of subject, but doesn't seem to emit the current value upon subscription?

What might work, I guess, is to subscribe to this piece of state on application initialization, and then pass that value on through all its child components, all the way down to this one, so this component turns into a dumb one instead of selecting from the store itself. Is that perhaps the way to go? Or is there something basic I'm missing to make this work?

like image 335
Vincent Sels Avatar asked Nov 15 '17 14:11

Vincent Sels


1 Answers

Ngrx does act like a BehaviorSubject where you get the current value when you first subscribe.

The issue is probably that you are pushing the value from a behavior subject (store) into a normal subject (mode$) before that subject is subscribed to. This would mean that you push the initial value into your subject (mode$) and it would have no subscriber to notify so it fizzles. When the subscriber subscribes to mode$ it is subscribing to a normal subject so it doesn't get the current value but it would get all future values.

What I would suggest is cutting out the middle-man and to expose the observable returned from the store directly to the view like so:

this.mode$ = this.store.select(...);

This way you are exposing an observable that will provide the current value on subscribe to any subscribers.

I am finding it hard to find specific documentation on where in the component lifecycle that the async pipe subscribes. So I put together a test where I publish to a subject in every lifecycle hook and subscribe to it via the async pipe.

The code:

import { 
  Component, 
  OnChanges, 
  OnInit, 
  DoCheck, 
  AfterContentInit, 
  AfterContentChecked, 
  AfterViewInit, 
  AfterViewChecked, 
  OnDestroy,
} from '@angular/core';

import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/do';

@Component({
  selector: 'app-test',
  template: '{{ data$ | async }}'
})
export class TestComponent implements 
  OnChanges, 
  OnInit, 
  DoCheck, 
  AfterContentInit, 
  AfterContentChecked, 
  AfterViewInit, 
  AfterViewChecked, 
  OnDestroy {
    public get data$() {
      return this.data.asObservable().do(x => console.log('event: ', x));
    }
    public data: Subject<string> = new Subject<string>();


  ngOnChanges() {
    this.data.next('ngOnChanges');
  }

  ngOnInit() {
    this.data.next('ngOnInit');
  }

  ngDoCheck() {
    this.data.next('ngDoCheck');
  }

  ngAfterContentInit() {
    this.data.next('ngAfterContentInit');
  } 

  ngAfterContentChecked() {
    this.data.next('ngAfterContentChecked');
  }

  ngAfterViewInit() {
    this.data.next('ngAfterViewInit');
  }

  ngAfterViewChecked() {
    this.data.next('ngAfterViewChecked');
  }

  ngOnDestroy() {
    this.data.next('ngOnDestroy');
  }
}

The results:

event:  ngAfterViewInit
event:  ngAfterViewChecked
event:  ngDoCheck
event:  ngAfterContentChecked
event:  ngAfterViewChecked

The results indicate that the async pipe first executes after ngOnInit which is where you first subscribed. This would explain why you missed it.

like image 131
bygrace Avatar answered Sep 22 '22 12:09

bygrace