I recently summarized some ways to make Components more elegant, and I’d like to share them here. Of course, it would be elegant to import NgRx and then reduce all the logic to the Reducer. But here’s a peek at some of the Component optimizations.

OnPush Detection

ChangeDetection: ChangeDetectionStrategy OnPush, how to say, we can as far as possible is in the new component is set, OnPush, less reliant for dirty checking. Dirty checking is a Magic thing for us a lot of the time, and we don’t really know what’s going on. Minimizing your reliance on dirty checking can improve performance on the one hand, but it can also help us understand more clearly when HTML is being rerendered and when it isn’t. Of course, using OnPush requires that we find a way to tell Angular that the change has occurred. However, you can set it up in the new Feature, and you will find that some examples that used to render successfully now don’t work. You can try to find out why, or at least get a better understanding of the dirty checking mechanism.

Event Trigger => Observable

It is very common to use the Event binding function during development, (click)=”onClickHandler($Event)”, something like this. An effective way of thinking is to convert events to Event streams to handle:

fromEvent(this.elementRef.nativeElement, 'click').pipe(
  switchMap(() => {
    //xxxxxxxxxxxxxx
  })
)
Copy the code

The advantage of this, of course, is that there are more flexible processing options, such as switchMap. We’ll talk about it in detail.

LifeCycle => Observable

React and Vue both adopt similar strategies. That is, we no longer need to split logic into different Life Cycle functions to reduce repeated logic calls and make experience more intuitive. Here’s an example:

private ngViewInit$ = new Subject<boolean>(); private ngDestroy$ = new Subject<boolean>(); private ngInit$ = new Subject<boolean>(); / /... / /... ngAfterViewInit(): void { this.ngViewInit$.next(true); } ngOnDestroy(): void { this.ngDestroy$.next(true); }Copy the code

In this way, we can write together the code that would have been separated into different Life Cycle functions, such as the Event listener. There is a very simple requirement that the List is displayed when the page is loaded, and the List is displayed when the Refresh Button is clicked:

const refreshButtonEvent$ = this.ngViewInit$.pipe( switchMapTo(fromEvent(this.elementRef.nativeElement, 'click')), ) const list$= merge(this.ngInit$, refreshButtonEvent$);Copy the code

It’s not hard to see that you no longer need to translate requirements into programmatic click events, but rather sequential programming like natural language.

Of course, the current disadvantage is that the conversion of Life Cycle function still needs to be realized manually.

Component distinguishes View Model variables from other variables:

How do you explain that? Because Angular binds this of the Component class to the Template, this poses a problem. Some properties are defined to display in the Template, and some variables are stored so that they can be called across different methods and share state.

In fact, one of the improvements is that only the ones used in the Template are defined as public, and everything else is private. This still results in a lot of properties for each Component. In essence, it is so convenient that the definition person often defines a property as a Component global without thinking about it. In fact, this is very similar to the use of global variables, which is not easy to split logic.

Recently, I tried to learn the React method. All properties used in Template are placed in State. On the one hand, the definition of public property is reduced, on the other hand, it is clear which properties are used in Template. An added benefit is that you can combine observables into one so that you don’t have too many Async pipes in the Template. Here’s an example:

const foods$ = xxxxx
const users$ = xxxxx

this.state$ = combineLatest(foods$, users$);
Copy the code

Of course, there are some problems with defining State this way:

  • State is an array, not an Object
  • The first emit of State is to wait for all Observables to emit at least once, which is not what we expected.

Our default initial value is probably more like:

{ foods: null, users: null, } // ..... { foods: [....] , users: null, } //..... { foods: [....] , users: [....] ,}Copy the code

Of course, it’s actually pretty easy to fix:

function createState(observables: Observable<any>[], projector: (subStates: Array<any>) => { [key: string]: any }) {
    const initialState$ = combineLatest(
        observables.map((observable) => observable.pipe(startWith(null)))
    );

    return projector ? initialState$.pipe(map(projector)) : initialState$;
}
Copy the code

It’s also easy to use:

this.state$ = createState([foods$, users$], ([foods, users]) => ({
  foods, 
  users,
}));

<div *ngIf="state$ | async as state">
  {{ state.foods | json }}
  {{ state.users | json }}
</div>
Copy the code

If we can make sure that all data in the Template can be retrieved from State or @Input, and State can make sure that all data sources are Observables and not ordinary variables directly from this, we can rest assured. A Component with ChangeDetectionStrategy OnPush. The template will only need to be rerendered if the Component props changes or the State emits new State.

Use immutable whenever possible

We often have trouble with immutable when using Angular. Here’s a simple example:

// parent.component.ts
this.users.push(new User());

// parent.component.html
<app-child [users]="users"></app-child>

----------------------------------------

// child.component.ts
ngOnChanges(changes: SimpleChanges) {
  console.log(changes?.users?.currentValue)
}

// child.component.html
{{users | json}}

Copy the code

You’ll notice that the Child Component correctly detects the newly added user, but ngOnChanges does not detect the changes. What is the reason for this?

This is simple because Angular’s dirty check is enough to re-render users, but because we used push, change Detection didn’t sense the change. If you set it to OnPush, you’ll see that the page doesn’t change. When the Template is inconsistent with the actual code, we can have some unexpected problems. So instead of using array push, we recommend using:

this.users = [...users, new User];
Copy the code

On the front end, it is not recommended to modify the array or object itself because of dynamic rendering. Always return the new address after the change. . Although this simplifies our operations, it would be much easier to use immutible.js or immer.js.

As little as possible, or no subscribe

Angular does not currently provide lifeCycle observeble, so in theory we need to manually unsubscribe subscription every time we destroy component. Sometimes we forget to unsubscribe, and sometimes we argue over whether a particular subscription needs unsubscribe. Let’s look at an example:

ngOnInit(): void {
  this.userService.get().subscribe((users) => {
    this.users = users;
  });
}
Copy the code

First, whether one Emit Value needs to be unsubscribe is itself a controversial topic. Suppose, then, we add the logic of unsubscribe:

ngOnInit(): void { this.subscriptions.push( this.userService.get().subscribe((users) => { this.users = users; })); } ngOnDestroy(): void { this.subscriptions.forEach(sub => sub.unsubscribe()); }Copy the code

You can see that the logic gets very complicated. Using async Pipe to let the platform handle it greatly simplifies the logic.

In fact, in my work, I found that many times, there are even a lot of subscribe nesting in the process of using, in fact, this is the same meaning as the then before the promise, should be avoided as far as possible.

If you find that there is a lot of code like this in your code:

this.service.getUser('zhangsan')
  .subscribe(user => {
    user.gender = 'male';
    this.service.updateUser(user).subscribe(user => {
      this.isUserUpdated = true;
    });
  }, () => {
    this.isUserUpdated = false;
  });
Copy the code

This becomes very unobservable. It goes against Angular’s intent to convert promises into Observables. If you’re familiar with this way of writing, converting promises to Observables makes more sense, and you don’t have to deal with unsubscribe:

try { const user = await this.service.getUser('zhangsan').toPromise(); this.isUserUpdated = await this.service.updateUser({... user, gender: 'male'}); } catch(){ this.isUserUpdated = false; }Copy the code

Of course, we can also make the code more observable, which will show its advantages:

this.isUserUpdated$ = this.service.getUser('zhangsan').pipe( switchMap(user => this.service.updateUser({ ... user, gender: 'male' })), map(() => true;) ; catchError(() => of(false)); )Copy the code

Either way, asynchronous processing reduces nesting and makes your code simpler and cleaner. It has been said that if you find a subscribe nested in your code, or even write more than twice a SUBSCRIBE in the same Component, you may need to re-read your code.