AngularJS: $scope.array.push() does not update the view, even with $apply

Mimu picture Mimu · Jun 29, 2014 · Viewed 57.8k times · Source

I'm trying to learn AngularJS and there is this thing that I don't understand, which seems like all the internet solved by using $scope.$apply, but I already use it and it does nothing.

Basically, I use Twitter API to retrieve a timeline, and when we scroll from the bottom, it loads more tweets. This part works, I'm using a factory to do it, but I can display the object receive in the console, I don't have issues here.

I have a view like this, to display the data:

<div class='timeline' ng-controller='TimelineCtrl' is-scrolled='loadData()'>
    <div class='tweet' ng-repeat='p in posts'>
        <img class='portrait' src='{{p.user.profile_image_url}}' />
        <p>{{p.text}}</p>
        <p class='date'>{{p.created_at}}</p>
    </div>
</div>

My controller looks like this:

    $scope.posts = [];

    // Load the original tweets list
    TwitterAPI.timeline($scope.count, function(data) {
        $scope.$apply(function() {
            $scope.maxId = data[data.length-1].id;
            $scope.sinceId = data[0].id;
            $scope.posts.push(data);
        });
    });

data is legit.

The thing I don't understand at all, and make me think that it's something very easy to solve and I just don't see it, is that if I use '= data' instead of 'push(data)' the view is updated. Even when I load more tweets, if I use '=' the view is updated (with the content being replaced of course which is not what I want).

Note: maxId, sinceId and count are initialized earlier, I didn't put it there since I don't think it matters.

Answer

Chris Bouchard picture Chris Bouchard · Jan 23, 2015

The trouble seems to be that Angular's NgRepeat stops if it iterates over the same object more than once. I've created a jsFiddle to demonstrate.

In the first section, you can add strings to an array. The first button always add the same string object, while the second creates a fresh string object each time. Notice that as soon as you click the first button twice, it doesn't matter what you add to the list.

In the second section, we always add a fresh object, even though those objects all contain a reference to the same string object. This works as you would expect.

So, to make this an explicit answer, make sure the things you add to your list are distinct objects, and use object literals to enforce this if needed. I would prefer Array#push over Array#concat because the latter creates a new array object each time, and if you have a lot of items, that will be a lot of churn and a lot of garbage collection.

The HTML:

<div ng-controller="Controller1">
    <button ng-click="addLine()">Add Line</button>
    <button ng-click="addNumber()">Add Number</button>
    <button ng-click="reset()">Reset</button>
    <div>{{lines}}</div>
    <div ng-repeat="line in lines">
        {{line}}
    </div>
</div>

<hr />

<div ng-controller="Controller2">
    <button ng-click="addObject()">Add Object</button>
    <button ng-click="reset()">Reset</button>
    <div>{{objects}}</div>
    <div ng-repeat="obj in objects">
        {{obj.data}}
    </div>
</div>

The JavaScript:

(function () {
    var myApp = angular.module('myApp', []);

    myApp.controller('Controller1', function ($scope) {
        $scope.lines = [];

        $scope.addLine = function () {
            $scope.lines.push('Hello world!');
        };

        $scope.addNumber = function () {
            $scope.lines.push('Line ' + $scope.lines.length);
        };

        $scope.reset = function () {
            $scope.lines = [];
        };
    });

    myApp.controller('Controller2', function ($scope) {
        $scope.objects = [];

        $scope.addObject = function () {
            var obj = { data: 'Hello world!' };
            $scope.objects.push(obj);
        };

        $scope.reset = function () {
            $scope.objects = [];
        };
    });
})();