Home > Software engineering >  Function call in templates or precalculate needed values?
Function call in templates or precalculate needed values?

Time:09-18

I read that function calls in templates are not very efficient since Angular's default change detection will execute that function every time. Are function calls okay if one uses the OnPush strategy? I am using NGXS for state management and the concept of dumb/smart components/containers. Should I preprocess objects so that they have already the values needed? Here is an example:

<div *ngFor="let object of objects">
    <input type="checkbox" [checked]="isObjectChecked(object)">
</div>
isObjectChecked(object){
    // if object is in the array, then check the checkbox
    return selectedObjects.some(obj => obj.id === object.id)
}

Since I am using NGXS I pass objects from the container to the dumb component using @Input(). Is this approach better:

// in container 
ngOnInit(){
        this.objectsWithIsCheckedProperty$ =  combineLatest([this.store.select(ObjectsState.objects), this.store.select(ObjectsState.selectedObjects)])
        .pipe(
            map(data => {
                let objects = data[0];
                let selectedObjects = data[1];
                objects.forEach(obj => {
                    obj.isChecked = selectedObjects.some(object => obj.id === object.id);
                });
                return objects;
            })
        )
    }

and then use the isChecked value in the template

<div *ngFor="let object of objects">
    <input type="checkbox" [checked]="object.isChecked">
</div>

Would it make a difference if I use the OnPush change detection strategy with function calls in templates since I have to explicitly tell that Angular should check whether variables changed?

CodePudding user response:

Alternative 1: Functions in the template

I strongly recommend not using functions when binding to your template. Even if you use ChangeDetectionStrategy.OnPush, these functions will be reevaluated whenever a change detection cycle is triggered on your component (e.g. when any @Input changes, any event you're listending on is triggered on your component, asynchronous tasks on your component complete).

As an example, imagine you have the following Component:

@Component({
  template: `
    <span>{{ myBoundFunc() }}</span>
    <button (click)="doSomething()"
  `
})
export class MyComponent {
  myBoundFunc() {
    console.log('Bound function called');
  }
  doSomething() {}
}

Now if you click the button, myBoundFunc will be called as well since clicking the button triggers a change detection cycle - the result of myBoundFunc might change due to your click after all.

This might be okay performance-wise in this example, but it is a bad practice and will lead to performance issues if you do something more demanding in that function.

Alternative 2: Precalculate the value in your Observable

This is a good solution, since your logic will only run when the underlying observable receives a new value. Performance-wise, this is miles better than alternative 1. I'd still recommend doing this in an immutable way, since this works better with Angular's change detection (which only checks for reference equality).

I'd change your example solution to something like this to make it immutable:

ngOnInit() {
  this.objectsWithIsCheckedProperty$ = combineLatest([
    this.store.select(ObjectsState.objects),
    this.store.select(ObjectsState.selectedObjects)
  ]).pipe(
    map(([objects, selectedObjects]) => 
      // Use .map and spread the obj into a new object, adding the isChecked property in the process
      objects.map(obj => ({
        ...obj,
        isChecked: selectedObjects.some(({id}) => obj.id === id)
      }))
    )
  )
}

Alternative 3: Precalculate the value in NGXS @Selector

Another way to precalculate the value is using an NGXS @Selector, which also transfers the logic to your state, making it reusable across your application. Performance-wise this should be about the same as alternative 2.

The state:

@Injectable()
@State<ObjectStateModel>({
  name: 'object'
})
export class ObjectState {
  // These 2 already seem to exist in your state
  @Selector() public static objects(state: ObjectStateModel) {}
  @Selector() public static selectedObjects(state: ObjectStateModel) {}
  // Or with "selectorOptions.injectContainerState = false"
  @Selector([ObjectState]) public static objects(state: ObjectStateModel) {}
  @Selector([ObjectState]) public static selectedObjects(state: ObjectStateModel) {}

  // The new selector that does the precalculation. Depending on if you have
  // "selectorOptions.injectContainerState" set to true or false, the first
  // parameter will always be the state or not. I'll add both here.

  // With "selectorOptions.injectContainerState = true" (default)
  @Selector([ObjectState.objects, ObjectState.selectedObjects])
  public static objectsWithIsCheckedProperty(state, objects, selectedObjects) {
    return objects.map(obj => ({
      ...obj,
      isChecked: selectedObjects.some(({id}) => obj.id === id)
    }))
  }

  // With "selectorOptions.injectContainerState = false" (recommended)
  @Selector([ObjectState.objects, ObjectState.selectedObjects])
  // Notice that "state" is not provided by default anymore
  public static objectsWithIsCheckedProperty(objects, selectedObjects) {
    return objects.map(obj => ({
      ...obj,
      isChecked: selectedObjects.some(({id}) => obj.id === id)
    }))
  } 
}

And in your Component:

ngOnInit() {
  this.objectsWithIsCheckedProperty$ = this.store.select(ObjectState.objectsWithIsCheckedProperty);
}
  • Related