So it happens again, right?
You wanted to fix a bug and you get this famous ?$digest already in progress??
You made this nightmare when Darth Vador says to Luke: ?I am your?? $digest already in progress??
You just snatcher the last hair on your head, trying to resolve the mystery of ?$digest already in progress??
You had this feeling when your baby, vomiting his lunch, say ?$digest already in progress??
Even one day, you surprised yourself bringing a digestive drug to help Angular?
But today is today, it?s time to change that!
You want to start a new project and you want to avoid that right?
You are at the middle of a project and you want to get rid of the angular?s digestion problem?
So?. It?s simple!
Keep this in mind: Angular is smart enough to detect all changes, if not call $scope.$apply()? THAT?S ALL
But why sometime we need $digest? $apply? $timeout? $evalAsync? Trust me you don?t and I will show you why with this visual guide!
When Angular is telling you the $digest is already in progress, it?s simply telling you something is wrong so please listen to it? fix the cause of that, not the digestion problem?
Fix the problem not the symptoms?
The documentation
I don?t want to tell my life, so I will be very brief. I you want to avoid the $digest soup or what I like to call ?The Dramatic Cascade? you have to follow the Angular?s documentation:
- $scope.$digest: Execute only the $digest phase, executing all watchers.
?Usually, you don?t call $digest() directly in controllers or in directives. Instead, you should call $apply() (typically from within a directive), which will force a $digest().?
- $scope.$apply: Evaluate the function given in parameter in the next cycle and call $rootScope.$digest()
?$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries.?
So please never use $digest().
?The Dramatic Cascade?
100% of the time, this cascade happen because for example you are using jquery, or you are using bootstrap and angular and make the link between them manually instead of using angular-bootstrap, I am right??? mmmhh???? you can tell me?
But only one, only ONE $apply misused and you start the dramatic cascade of $digest already in progress?
The cycle of life
I don?t know if there is a real name for that, I found nothing about that in the angular source code, but Angular execute a succession of $apply phase and $digest phase, I will call $apply + $digest = a cycle, to better understand what is happening?
Angular?s cycles
Don?t break those cycles and you will be fine?
Different use cases, I have seen, and where someone was trying to force a digest or an apply?
1. A complete use case
An use case to understand where is executed when in a cycle
// HTML<button ng-click=”toto.click()”>ClickMe</button>// Javascriptthis.click = function() { console.debug(‘ng-click code executed’); $timeout(function() { console.debug(‘$timeout code executed’); }); $scope.$evalAsync(function() { console.debug(‘$scope.$evalAsync code executed’); }); // ERROR $scope.$apply(function() { console.debug(‘$scope.$apply code executed’); });};
- ng-click code start a new cycle, cycle 1
- We are already in an $apply phase, so if you call $apply() you will get an error and calling $digest() is useless because it?s the next phase (and you get the same error)
- $evalAsync is useless
- $timeout code will force Angular to create a new cycle, cycle 2 (performance impact)
2. Third-party/DOM Events use case
This one is THE MOST IMPORTANT one, all problems come from the absence of $apply call when you are in the callback of a thirdparty or in the callback of a jquery/DOM event. Very often forgot, this is the main cause of a dramatic ?? already in progress? cascade
The Good version
// HTML <button id=”button1″>Click outside Angular digest</button>// Javascriptdocument.querySelector(‘#button1’).onclick = function() { console.debug(‘event click code executed’); $scope.$apply(function() { console.debug(‘$scope.$apply code executed’); });};
- DOM event click calls our code (it could be any other DOM events or a thirdparty event/callback)
- As you can see $scope.$apply execute a new cycle with our code inside, it?s perfect, our code is correctly insecuted in Angular context
- We have both phase: $apply & $digest
Things get worse?
So here, is an example of something you should never do, using $digest() instead of $apply()
// HTML <button id=”button2″>Click outside Angular digest</button>// Javascript// NEVER DO THATdocument.querySelector(‘#button2’).onclick = function() { console.debug(‘event click code executed’); $scope.myValue = 2; //BAD $scope.$digest();};
The $apply() phase is not executed! It?s why Angular strongly recommand to never use $digest().
3. In a promise
Using $q, the Angular?s promise library, you are sure to be in the Angular context, so no need to $apply or $digest
// Javascriptthis.ngClick = function() { console.debug(‘call promise’) asyncGreet().then(function(greeting) { console.debug(‘promise result received’); })};
Angular create a new $digest only cycle, when the promise is resolved, so if you call $scope.$digest() or $apply() inside the promise resolve callback, you will get ?$digest already in progress?
3. In a watcher
Watchers are already in the Angular context, so?
Third party example
An example if you are in a thirdparty, or a DOM event.
// Javascript$scope.$watch(‘myValue’, function(newValue) { console.debug(‘$watcher myValue’, newValue);})document.querySelector(‘#button2’).onclick = function() { console.debug(‘event click code executed’); $scope.myValue = 2; $scope.$apply();};
- You call $apply in the onclick function because you are out of the Angular context.
- In the watcher, never call $apply() or $digest() because you are 100% of the time in a cycle
In an angular context
I replace the use case above with an ng-click, so I?m now in the Angular context thank to ngClick directive.
// HTML<button ng-click=”toto.ngClick()”>Click Me</button>// Javascript $scope.$watch(‘myValue’, function(newValue) { console.debug(‘$watcher myValue’, newValue);})this.ngClick = function() { console.debug(‘code ng click’) $scope.myValue = 3;};
Conclusion
I hope this guide will help you to understand how you can completely avoid any ?$digest/$apply already in progress?.
- $digest() is bad, really, never use it
- $apply(your code) is to execute code that are not executed in the Angular context, like third party library, events dom?
- $timeout, $evalAsync, ? try to never use them and trust me, you can
- You can avoid 90% of $scope.$digest()/$scope$apply(), the 10 others are only for point #2
- Only one misused $apply() or $digest() and you start a dramatic cascade of ?? already in progress? errors.
- Never never never NEVER try to apply this bad thing that you can see everywhere on internet where you try to deal with the Angular?s private property $$phase to execute a digest/apply if it?s not the current phase of angular? EEERRRKKKK