Aug 20, 2015

Leave $rootScope alone, use AngularJs Dependency Injection.

AngularJs brings dependency injection into UI development. It works very well until it is misused. Recently, I was working on an AngularJs project. One interesting thing is that some application settings is attached to $rootScope for convenient consumption. When I saw it, I felt this was so Déjà vu. The $rootScope has essentially become the new "window" object in the browser, where all global variables can be attached to. So I Googled about this and wanted to know whether other people have similar problem, and I found this question in StackOverflow, Global variables in AngularJS. It seems that this is a quite a common problem out there. So I wanted dig into it more.

In the article Global Variables Are Bad, a few bad reasons were listed for using global variables.

  1. "What's a 'local variable'?"
  2. "What's a 'data member'?"
  3. "I'm a slow typist. Globals save me keystrokes."
  4. "I don't want to pass it around all the time."
  5. "I'm not sure in what class this data belongs, so I'll make it global."

I think the the reason we want to attach variables to $rootScope is because of #3 and #4 of the reason above. I don't need to argue why Global variables are bad, because the article has very good coverage. But in case of AngularJs, by changing the dependency on "setting" to "$rootScope.setting", the dependencies of "setting" become implicit. This is bad for communication, documentation, and maintenance. And it will encourage dumping more dependencies into $rootScope. The advantage of AngularJs dependency injection is completely gone. Explicit dependencies become exploring dependencies in the dark world of $rootScope.

One more possible reason in using $rootScope is that it makes the databinding easier, because all child scopes will inherit $rootScope's properties. But this only works when the child scope is not isolated scope. This practice will discourage the use of isolated scope, which is a great way to improve reusability of your directive.

Using inheritance from parent scope and $rootScope is very fragile and heavy coupling. A better way is to use dependency injection. Let's see an example, which is over-simplified. It may violate the best practice of AngularJs you have learned from somewhere else. But the point is to demonstrate the difference between using global variable and dependencies injection.

So the demo app wants to show a user a login screen if the user is not logged in, otherwise a welcome screen. Simple enough. The following is the implementation using global variable. You can see the code at http://output.jsbin.com/monozu. This implementation creates a global variable attached to $rootScope, which is visible to child scopes.

  <div ng-controller="login">
    <div  ng-if='!user.isAuthenticated'>
      You are not logged in yet.
      <button ng-click="user.login()">
        Log in
      </button>
    </div>
  </div>
  
  <div ng-controller="welcome" >
   <div ng-if='user.isAuthenticated'>
      Hello, {{user.firstName + ',' +  user.lastName}}
    </div>
  </div>
  var app = angular.module('app', []);
  
  app.run(function ($rootScope) {
    $rootScope.user = {
      isAuthenitcated: false,
      login: function () {
        this.firstName = 'John';
        this.lastName = 'Doe';
        this.isAuthenticated = true;
      }
    };
  });

app.controller('login', function ($scope) {

  });
  
  
  app.controller('welcome', function ($scope) {

  });

The following is the implementation using dependency injection. You can see the code at http://output.jsbin.com/senuti. The html template is still the same, but in JavaScript, the user global variable is removed from $rootScope, and become a service, which is injected into to each controller.

  app.factory('user', function () {
    return {
      isAuthenticated: false,
      login: function () {
        this.firstName = 'John';
        this.lastName = 'Doe';
        this.isAuthenticated = true;
      }
    };
  });
  

  app.controller('login', function ($scope, user) {
    $scope.user = user;
  });
  
  
  app.controller('welcome', function ($scope, user) {
    $scope.user = user;
  });

The implementation using dependency injection may use a little more code, but is more testable, maintainable and scalable. Please comment and let know what you think.