I Have A Fundamental Misunderstanding Of Change Detection In Angular 2 Beta 8
Learning Angular 2 is, without a doubt, an uphill battle. Thankfully, most of the rock scrambles result in beautiful scenic overlooks. But, the one peak that I have yet to climb is the concept of change detection. For weeks, I have been baffled by errors in change detection using ngModel. But, at least in that case, I was actually trying to do something complex. This morning, however, I wasn't trying to do anything clever. In fact, I was barely trying to doing anything at all. And yet, I am still getting change detection errors. Right now, I feel like I need to get in my car, drive to Ikea, buy a desk, drive home, assemble the desk, and then flip it out of sheer frustration! Clearly, I have a fundamental misunderstanding of change detection in Angular 2, and it's making me feel hella dumb!
Run this demo in my JavaScript Demos project on GitHub.
To put a face to my frustration, I was experimenting with the ViewChildren feature in which Angular 2 will inject the component controller with an iterator containing embedded component instances. In my experiment, all I am doing is trying to set up the ViewChildren metadata and then echo out the number of embedded components that were found within the rendered view:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
I Have A Fundamental Misunderstanding Of Change Detection In Angular 2 Beta 8
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
I Have A Fundamental Misunderstanding Of Change Detection In Angular 2 Beta 8
</h1>
<my-app>
Loading...
</my-app>
<!-- Load demo scripts. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/8/es6-shim.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/8/Rx.umd.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/8/angular2-polyfills.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/8/angular2-all.umd.js"></script>
<!-- AlmondJS - minimal implementation of RequireJS. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/8/almond.js"></script>
<script type="text/javascript">
// Defer bootstrapping until all of the components have been declared.
// --
// NOTE: Not all components have to be required here since they will be
// implicitly required by other components.
requirejs(
[ /* Using require() for better readability. */ ],
function run() {
var App = require( "App" );
ng.platform.browser.bootstrap( App );
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide the root App component.
define(
"App",
function registerApp() {
var Item = require( "Item" );
// Configure the App component definition.
ng.core
.Component({
selector: "my-app",
directives: [ Item ],
// Here, we are asking Angular to inject a QueryList for the
// collection of Item directives rendered within our component
// view. This will inject `itemList` as a public property on
// the App component instance.
// --
// CAUTION: View queries are set before the ngAfterViewInit()
// life-cycle method is called, but after the ngOnInit() life-
// cycle method is called. As such, the `itemList` property will
// not exist until the View is initialized.
queries: {
itemList: new ng.core.ViewChildren( Item )
},
// Notice that our view has 2 Item instances. And, that we are
// using the injected `itemList` property to display the count
// of items currently rendered in the view.
template:
`
<h2>
Item Count: {{ itemList?.length }}
</h2>
<item></item>
<item></item>
`
})
.Class({
constructor: AppController
})
;
return( AppController );
// I control the App component.
function AppController() {
// ... nothing to do here.
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a super simple Component for no other purpose than to have a
// directive that can be rendered in the App component.
define(
"Item",
function registerItem() {
// Configure the Item component definition.
return ng.core
.Component({
selector: "item",
template: "This is an Item!"
})
.Class({
constructor: function ItemController() { /* Nothing to do. */ }
})
;
}
);
</script>
</body>
</html>
As you can see, I'm not really doing anything. I'm only taking the data that Angular gives me and then I'm rendering it in my view. My controllers have zero logic. And yet, when I run this page, I get an error:
What don't I get? Where is my misunderstanding? Where is there even any opportunity for wiggle room in the logic? Why does Angular 2 change detection continue to confound and befuddle me?
And, so help me, if anyone even dares suggesting that I try using OnPush change detection, I will freaking curl up in a ball on the floor and start crying! Please don't make me cry. Change detection strategies are about optimization, not about avoiding errors.
The same goes for putting the code in "Production Mode". All that does it "cover up" the error, not avoid it.
Ok, sorry for the rant. I'm just feeling very frustrated right now because I cannot seem to close this knowledge gap on change detection. It's been weeks of experimenting and reading and looking at source code and still, I simply don't get it. I am feeling very less than intelligent right now. And I'm acting out. So I appreciate your patience during my temper tantrum.
Want to use code from this post? Check out the license.
Reader Comments
https://github.com/angular/angular/issues/6005
@Yuri,
Ironically, I actually posted a comment in that thread back in January in relation to my ngModel frustrations. But, in all of the cases in that thread, the developer seems to be trying to *do something* in their controllers, like response to the rendering of a view to set a value or change a host binding. In my case, I'm not doing *anything*. My controllers have no logic - there's no "hook" for me even trigger a .tick() event or what have you.
I am at a loss :(
@Ben,
As an workaround you can do something like:
```
ng.core
.Component({
selector: "my-app",
directives: [ Item ],
// Here, we are asking Angular to inject a QueryList for the
// collection of Item directives rendered within our component
// view. This will inject `itemList` as a public property on
// the App component instance.
// --
// CAUTION: View queries are set before the ngAfterViewInit()
// life-cycle method is called, but after the ngOnInit() life-
// cycle method is called. As such, the `itemList` property will
// not exist until the View is initialized.
queries: {
itemList: new ng.core.ViewChildren( Item )
},
// Notice that our view has 2 Item instances. And, that we are
// using the injected `itemList` property to display the count
// of items currently rendered in the view.
template:
`
<h2>
Item Count: {{ count }}
</h2>
<item></item>
<item></item>
`
})
.Class({
//count: 1,
ngAfterContentInit() {
console.log('// parent is initialized');
console.log(this);
},
ngAfterViewInit() {
var self = this;
setTimeout(function(){
self.count = self.itemList.length;
console.log('// parent is initialized');
});
console.log('// parent is initialized');
},
constructor: AppController
})
;
```
@Ben, Sorry, I thought that I could post some markdown code
@Yuri,
Yeah, sorry about that :( I've been meaning to fix that at *some point* :( Just never got around to it - since the blog platform is a home-grown.
I appreciate your work-around; but, really it's just getting me to do *something* as a work around for having to do *nothing*, which is super frustrating. So, at that point, I think there are two options:
- Change detection is befuddling complex.
- There is a BUG in change detection.
The only reason I am against it being a BUG is because no one from the Angular team seems to acknowledge it as being a bug in any of those related issue. So, that makes me think I am just doing something wrong.
You are not using lifecycle hooks, so i don't see how it could be your code doing this. You mention that you have been having change detection errors for weeks, thats strange. You are using the umd modules but that must not have anything to do with it.
The problem might be that QueryList is not meant to be used directly in the template, and thats not documented anywhere.
Does it work by subscribing to the list instead of using it directly in the template? And then act on the callback of the result by setting a variable that gets passed to the template.
Does that work ? And thank you for these articles, very informative and its the best place to see what ES5 code looks like all other examples out there are in Typescript. Thx
Forgot to post this, does it help ? -> http://blog.jhades.org/how-does-angular-2-change-detection-really-work/
with this its possible to debug this, although it for sure won't be easy
"So, at that point, I think there are two options:
- Change detection is befuddling complex.
- There is a BUG in change detection.
And @Ben, the answer for me is there has been a BUG in change detection.
Have you tried this on beta.03 ? I am soo afraid of getting past beta.03 !
+1 to: if anyone even dares suggesting that I try using OnPush change detection, I will freaking curl up in a ball on the floor and start crying!
You're triggering a change detection loop. The view-bound model object itemList is null after init. In the same VM turn the view initializes which adds Item components which necessarily causes the itemList query to trigger which updates a view-bound model object.
It sounds like it might be time to switch to Aurelia, Ben.
All joking aside, I think this post really highlights one of the many shortcomings in Angular 2. While it is arguably better than Angular 1.x as they have done away with some of the confusing concepts and issues that plagued the underlying architecture, Angular 2 falls back into old habits. For every reason I see to switch to Angular 2, I see a couple more reasons to avoid it entirely.
How did Angular 2 even get to this point? I feel as though Angular 2 is a conspiracy to drive people away from Javascript frameworks entirely and to libraries like React and Polymer instead. If that's the goal, I think they're succeeding.
I see Angular 2 as the abusive partner. It swears it loves you, heck, it even went to see a counsellor. For a brief moment in time Angular 2 starts to make good on its promise to change for the better, you feel like it finally started listening to your complaints. But then Angular 2 starts falling back into its old ways again.
I hope you resolve your problem, Ben. Sorry I don't have much to offer on the subject. Seeing posts like this just frustrates me because the Angular team had a great opportunity with the rewrite to make an intuitive Javascript framework that removed the confusion and complexity of Angular 1.x, instead Angular 2 currently feels like the engineers working on the project had a little too much say and not enough community consultation.
@Jhadesdev,
I haven't tried using the .changes.subscribe approach in this particular demo; but, I believe Pascal Precht said it would cause the same problem since a change is being created after the change detection is run. Unless, of course, you put in a setTimeout() like Yuri suggested.
@Anas,
I haven't tried this demo on any earlier betas. But, I have tried my ngModel demo on every beta since 2 and they all throw the same modified value error :(
@Peter,
> You're triggering a change detection loop.
... just to be clear, I believe that the phrasing "You're" should be replaced with "Angular is". I am not doing anything - I am simply taking the variable that Angular is injecting and am using it in the template. If this its possible that this can cause a problem, then Angular should be responsible for waiting a tick or something that is safer for change detection.
@Dwayne,
To be fair, I don't write as much about the stuff that "just goes right" :D But, I will say that while the syntax is smaller for Angular 2, I do find that the mental model needs to be far more robust in order to think about the aspects of an Angular 2 application. I mean, case in point, I've been digging into Angular 2 for about a month and a half and I have not even started looking at how Routing works yet (which is, itself, likely to be a large topic with lazy loading).
But, I'm hesitant to say that it's any worse than something like React (the only other library that I've really dug into in the past). React, like Angular 2, has a fairly small component API. But, even at the end of my React studies, I don't think I actually knew how to build a React App - sure I knew how to render some components and deal with Refs and think about controlled and uncontrolled components. But, that's such a tiny fraction of what is required think about and build a full featured application.
All I'm saying is that I think every framework is probably good in its own ways and frustrating in its own ways.
That said, I've heard a lot of good things about Aurelia - it would be fun to dig into it a little bit, if for no other reason to get a sense about how Rob thinks / reasons about application development.
@Ben,
I've tried it and indeed doesn't work. At this point i have the impression that inside both lifecycle callback methods ngAfterViewInit and ngAfterContentInit we can't really do anything that mutates something displayed on the template ! As it will trigger change detection loops.
There is this issue, reopened by an Angular team member: https://github.com/angular/angular/issues/5950
and one of the comments mentions:
> I'm re-opening this one since model mutation in lifecycle callbacks needs more thinking
@Dwayne
There are a few hiccups, but oh gosh is angular 2 glorious. The hardest part was setting up dev environment (angular cli will help with that), but after that, my productivity is really awesome and the there is a noticable difference with performance, event when I haven't tapped into OnPush change detection strategy and using immutable objects.
As I understand it, the core philosophy of Angular 2 is that data flows *down* the hierarchy of components, entering them via their inputs and then passing into their rendered view and child components. Generally, information should only travel *up* the hierarchy via events.
Think of it was a wave passing down the tree of components, starting at the root, application node, then moving down into each subsequent layer of components. At each layer, Angular 2 performs change detection and knows whether to re-render components and whether to update the inputs of the next layer down. And once a layer has received its inputs and detected any changes, it should be "settled". Angular should know for sure that it won't change again this cycle.
This makes it very easy for the framework, and programmers, to reason about data flow within the application. In a more dynamic MVC/MVVM framework, like Angular 1, changes to the model can trigger listeners in various views, which can make further changes to the model, and the effect can be endless ripples of changes flowing back and forth throughout the application.
However, in Angular 2, querylists allow a special-purpose violation of the downwards data flow. They allow a component to potentially receive and act upon information from *lower* in the hierarchy. This is necessary for certain types of components, such as containers, but it carries with it a risk of creating a cycle.
In the case of your example application, you haven't created a cycle, but you *have* created a situation where the rendered output of a component changes *within* a cycle, *after* it should have settled. When the my-app component is first loaded, the querylist is empty, then it renders its children, and the querylist value changes.
This might all seem fairly harmless. But imagine if you weren't just rendering the size of the querylist, but using it control the rendering of another component that was also subject to a querylist. Now you've just changed that querylist's value as well, and suddenly changes are flowing all over the component hierarchy, just like in Angular 1.
Or imagine you used the querylist length within an ng-if that only rendered the items if the querylist length == 0. Now you've created an endless cycle, whereby rendering the items makes them invisible, which makes them visible, and so on to infinity.
I think you can see the danger in all of this. Therefore, the values a component displays and passes into its children must not change again after they've been read as part of the change detection process. Querylists, but their very nature, violate this principle, as so require special handling. I think Angular 2 could definitely be improved in how it handles this case, but hopefully this makes it clearer why it is occurs.
Hi Ben.
Sorry for the frustration on this point. FWIW, I confronted your particular issue long ago when I wrote the sample for the "Lifecycle Hooks" chapter.
I wanted to display on screen when Angular calls the `OnViewChecked` hook. Got the same error. Had to come up with a workaround.
I'll disclose that workaround. But what good is a workaround without understanding?
IMO, there isn't a bug in change detection but in our understanding of it.
The "unidirectional data flow" rule is the key. Officially, A2 evaluates each component in the component tree exactly ONCE. It proceeds top down.
You have a parent and children. The parent, `App`, is displaying the number of child views (the instances of `Item`.
Q: When is that number of child item views known?
A: Only _after_ the child items are rendered.
But that's a cycle, right? A2 finishes evaluating the parent display and its child views and only then is the number of child views known. But you turn around try to update the parent that was already rendered.
That kind of thing is explicitly disallowed. We are entitled to ONE pass through the tree for each turn. You're asking for two: Parent, children, parent again.
A good way to see this is to add an AfterViewInit hook to your component and give it a `debugger` statement:
ngAfterViewInit() {
debugger
}
Now open your browser tools and look at the display. Notice that everything is drawn ... parent and two children ... except the count of child views. No error yet either.
Now let it go. Boom!
The unidirectional data flow rule is a big difference with A1. It bites us from time to time. But we can't say they hid it from us. They shout it from the roof tops.
You indicated your semi-awareness of this fact when you used the "elvis" operator to display the length:
Item Count: {{ itemList?.length }}
Clearly you understood that the queried itemList wouldn't be available on the first pass.
WHY THE ERROR?
In development mode, A2 makes a second pass to see if anything changed in violation of the rules. It's not perfect but it caught you. It detected that the displayed value was null the first time and '2' the second.
This 2-pass approach is supposed to help you catch mistakes in your code during development.
When you call `enableProdMode()`, you disable the 2-pass check and the reporting of the error disappears.
What's really curious is that it seems to work. You've violated the unidirectional data flow rule and yet it displays length '2'.
Although I can explain why it _shouldn't_ work, I can't (yet) explain why it works in this case. I'm pretty convinced it won't always work. But I guess it works some of the time.
I accept that it _shouldn't_ even if it occasionally does and take steps to _do it right_.
WORKAROUND:
You could cheat and `enableProdMode()`. You know better and if you did, that expedient will let you down some place (if not here).
I often use setTimeout to beat this one. In a static situation like this, I could call it inside the AfterViewInit hook:
// TypeScript
ngAfterViewInit() {
setTimeout( () => this.viewCount = this.itemList.length, 0);
}
You'd need something trickier if you expected the number of child items to change (using AfterViewChecked ... but watch out! ... it's called a lot and it's easy to recurse to death).
Maybe there's a more clever way. That's the best I have for you as I write today.
HTH.
My "solution" in a plunker:
http://plnkr.co/edit/xm50CY5uePYC6mJddwAu?p=preview
BTW, the reason that setTimeout works is because it waits one cycle before updating the parent `viewCount`. Waiting means it doesn't violate the unidirectional flow rule.
Based on the comments on issues in github, it looks like this will be tought through some more.
The ngAfterViewInit is useful to do things like integration with third party libraries, where we need a hook to know when the DOM is ready in order to trigger some external library on a given element.
For that type of code we won't run into issues. but then this lifecycle hook got linked to the use of ViewChildren and such, which was not the case initially. Before the queries simply got injected via constructor injection, which does not seem to be possible anymore.
The result is a strange situation: for using ViewChildren we need ngAfterViewInit, and if we use it we trigger change detection loops if we change anything used by the template, which will be almost all the time.
I hope like the comments mention that some improvement is made, because its the type of thing we wanted to avoid. Its a bit like having to sprinkle $cope.$apply() here and there. We can't use ViewChildren in a transparent way without having to reason about the framework internals, which is a pity because its a very commonly used functionality.
But all in all I love the new version of Angular and think its a huge step forward, there will always be some small gotchas here and there in any tools we use, and this for the moment looks like its one of those.
Or is there another way to use ViewChildren where we don't fall into this situation?
@Jhadesdev,
Ah, good to know. Thanks for pointing that out. I really haven't seen any indication that this is considered an "issue", so I'm glad someone pointed out that it might need some more consideration.
@Jon, @Ward,
I appreciate the in-depth discussion about one-way data flow. That definitely clarified why the error is actually being thrown. And, with something like ViewChild / ViewChildren, I can certainly understand it due to the very nature of what they represent (reactions to the view itself).
However, I would caveat that I do run into the very same issue with trying to configure ngModel to work with a custom control. I think it ends up creating something similar using the valueAccessor. I have a view that I've set with some sort of default value. Then, Angular 2 calls the `valueAccessor.writeValue( newValue )` and I use that `newValue` to update my view and BOOM, it all goes to pieces:
www.bennadel.com/blog/3007-failure-using-ngmodel-with-a-custom-component-in-angular-2-beta-1.htm
The reason (my theory) that ngModel doesn't explode for any of the native controls is that they don't use Views to render the controls - they use core DOM methods to update Input values. As such, it's not a matter of view evaluation having to run twice -- but, it really is the same violation, I think - it just doesn't happen to cause an error (because it's mutating the DOM directly).
And, as much as the Angular team is shouting the one-way data flow from the rooftops, it just seems odd that data provided by Angular is provided in a way that can violate that rule. Meaning, if one-way data flow is so important, why wouldn't Angular implement this setTimeout() workaround internally if it knows its providing dangerously timed data?
I will also concede that maybe these are just *edge cases* - custom controls and queries. If this is a "there be dragons!" situation, that's fine. I guess I'm just trying to understand where on the map the dragons live :D
@Ben,
Yes, that does seem strange, and I reached the same conclusion about how ngModel is interacting with the component it writes to. I created a reduced plunkr example and filed an Angular issue: https://github.com/angular/angular/issues/7461
It will be interesting to hear what the Angular devs have to say about this. I suspect their answer may be that ngModel is not intended to write to other Angular components. After all, if your component has inputs and change events, why not just bind directly to them using the standard template syntax, rather than writing a ngModel wrapper? But it does seem like this is likely to cause confusion amongst new ng2 developers.
@Ward: Thanks for the explanation and workaround. It does make sense in this scenario.
Although IMHO, setTimeout( () => this.viewCount = this.itemList.length, 0)
reminds me of $apply
IDK which word to use, it's a "problem" or a "benefit" of top-down data flow. Overall, unidirectional data flow makes a lot of sense.
@Jon,
I think adding the ngModel wrapper is important because that adds all of the ngForm / ngControl type interactions. Like adding the "pristine" and "dirty" CSS classes as you interact with the component. Without the ngModel wrapper, sure, the data flow will work; but, you won't get any of the niceties that other ngModel controls provide.
NOTE: That is mostly theoretical - I haven't really learned much about ngForm yet :D
@Dwayne,
I think you weren't really joking about switching to Aurelia! I know you blog about Aurelia a lot, but others here might not know that, so you might want to clarify that it is a serious competitor.
I read blog posts like the one here (and more positive ones as well) in order to stay informed about all major frameworks, but every code sample I see of Angular 2 (or Angular 1) makes me cringe in comparison to the elegance of Aurelia. I'm really looking forward to using Aurelia in production soon!
@YipYipX4,
I did just listen to Rob Eisenberg speak about the Aurelia on the JavaScript Jabber postcast. It does sound like is very intent on keep the framework very simple and easy to use and "out of your way" when you are building your apps. It does seem to have a compelling story. I should really check it out, for no other reason, just as yourself, to stay informed on how other people are solving problems.
Seriously, try React. It will be a relief. Way simpler, more easy to debug and understand. These kind of things made me move away from Angular. Not because I couldn't fix it, but because it felt fundamentally wrong.