3
\$\begingroup\$

I wrote some code that will get the name and data of a node or multiple of nodes and put them in an object (seriesData) so I can use said object for a chart (renderChart). I used setTimeout() so seriesData can populate before rendering the chart.

Everything works as intended. Is there a better pattern to handle this? I read up on Promises but am not sure how to implement in a looped AJAX within getData() or is even needed.

const seriesData = {
    name: [],
    data: [],
};

function getNodeList() {
    let nodeList = [];
    $.ajax({
        type: 'GET',
        url: '/masternode',
        success: function (result) {
            let nodes = $(result).find('ID');
            let addresses = $(result).find('Address');
            for (let i = 0; i < nodes.length; i++) {
                let node = $(nodes[i]).text();
                let address = $(addresses[i]).text();
                nodeList.push({ node: node, address: address });
            }
            getData(nodeList, seriesData);
            setTimeout(renderChart, 50);
        },
    });
}

function getData(nodeList, seriesData) {
    for (let i = 0; i < nodeList.length; i++) {
        let ipAddress = nodeList[i].address;
        $.ajax({
            type: 'GET',
            url: '/' + ipAddress,
            success: function (result) {
                let sData = $(result).find('Data').text();
                seriesData.name.push(ipAddress);
                seriesData.data.push(sData);
            },
        });
    }
}

// Omitted chart options
// ...


function renderChart() {
    chart.render();
}
\$\endgroup\$
4
  • \$\begingroup\$ Welcome to Code Review! Are you sure it works as intended? are the requests going to a nearby/local server? One could call renderChart() within the success handler within getData() when i matches nodeList.length - 1... though there is no guarantee that all of the requests will complete in sequential order unless async: false is added to the requests \$\endgroup\$ Sep 14, 2022 at 23:39
  • \$\begingroup\$ The request is going to a nearby server. If I change the SetTimeout lower than 50, it won't render the data it was pushed by getData function. I haven't thought about changing the async flag, I haven't define it. \$\endgroup\$
    – Bewarden
    Sep 15, 2022 at 2:32
  • \$\begingroup\$ Because I would prefer to not assume, is $.ajax() the jquery method jQuery.ajax() or does it come from another library? \$\endgroup\$ Sep 17, 2022 at 12:59
  • \$\begingroup\$ $.ajax() is jQuery.ajax(). I do appreciate you commenting on this. This helped me look at the code from another perspective. I have changed the code quite a bit and utilized jQuery.ajax() async. \$\endgroup\$
    – Bewarden
    Sep 18, 2022 at 22:07

1 Answer 1

2
\$\begingroup\$

Initial Review thoughts

Before addressing the main question I’d like to address a few points first.

Variable declaration style

It is nice that seriesData is declared with const, since it never gets reassigned. Some variables within the functions like nodeList, nodes, addresses and ipAddress also are never reassigned so they could also be declared with const. This helps avoid accidental re-assignment and other bugs.

ES-6 Object declaration

The object pushed into nodeList in the loop uses plain object syntax:

nodeList.push({ node: node, address: address });

The shorthand property definition notation can be used to eliminate the keys, since they are the same name as the variables used for the values:

 nodeList.push({ node, address });

Single use variables

The variable ipAddress is only used once. Instead of

url: '/' + ipAddress,

unless that variable was used for debugging, it can be eliminated:

url: '/' + nodeList[i].address,

The same is true for node and addressed, but if those were eliminated then the ES-6 shorthand syntax could not be utilized.

Main Question

The main question posed here is “Is there a better pattern to handle this?

I would first ask if the API supports returning data for multiple nodes in a single endpoint. If this was possible then there would be no need to make multiple requests, improving network efficiency and eliminating the risk of network connection issues, service interruptions for others, etc.

One could ask a question like “How to know when all the ajax calls in a loop are completed?

And there are similar questions asked on stack overflow - for example: How to know when all the ajax calls in JQuery each loop are completed?. As the accepted answer suggests, the jQuery utility function jQuery.when() can be used to execute a callback function when promises are completed.

Note that $.ajax() returns a jqXHR object and

The jqXHR objects returned by $.ajax() as of jQuery 1.5 implement the Promise interface, giving them all the properties, methods, and behavior of a Promise (see Deferred object for more information).

By putting the return values from calling $.ajax() into an array, that array can be passed in a call to $.when() so renderChart() can then be called after all of the requests have completed.

See the example below for a demonstration. It uses the convenience method $.get() instead of $.ajax() to skip passing type: 'GET',. As the log may show- the requests often finish in an order that is not sequential.

const seriesData = {
  name: [],
  data: [],
};
let count = 0;
const progressBar = $('progress');

function getNodeList() {
  $('#log').append($('<div>').html(`fetching list of ships from API`));
  $.ajax({
    type: 'GET',
    url: 'https://swapi.dev/api/vehicles/',
    success: function(ships) {
      if ((typeof ships !== 'object') || !ships.hasOwnProperty('results')) {
        console.error('invalid response', ships)
        return;
      }
      progressBar.val(++count);
      $('#log').append($('<div>').html(`got list of ships from API`));
      getData(ships.results.slice(0, 5).map(ship => ship.url), seriesData);
    },
  });
}

function getData(nodeList, seriesData) {
  const promises = nodeList.map((url, i) =>
    $.get({
      url,
      success: function(ship) {
        seriesData.name.push(ship.name);
        seriesData.data.push(ship.length);
        progressBar.val(++count);
        $('#log').append($('<div>').html(`${i+1} added ${ship.name} with length: ${ship.length}m`));
      },
    })
  );
  $.when(...promises).then(function(result) {
    renderChart();
  });
}

// Omitted chart options
// ...

getNodeList()

function renderChart() {
  Chart.defaults.global.legend.display = false;
  const config = {
    type: 'bar',
    data: {
      labels: seriesData.name,
      datasets: [{
        data: seriesData.data,
        label: 'Ship Length',
        backgroundColor: 'red',
        borderColor: 'green',
        fill: false
      }]
    },
    options: {
      title: {
        display: true,
        text: 'Ship Lengths'
      },
      scales: {
        xAxes: [{
          display: true,
          scaleLabel: {
            display: true,
            labelString: 'Ship'
          }
        }],
        yAxes: [{
          display: true,
          scaleLabel: {
            display: true,
            labelString: 'Length (meters)'
          }
        }]
      }
    }
  };
  const ctx = document.getElementById('canvas').getContext('2d');
  new Chart(ctx, config);
}
#loading_container {
  display: flex;
  flex-direction: row;
}
#loading_container  > progress {
  flex: auto;
}
#loading_container > span {
  width: 9rem;
}
#log {
  font-size: x-small;
}
<script src="//www.chartjs.org/dist/2.7.2/Chart.bundle.js"></script>
<script src="//www.chartjs.org/samples/latest/utils.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="loading_container">
  <span>Loading Progress:</span>
  <progress max="6" value="0"></progress>
</div>
<span id="log"></span>
<div>
  <canvas id="canvas"></canvas>
</div>

If one wasn't using jQuery, the generic method Promise.all() could be used to execute a callback when promises are completed.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.