Change Detection without Change Detection
Tags: javascript, angular, rxjs, typescript
Published: 2022-05-23 06:12:47

_Original cover photo by [Adi Goldstein](https://unsplash.com/@adigold1) 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:

```ts
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:

```ts
@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](https://netbasal.com/unleash-the-power-of-di-functions-in-angular-2eb9f2697d66) by [Netanel Basal](https://netbasal.medium.com/) 

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:

```ts
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:

```ts
@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

```ts
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:

```ts
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:

```ts
// instead of this
this.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:

```ts
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.