9 Nov 2010

jQuery: Using .ajax() inside a loop and variable scope

Source code

On my latest web app I ran into a problem that had me stumped for a while. I was trying to load data via AJAX from a number of files to insert into the web page. I did this by iterating through a number of jQuery .ajax() calls with a for loop. I was getting very strange behaviour such as seemingly missing AJAX calls and a muddle for-loop! It took a lunch break to clear my mind so I could regather my wits and troubleshoot the issue. Which turned out to be quite a sly JavaScript variable scope problem.

Here’s an example of my problem code (edited for simplicity). You can see the ‘i‘ variable is used to point each requested file (i.e. ‘panel_data_0.xml‘) and to then place the response data in the HTML element (i.e. ‘#panel-0‘). This way each XML file’s data is inserted into its numbered HTML element, five all up, numbered 0 to 4:

function loadPanelData()
{
   // Iterate through our 5 panels...
   for (i = 0; i <= 4; i ++)
   {
      $.ajax(
      {
         type: 'GET',
         url: 'panel_data_' + i + '.xml',
         success: function(xml)
         {
            $(xml).find('data').each(function()
            {
               // Read data from XML...
               var heading = $(this).find('heading').text();
               var paragraph = $(this).find('paragraph').text();

               // Insert data into panel...
               $('#panel-' + i).append('<h1>' + heading + '</h1>');
               $('#panel-' + i).append('<p>' + paragraph + '</p>');
            });
         },
         error: function(xml)
         {
            // Handle errors here.
         }
      });
   }
}

The problem came about because the AJAX call is asynchronous meaning the for-loop is allowed to iterate from 0 to 4 without waiting for the AJAX request to receive its response from the server. That’s all well and good, until I realised that I’m relying on the ‘i’ variable in the .ajax() success callback function, and by the time that is reached, i has already zipped through the loop up to 4.  Things get messy from there!  :-S

The cheap and nasty fix

The most obvious answer is to simply add the ‘async‘ option and set it to false – like so:

$.ajax(
{
   type: 'GET',
   url: 'panel_data_' + i + '.xml',
   async: false,
   success: function(xml)
   {
      // ... etc., etc...

This makes our AJAX synchronous, so that the for-loop must wait for the jQuery .ajax() function to complete before being allowed to continue on. But then our page will load slowly in stages and the browser will be interrupted. Basically, it’s an ugly fix.

The elegant fix

The better way to go about solving this problem is to give each .ajax() function its own copy of variable ‘i‘ so that no matter what the for-loop does, we always know what ‘i’ used to be and should be for each AJAX call. This allows us to keep the AJAX calls in the for-loop, and have all the AJAX calls happen asynchronously, at the same time!

Here’s how it looks:

function loadPanelData()
{
   // Iterate through our 5 panels...
   for (i = 0; i <= 4; i ++)
   {
      $.ajax(
      {
         type: 'GET',
         url: 'panel_data_' + i + '.xml',
         ajaxI: i, // Capture the current value of 'i'.
         success: function(xml)
         {
            i = this.ajaxI; // Reinstate the correct value for 'i'.

            $(xml).find('data').each(function()
            {
               // Read data from XML...
               var heading = $(this).find('heading').text();
               var paragraph = $(this).find('paragraph').text();

               // Insert data into panel...
               $('#panel-' + i).append('<h1>' + heading + '</h1>');
               $('#panel-' + i).append('<p>' + paragraph + '</p>');
            });
         },
         error: function(xml)
         {
            // Handle errors here.
         }
      });
   }
}

You can see I added ‘ajaxI’ in the .ajax() function options and set it to ‘i’.  This means that as soon as we create the .ajax() function we preserve the value of ‘i’.  Then when we finally get a response back and before we insert the data into the HTML, I reinstate the value of ‘i’ so everything matches up nicely. :-)

About the Author:

Hardware and software engineer with experience in product development and building automation. Product Manager at NEX Data Management Systems, based out of Brisbane, Australia.

4 comments

  1. Ryan Wheale

    Or you could use the built in “context” property of the ajax object.

    • Great point Ryan, I hadn’t looked at context in this way. From what I understand, context is a way of limiting your jQuery selector to a certain DOM node.

      I haven’t used context before, but I’m guessing you mean when the .ajax() call is made I should add an option like this?…

      $.ajax(
      {
      type: 'GET',
      url: 'panel_data_' + i + '.xml',
      context: $('#panel-' + i),
      success: function(xml)
      {
      // ... etc, etc,...

      And then in the success callback, do something like this:

      // Insert data into panel...
      $(this).append('< h1>' + heading + '');
      $(this).append('< p>' + paragraph + '');

      I’ll play around with this later today next week. It’s looks like my post may need to be updated! Thanks for the feedback.

  2. ata

    Thank you !!!

  3. Eric Knight

    You….are….my….hero!!!!!!!

    Been working on this all day. Such a simple, easy fix, and as you said, ELEGANT solution.

Leave a Reply

*