Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
May 23, 2022 06:12 am GMT

Change Detection without Change Detection

Original cover photo by Adi Goldstein on Unsplash.

What's the problem?

In Angular, we have the powerful change detection mechanism to help us rerender the UI when data changes.
In simple terms, this works in the following way:

  1. We assume state only changes on async events (clicks and other browser event, Promise resolve, setTimeout/setInterval )
  2. Angular uses zone.js to monkey patch async events
  3. When an async event happens, Angular calls the change detector
  4. The change detector traverses the tree of components and checks if any of the data has changed
  5. If so, it rerenders the UI

This process is overall known as change detection. Notice that the change detector will definitely be invoked in situations where no changes have been made at all, making it less efficient than we would ideally want.

We can do some optimizations, like using the ChangeDetectionStrategyOnPush to help the change detector work better. Or we can detach the change detector from some components if we know that they do not need change detection (a very rare scenario).

But can anything be done to make this work better? We know we can manually trigger the change detection process via a reference to the change detector (the ChangeDetectorRef class).

But how do we recognize when we need to manually trigger the change detection process? How do we know a property has changed? Also, how do we obtain the change detector reference outside of a component, so we can solve this problem with a generic function?

Let's try and address all of these questions using the new features provided by Angular version 14, and some JavaScript magic.

Disclaimer: the following code examples are an experiment, and I do not encourage you to try using this in production code (at least yet). But this approach is an interesting avenue to investigate

Enter Proxy objects

If you are unfamiliar with Proxy objects, as we are going to use them, let's explore them a bit. Proxy in JavaScript is a specific class, which wraps around a custom object, and allows us to define a custom getter/setter function for all properties of the wrapped object, while simultaneously from the outside world, the object looks and behaves as a usual object. Here is an example of a Proxy object:

const obj = new Proxy({text: 'Hello!'}, {    set: (target, property: string, value) => {        console.log('changing');        (target as Record<string, any>)[property] = value;        return true;    },    get(target, property: string) {        // just return the state property          return (target as Record<string, any>)[property];    },});console.log(obj.text); // logs 'Hello!'obj.text = 'Bye!'; // logs 'changing' and 'World' because the setter function is called

Now, what if we have Proxy objects in our app, which will call the change detector manually when the properties are changed? The only remaining caveat is obtaining the reference to the specific component's change detector reference. Thankfully, this is now possible with the new inject function provided in Angular version 14.

Inject?

inject is a function that allows us to obtain a reference to a specific token from the currently active injector. It takes a dependency token (most commonly a service class or something similar) as a parameter, and returns the reference to that. It can be used in dependency injection contexts like services, directives, and components. Here is a small example of how this can work:

@Injectable()class MyService {    http = inject(HttpClient);    getData() {        this.http.get('my-url'); // no constructor injection    }}

Aside from this, we can also use this in other functions, provided these functions are called from DI contexts as mentioned. Read more about the inject function in this awesome article by Netanel Basal

Now, with this knowledge, next we are going to create a function that helps us ditch the automatic change detection but still use Angular (more or less) as usual.

So what's the solution?

We are going to create a function that makes a proxy of an object which manually triggers the change detection process when a property is changed. It will function as follows:

  1. Obtain a reference to the change detector of the component
  2. detach the change detector; we don't need automatic change detection
  3. using setTimeout, perform the change detection once after the function is done (so that initial state is reflected in the UI)
  4. Create a proxy from the plain object
  5. When an object property is called (get), we will just return the value
  6. When an object property is set, we will set the value and manually trigger the change detection
  7. Observe how the UI changes

Here is the full example:

function useState<State extends Record<string, any>>(state: State) {    const cdRef = inject(ChangeDetectorRef);    cdRef.detach(); // we don't need automatic change detection    setTimeout(() => cdRef.detectChanges());     // detect the very first changes when the state initializes    return new Proxy(state, {        set: (target, property: string, value) => {            (target as Record<string, any>)[property] = value;             // change the state            cdRef.detectChanges();            // manually trigger the change detection            return true;        },        get(target, property: string) {            // just return the state property            return (target as Record<string, any>)[property];        },    });}

Now, let's see how this in action:

@Component({    selector: "my-component",    template: `    <div>        {{text}}    </div>    <button (click)="onClick()">Click me!</button>    `})export class MyComponent {    vm = useState({text: 'Hello, World!'}); // now we have a state    onClick() {        this.vm.text = "Hello Angular";        // works as expected, changes are detected    }    get text() {        console.log('working');        return this.vm.text;    }}

Now this works as any other Angular component would work, but it won't be checked for changes on other change detection iterations.

Caveats

Nested plain objects

Nested object property changes won't trigger a UI update, for example

this.vm.user.name = 'Armen';

Won't trigger change detection. Now, we can make our function recursive so that it makes a sport of "deep" Proxy
object to circumvent this constraint. Or, otherwise, we can set a new reference to the first-level object instead:

this.vm.user = {...this.vm.user, name: 'Armen'};

I personally prefer the latter approach, because it is more explicit and does not involve nested object mutations.

Array methods

With this approach, we cannot count on functions like Array.push to update the DOM, instead we would need to do the same thing as in the previous example:

// instead of thisthis.vm.item.push(item);// we will have to do this:this.vm.items = [...this.vm.items, item];

Input properties

As we have detached the change detector, if the component has properties decorated with @Input(), the change detection will not be triggered and we won't see new values from the outside world. We can circumvent this using this approach:

export class MyComponent implements OnChanges {    @Input() value = '';    vm = useState({text: 'Hello, World!'}); // now we have a state    cdRef = inject(ChangeDetectorRef);    onClick() {        // works as expected, changes are detected        this.vm.text = "Hello Angular";    }    ngOnChanges() {        // detect input changes manually        this.cdRef.detectChanges();    }}

This solves the problem, but does not look very pretty.

In Conclusion

This approach is, of course, experimental, but it provides an interesting insight into how Angular operates, and how we can make tweaks to boost performance without sacrificing code quality.


Original Link: https://dev.to/this-is-angular/change-detection-without-change-detection-5pa

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To