Change detection in Angular 101
I often see people being sorts of scared of change detection in Angular as if it were a miraculous superpower that can’t be handled by a mere developer.
First, we are hit by an ExpressionHasChanged error, then we are hit by an interview where we get destroyed by questions about change detections and the fear starts to grow. However, I must assure you that it’s quite simple, once you sort it out and that’s what we will do today.
We won’t be able to cover all the topics about change detection as the topic itself is huge when all the details are covered, nonetheless, I’ll explain the main concepts and differences you need to know on an everyday basis.
What is change detection?
It’s a special mechanism in Angular that allows it to know what, where and when to update on the screen. It’s important to remember that it only checks the template bindings, which means that unless you use your class’s properties in the template they won’t be compared during change detection cycles.
How is it different from Virtual DOM in React?
In React you must use setState
or use context providers to explicitly declare the data change. When done so, React builds a DOM tree which is basically a bunch of JS objects representing rendered elements.
Angular, on the other hand, does not build whole elements representations, instead, it compares the values that you bind to the template.
This gives us a lesser runtime memory consumption as having several values to check against their new values are much lighter than building JS object representing whole elements.
How does Angular know when to run change detection?
It’s quite easy. Any changes to a component’s state you could do is always a result of some event. It’s either a callback for the click event or a callback for the setTimeout.
The change detection is triggered by all sorts of browser events: HTTP requests, web sockets, user events (click etc), timers and so on. It’s done so by using Zone.js library which monkey patches browser APIs and Angular hooks into it, then after an event happens Zones tell Angular that it’s most probably that something has changed and Angular starts the process.
If you check the documentation, you will see that there is a “magic” method called tick
, that’s how the change detection is triggered for you by Angular+Zones combination.
Change detection strategy
It’s often said that there are 2 strategies: Default and OnPush. However, I would say that it has 2.5 strategies. Half, because you could disable Zones or detach the whole app from the change detection tree, thus, making you fully responsible for the change detection.
Default strategy
That’s what you get out of the box. You don’t need to think about how you will tell the application when to update the rendered templates. It always works as an almighty app that is always aware of when and what to update.
On each event happening in the browser, it will run a change detection which will go checking your app tree for changes.
This strategy is considered to be not ideal as it checks the whole tree and even branches of components that don’t have anything to do with the data changes. That’s the trade-off for the simplicity of state updates.
I can assure you that if you go with the Default change detection strategy it’s not necessarily that you would encounter some performance issues. I’ve built quite a large app that could open virtual in-app tabs with huge forms (20+ fields) and it was working perfectly even on an old laptop.
The only performance hit that we discovered and on that laptop only (it was really old) was caused by someone binding to the template base64 image of a user which might be about 2 Mb+. As soon as we fixed that it was blazing fast.
On the example app, you can see how all elements are updated. We will refer to this example when exploring the OnPush strategy.
OnPush strategy
OnPush is called that not just for say. Unless you really “push” your component it won’t update.
When you apply this strategy the component that it’s applied to and its underlying tree of components won’t be checked for changes unless:
- This component’s
@Input
s references change - There was a DOM event in this component or its children
- Invoked
markForCheck
ordetectChanges
Let’s get back to our example app and put the OnPush strategy to the A component.
Now, none of the components has their counter updated but the app component, as it is still under the Default strategy.
You might be wondering why we don’t have updates in the underlying components, but let’s refer to the items above. We don’t satisfy any of them. Therefore the change detection isn’t called for them.
However, know that those timers still lead to calls of tick()
s, so if you actively work with events like WebSockets you might be better considering working outside Angular zones.
OnPush strategy quirks
Let’s consider several interesting behaviours which aren’t quite documented.
1. If you click inside blocks they won’t rerender.
You might wonder why, as it’s said that DOM events from within the component’s tree should make it dirty and thus re-render the template.
Unless you listen for those events using Angular’s bindings it won’t be causing any updates.
2. If you define the OnPush strategy on a component the underlying tree is still using the Default strategy
Take a look at this example
Here I use the previous app with a small change — I added markForCheck
in the component C timer.
Let’s take a deeper look at this. We still have the OnPush strategy set on component A, however, all the underlying components use the Default strategy. This means that as soon as we satisfy one of the three conditions stated above for component A its whole tree will be checked.
Therefore, if you want to have the app have as few checks as possible you should specify the OnPush strategy on all components.
3. ChangeDetectorRef#detectChanges causes updates in its subtree only
This is the default behaviour and is documented, though I wanted to show what it looks like.
In this example, we have the conditions from example #1 of the OnPush strategy with only change in component B as it calls detectChanges
.
Note that component C is always one count behind component B. It happens because detectChanges
is synchronous and for the time the first counter advanced the child’s counter didn’t yet update its component’s value.
4. If you disable Zones markForCheck
won’t work as you might expect it to
It happens because there are no more Zones that would produce ticks and therefore the change detection is never run.
MarkForCheck VS DetectChanges
markForCheck
is the safest method to use. It only marks your component and its parents as “dirty” which means that on the next app.tick() (change detection cycle) it will be checked for changes in template bindings.
detectChanges
is usually used when implementing one’s own change detection strategy. It will immediately start the change detection cycle on the component that is called it and all its children.
I strongly advise against detectChanges
, because it’s synchronous and multiple calls will cause performance issues that wouldn’t be there even with the Default strategy. Don’t use it unless you are implementing your own change detection mechanics. On the other hand, markForCheck
doesn’t start the change detection cycle on its own and therefore is safe to use.
“Expression has changed” error
This error is usually caused when you change some value that is bound to a template changed after the change detection.
The most common case is when you update a value synchronously in the ngAfterViewInit
hook. You shouldn’t do that, because this hook is called after the component’s view is checked for changes.
What does this mean? In Angular, you should follow the unidirectional data flow. That means that data flows from the top to the bottom. When a value in the component changes after it has been checked but the cycle hasn’t been finished yet it tells Angular that there is something wrong. Most probably a child has updated its parent’s property.
Also, it might be evidence that the property has been changed as a result of binding in the template (e.g. you did something like this [value]="counter++"
).
These two cases and perhaps some more point to the inconsistencies between renders. That we caused changes we didn’t intend to. This might cause our app to enter an unexpected state and produce lots of bugs and even cost us users or money. We don’t want that, right?
How to fix it?
- Check if you update synchronously values in the
ngAfterViewInit
hook, if possible move those to thengOnInit
hook - If you need to sync a value between a parent and its child and the child is responsible for the value initialization, make
Output()
async. It will delay the event handling in the parent and thus fix the issue - If it’s caused by an observable which emits value in the window between change detection started and ended you could use asyncScheduler to make the value emission asynchronous.
- As a last resort, you could use
setTimeout
to update the value - Another last resort is to call
detectChanges()
at the end of thengAfterViewInit
hook, however, I recommend reconsidering the data updates
Looking for more resources to get more understanding of how change detection works? Check these resources
Expression has changed after it was checked in the official docs.
Take a quiz to learn how good you know the change detection in Angular.