46

I'm trying to wrap my head around best practice when using Observables alongside ChangeDetectionStrategy.OnPush.

The example demonstrates the common scenario of wanting to show some kind of loading message (or a simple spinner animation perhaps):

Plnkr here

@Component({
  selector: 'my-app',
  template: `Are we loading?: {{loadingMessage}}`,

  // Obviously "Default" will notice the change in `loadingMessage`...
  // changeDetection: ChangeDetectionStrategy.Default

  // But what is best practice when using OnPush?
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class App implements OnInit {

  private loadingMessage = "Wait for it...";

  constructor() {

  }

  ngOnInit() {

    // Pretend we're loading data from a service.
    // This component is only interested in the call's success
    Observable.of(true)
      .delay(2000)
      .subscribe(success => {

        if(success){
          console.log('Pretend loading: success!');

          // OnPush won't detect this change
          this.loadingMessage = 'Success!';
        }

      });

  }
}

I more or less understand the requirement for immutability with OnPush and, to me at least, it currently makes sense when talking about actual model data (likely held in some kind of store).

So, I have two questions:

  1. Why doesn't the assignment of the new string value 'Success!' trigger the change detector? As far as immutability is concerned, the value has changed, right?
  2. How should lightweight internal component state (ie. loadingMessage) be implemented when using ChangeDetectionStrategy.OnPush? If there are multiple best practices, please point me in the right direction.
Philip Bulley
  • 9,014
  • 3
  • 33
  • 46

3 Answers3

59

Good question. I have two quotes from Savkin about onPush (since the Angular.io docs don't seem to have any info on this topic yet):

The framework will check OnPush components only when their inputs change or components' templates emit events. -- ref

When using OnPush, Angular will only check the component when any of its input properties changes, when it fires an event, or when an observable fires an event. -- ref (in a comment reply to @vivainio)

The second quote seems more complete. (Too bad it was buried in a comment!)

Why doesn't the assignment of the new string value Success! trigger the change detector? As far as immutability is concerned, the value has changed, right?

OnPush immutability is in reference to input properties, not normal instance properties. If loadingMessage were an input property and the value changed, change detection would execute on the component. (See @Vlado's answer for Plunker.)

How should lightweight internal component state (i.e., loadingMessage) be implemented when using ChangeDetectionStrategy.OnPush? If there are multiple best practices, please point me in the right direction.

Here's what I know so far:

  • If we change the internal state as part of handling an event, or part of an observable firing, change detection executes on the OnPush component (i.e., the view will update). In your particular case, I was surprised that the view did not update. Here are two guesses as to why not:
    • Adding delay() makes Angular look at it more like a setTimeout() rather than an observable change. setTimeouts do not result in change detection execution on an OnPush component. In your example the Change Detector has completed its work 2 seconds before the value of loadingMessage is changed.
    • Like @Sasxa shows in his answer, you have to have a binding in the template to the Observable. I.e., maybe it is more than just "an observable fires"... maybe it has to be a bound observable that fires. In which case, creating loadingMessage (or even a more generic state property) as an Observable itself will allow you to bind your template to its value (or multiple async values), see this example plnkr.
      Update 2016-04-28: it appears the binding must include | async, as shown in the plnkr, and in this plnkr.
  • If we change the internal state and it is not part of event handling or an observable firing, we can inject ChangeDetectorRef into our component and call method markForCheck() to cause change detection to execute on the OnPush component and all ancestor components up to the root component. If only view state is changed (i.e., state that is local to the component and maybe its descendants), detectChanges() can be used instead, which will not mark all ancestor components for change detection. Plunker
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • Thanks for your comprehensive answer. I'm thinking that the best solution is either `markForCheck()` (I'm borderline as to whether this is a little hacky or acceptable). Or creating a `loadingMessage` or `state` value as an `Observable` itself, that way it can represent multiple states as per this [plnkr](http://plnkr.co/edit/OiBSDA8Kcsgl4vBF15xW?p=preview). I think I'll add this to your answer and accept as it appears to cover all bases. – Philip Bulley Feb 14 '16 at 11:22
  • 4
    @PhilipBulley, `markForCheck()` is not hacky. The [ChangeDetectorRef API doc](https://angular.io/docs/ts/latest/api/core/ChangeDetectorRef-class.html) shows an example very similar to your use case: a component with `OnPush` has a `numberOfTicks` property that is updated in a `setTimeout()` callback, and `markForCheck()` is called to ensure the view updates. – Mark Rajcok Feb 15 '16 at 15:50
  • Thanks for clarifying with the example. – Philip Bulley Feb 15 '16 at 19:58
  • @MarkRajcok I can not find this quote on blog 'When using OnPush, Angular will only check the component when any of its input properties changes, when it fires an event, or when an observable fires an event. -- ref (in a comment reply to @vivainio)'? – alt255 Sep 25 '17 at 16:04
  • @alt255, I don't see it anymore either. – Mark Rajcok Sep 25 '17 at 17:31
  • This saved me. My entire application was set to `OnPush`, so in order to force change detection inside of subscriptions and timeouts, running `changeDetector.detectChanges()` or `changeDetector.markForCheck()` was needed to sync the view with the current context – TabsNotSpaces Oct 15 '19 at 18:50
9

AFAIK, OnPush is used when working directly with observables:

//our root app component
import {Component, OnInit, ChangeDetectionStrategy} from 'angular2/core'
import {Observable} from 'rxjs/Observable';
import 'rxjs/Rx';

@Component({
  selector: 'my-app',
  template: `Are we loading?: {{ loadingMessage |async }}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class App implements OnInit {
  private loadingMessage;
  constructor() { }

  ngOnInit() {
    this.loadingMessage = Observable.of(true)
      .delay(2000)
  }
}
Sasxa
  • 40,334
  • 16
  • 88
  • 102
  • This is a good solution based on my simplified example. In reality, the call which returns an `Observable` isn't made in `ngOnInit`, but instead within a form submit handler. Meaning the template binding would then need to be `{{ loadingMessage && loadingMessage | async }}`. – Philip Bulley Feb 14 '16 at 11:03
  • ...Also, in reality, I'm maintaining internal state using a single property `state`, the loading is just one of several states. I guess `state` could be another internal `Observable`, as per this [plnkr](http://plnkr.co/edit/OiBSDA8Kcsgl4vBF15xW?p=preview). Multiple `state` values could be pushed to it over time. – Philip Bulley Feb 14 '16 at 11:05
  • 3
    @PhilipBulley Also note that Angular2 forms have built-in observables (`valueChanges` property). You might wanna look into that too... – Sasxa Feb 14 '16 at 11:33
2

With this ChangeDetectionStrategy.OnPush you are telling your component to only listen for changes on it's input properties.

I added loading component to your example just to show you how it works: http://plnkr.co/edit/k5tkmcLlzkwz4t5ifqFD?p=preview

Vlado Tesanovic
  • 6,369
  • 2
  • 20
  • 31
  • Thanks for your demo, Vlado. Creating a nested component just to detect a change isn't a great solution, but it does illustrate your point nicely. – Philip Bulley Feb 14 '16 at 10:40
  • Actually, I don't know why `loadingMessage` is not rendered to `test`, as we set the value to `test` in last the `setTimeout`. Could someone help me explain this? – Ricky Jiao Jul 17 '17 at 05:56