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.
renderChart()
within thesuccess
handler withingetData()
wheni
matchesnodeList.length - 1
... though there is no guarantee that all of the requests will complete in sequential order unlessasync: false
is added to the requests \$\endgroup\$SetTimeout
lower than50
, it won't render the data it was pushed bygetData
function. I haven't thought about changing theasync
flag, I haven't define it. \$\endgroup\$$.ajax()
the jquery methodjQuery.ajax()
or does it come from another library? \$\endgroup\$$.ajax()
isjQuery.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 utilizedjQuery.ajax()
async. \$\endgroup\$