Using track by in ng-repeat function triggers an endless $digest-loop issue

It appears that I am still struggling to grasp the mechanism behind ng-repeat, $$hashKeys, and track by.

Currently, in my project, I am working with AngularJS 1.6.

The Issue:

I have an array of complex objects that I want to display as a list in my view. However, before rendering them, I need to modify these objects by mapping or enhancing them:

const sourceArray = [{id: 1, name: 'Dave'}, {id:2, name: Steve}]

const persons = sourceArray.map((e) => ({enhancedName: e.name + e.id})) 

//The content of persons becomes:
//[{enhancedName: 'Dave_1'}, {enhancedName: 'Steve_2'}]

Binding this to the view should work like this:

<div ng-repeat="person in ctrl.getPersons()">
    {{person.enhancedName}}
</div>

However, this leads to a $digest()-loop because .map creates new object instances each time it is called. As I bind this to ng-repeat via a function, it gets reevaluated in every $digest cycle, preventing the model from stabilizing and causing Angular to run additional cycles due to the objects being marked as $dirty.

Confusion:

This issue is not new, and there are various solutions available:

In an Angular issue from 2012, Igor Minar suggested manually setting the $$hashKey property to indicate that the generated objects are the same. Despite his successful fiddle example, even simple implementations led to a $digest loop in my project. I attempted upgrading the Angular version in the fiddle which resulted in a crash.

Since Angular 1.3, we have track by for addressing this specific problem. However, both:

<div ng-repeat="person in ctrl.getPersons() track by $index">   

and

<div ng-repeat="person in ctrl.getPersons() track by person.enhancedName">   

resulted in a $digest loop. I believed that the track by statement would inform Angular that it works with the same objects, but it seems that it continuously checks for changes. At this point, I am unsure how to effectively debug this issue.

Question:

Is it feasible to use a filtered/modified array as a data source for ng-repeat?

I prefer not to store the modified array on my controller as I require constant updates to its data. Having to manually maintain and refresh it in the controller rather than relying on databinding is something I wish to avoid.

Answer №1

After testing the "it crashes" fiddle you shared, it did not lead to an infinite digest issue in my case. However, I noticed that the Angular app could not bootstrap successfully using the method shown in the fiddle (seems like the latest Angular version has different requirements for bootstrapping).

To demonstrate the crash as you mentioned, I took the liberty to rewrite it with a different Angular bootstrapping approach that I understood better.

I was able to find a solution by creating a successful track by stringified JSON.

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.6/angular.min.js"></script>

<script>
angular.module('myApp',[])
.controller('Ctrl', ['$scope', function($scope) {
    angular.extend($scope, {
    stringify: function(x) { return JSON.stringify(x) },
    getList: function() {
      return [
        {name:'John', age:25},
        {name:'Mary', age:28}
      ];
    }
  });
}]);
</script>

<div ng-app="myApp">

<div ng-controller="Ctrl">
  I have {{getList().length}} friends. They are:
  <ul>
    <li ng-repeat="friend in getList() track by stringify(friend)">
      [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
    </li>
  </ul>
</div>

</div>

In essence, we introduced a custom tracking function, stringify(), which resolved the issue. There might be a built-in Angular function for this purpose as well.

Interestingly, using track by $index also worked contrary to your initial observation. It seems like JsFiddle might have impacted our experimentation results somewhat*

*In my personal experience, I encountered some challenges with JsFiddle itself. For instance, my implementation of track by stringify() only started working properly after I duplicated the Fiddle and ran the code in a new browser context. It felt like there was residual state from previous executions causing issues. Therefore, I recommend retesting anything that failed in JsFiddle within a fresh instance.

Regarding why your usage of $$hashKey resulted in an infinite digest loop — Angular probably does not expect

$$hashKey</code> to be a function. Instead of executing the function, Angular likely performed a <em>reference comparison</em> on the assigned function for <code>$$hashKey
.

Since you assign a new comparator instance to $$hashKey each time getList() is called, the references would never match during subsequent digests, leading to continuous digest attempts.

EDIT: Updated StackOverflow embed and JsFiddle links to use HTTPS CDN to prevent mixed content security errors.

Answer №2

When the function getPersons() returns a fresh array, even if it contains the same elements, the $digest process will continue due to the use of the === comparison; regardless of the track by expression which impacts the display of nodes after the detection of changes in the ngRepeat.

(function() {
  angular
    .module('app', [])
    .controller('AppController', AppController)

  function AppController($interval) {
    // Consider more efficient methods 
    const hashFn = angular.toJson.bind(angular)
    // Logic for mapping data presentation
    const mapFn = (e) => ({
      enhancedName: e.name + e.id
    })

    // Initial data setup
    let sourceArray = [{
      id: 1,
      name: 'Dave'
    }, {
      id: 2,
      name: 'Steve'
    }]

    // Initialize "cache"
    let personList = sourceArray.map(mapFn),
        lastListHash = hashFn(sourceArray)

    Object.defineProperty(this, 'personList', {
      get: function() {
        const hash = hashFn(sourceArray)
        if (hash !== lastListHash) {
          personList = sourceArray.map(mapFn)
          lastListHash = hash
        }

        // Return **the same** array
        // when source remains unchanged
        // for consistency in `$digest` cycle
        return personList
      }
    })

    // Change test
    $interval(() => sourceArray.push({
      id: Date.now(),
      name: 'a'
    }), 1000)
  }
})()
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.6/angular.min.js"></script>
<div ng-app="app">

  <div ng-controller="AppController as ctrl">
    There are {{ctrl.personList.length}} persons.
    <ul>
      <li ng-repeat="person in ctrl.personList track by $index">
        [{{$index + 1}}] {{ person.enhancedName }}
      </li>
    </ul>
  </div>

</div>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the method to utilize the process.env object from within an imported npm package in the importing class?

In my React app, I used dotenv to set process.env variables for each component. When all components were in the same source code repo and imported via '../component/component_name', accessing these variables was easy using process.env.variable_na ...

Error encountered: Unable to reference Vue as it has not been defined while importing an external JavaScript file

Currently, I am implementing a package called https://github.com/hartwork/vue-tristate-checkbox within my Vue.js component by adding it through the command: yarn add hartwork/vue-tristate-checkbox. In my Vue.js component, the package is imported in the fo ...

"Learn how to handle exceptions in Nest JS when checking for existing users in MongoDB and creating a new user if the user does

Below is the implementation of the addUser method in users.service.ts - // Function to add a single user async addUser(createUserDTO: CreateUserDTO): Promise<User> { const newUser = await this.userModel(createUserDTO); return newUser.save() ...

Make sure to validate onsubmit and submit the form using ajax - it's crucial

Seeking assistance for validating a form and sending it with AJAX. Validation without the use of ''onsubmit="return validateForm(this);"'' is not functioning properly. However, when the form is correct, it still sends the form (page r ...

Detecting when an object exits the proximity of another object in ThreeJS

In my ThreeJS project, I have planes (Object3D) flying inside a sphere (Mesh). My goal is to detect when a plane collides with the border of the sphere so that I can remove it and respawn it in a different location within the sphere. I am wondering how I ...

Angular2: the setTimeout function is executed just a single time

Currently, I am working on implementing a feature in Angular2 that relies on the use of setTimeout. This is a snippet of my code: public ngAfterViewInit(): void { this.authenticate_loop(); } private authenticate_loop() { setTimeout (() =& ...

Issues with jQuery focus function in Internet Explorer 11

Is there a way to set the focus on an anchor tag using the URL? Anchor Tag: <a id='714895'>&nbsp;</a> URL: jQuery Code: try { if(document.URL.indexOf("?ShowAllComments=True#") >= 0){ var elementClicked = "#" +location.hre ...

Ensuring secure communication with PHP web service functions using Ajax jQuery, implementing authentication measures

jQuery.ajax({ type: "POST", url: 'your_custom_address.php', dataType: 'json', data: {functionname: 'subtract', arguments: [3, 2]}, success: function (obj, textstatus) { if( !('error' in obj) ) { ...

Issues with CreateJS chained animations failing to reach their intended target positions

Currently, I am tackling a project that involves using Three.js and CreateJS. However, I have encountered an issue with the animations when trying to move the same object multiple times. The initial animation fails to reach the target position, causing sub ...

What steps can be taken to avoid the appearance of the JavaScript prompt "Leaving site"?

Hi there, I'm currently trying to find a way to remove the Javascript prompt/confirm message that asks "Do you want to leave this site?" like shown in this link: The issue I am facing is that when a modal opens and the user clicks on "YES", it redire ...

Perform a Fetch API request for every element in a Jinja2 loop

I've hit a roadblock with my personal project involving making Fetch API calls to retrieve the audio source for a list of HTML audio tags. When I trigger the fetch call by clicking on a track, it always calls /play_track/1/ and adds the audio player ...

Access the data attribute of a button element in AngularJS

Utilizing Angularjs for managing pagination loop button id=remove_imslide, I am attempting to retrieve the value of data-img_id from the button to display in an alert message using a Jquery function on button click, but I am encountering issues. This is h ...

GAS: What strategies can I implement to optimize the speed of this script?

I have a sheet with multiple rows connected by "";" and I want to expand the strings while preserving the table IDs. ID Column X: Joined Rows 01 a;bcdfh;345;xyw... 02 aqwx;tyuio;345;xyw... 03 wxcv;gth;2364;x89... function expand_j ...

What is the secret behind Node.js's ability to efficiently manage multiple requests using just one thread

After conducting some research on the topic, I noticed that most people tend to focus solely on Non-blocking IO. For instance, if we consider a basic application that simply responds with "Hello World" text to the client, there will still be some executio ...

Tips for dynamically updating the path in angular as you scroll

https://i.stack.imgur.com/KlmnQ.jpg Currently utilizing Angular 17, my goal is to create a page with multiple components accessible through unique paths. Clicking on the navigation menu should automatically scroll to the desired component and update the p ...

Transform socket.on into a promise

Currently, I am developing a service that involves the function init acting as resolve. Initially, it generates a folder using the connection's id and then proceeds to write some default files inside this newly created folder. The main goal here is to ...

Swap out the hyperlink text for a dropdown menu when clicked and then revert back

Is there a way to dynamically switch between a label/text and a Kendo Combobox in a div using JavaScript when clicking on the text? The desired functionality includes: Clicking on the text displays the combobox, clicking away from it hides the combobox a ...

What is the best way to swap the values of options between two input select elements?

I am trying to create a feature where I have two select dropdowns with the same options, and when a trigger is clicked, the option values are inverted between the two selects. Here is an example: <select id="source_currency"> <option value="BRL" ...

When Vue is clicked, it appears to trigger multiple methods simultaneously

Currently, I am learning Vue and encountered a problem that I need help with. When using the v-on:click directive to call a method, all other instance methods are also called when the same method is used elsewhere. HTML: <div id="exercise"> &l ...

How can I make $.when trigger when a JSON request fails?

I am currently using the code below to retrieve JSON data from multiple URLs. However, I have noticed that if one of the URLs fails or returns a 404 response, the function does not execute as expected. According to the jQuery documentation, the "then" fu ...