In my react application, I have a Grid. User can select many grid rows at a time and click on a button to take bulk action on selected grid rows.
On a server side I have a script which I want to execute for each selected row (to make question simple I am making call to "jsonplaceholder.typicode.com" in the example below for each selected row) on a click of a bulk action button. On bulk action button click, I get selectedRows in action creator, where I iterate over selectedRows and make ajax call for each of the selected row.
Since selectedRows may contain more than 1000 items and if I just iteratively make the ajax call using the forEach loop, the browser page may eventually stop responding before each of the requests are resolved. Hence I used solution below, to send request in a batch of 5 and then wait until those 5 are resolved.
// Action creator, selectedRows is an array.
function onGridRowsSelection(selectedRows) {
makeBatchCalls(selectedRows,5)
}
async function makeBatchCalls(selectedRows, length) {
let test = arrayIds.reduce((rows, key, index) => (index % length == 0
? rows.push([key])
: rows[rows.length-1].push(key)) && rows, []);
let Batchresults = []; //convert them to two dimensionl arrays of given length [[1,2,3,4,5], [6,7,8,9,10]]
for (calls of test) {
Batchresults.push(await Promise.all(calls.map((call)=>{
fetch(`https://jsonplaceholder.typicode.com/posts/${call}`)
})
));
}
return Promise.all(Batchresults); //wait for all batch calls to finish
}
Solution above works fine, but has one problem,
- Select more than 5 rows from a grid and click on bulk action button,
- Again select more than 5 rows and click on bulk action button,
- Now I see 10 requests active at a time.
How can I restrict this?
Follow up question for the problem mentioned here is asked in React - controlling async calls smartly without any side effect in complex applications
This question is a follow up question of JavaScript, React - sending multiple simultaneous ajax calls
The async
module has a function for this: async.queue
. First you define a task function. Then you give it a task - in your case, an array of rows and the action you want it to take. The task will be run, or added to the queue if there is already a task in progress. When a task is completed, the next one will be taken from the queue.
Better yet, you could define the task function for just one row and set the concurrency of the queue to 5. When the user clicks the button, you add lots of tasks to the queue, one for each row that was selected. 5 tasks will start running immediately, and the rest will be queued. This is probably better than what you're trying to do, because this way the user can start 2 tasks and then immediately start another 3, and they will all run in parallel.
Try the following code:
const async = require('async'); // or whatever mechanism you're using for module management.
const queue = async.queue((row, callback) => {
fetch(`https://jsonplaceholder.typicode.com/posts/${call}`)
.then(callback, callback);
}, 5);
function onGridRowsSelection(selectedRows) {
for (let call of selectedRows) {
queue.push(call);
}
}
This is bound to happen because in your code, there is no check to see if already a batch request is running or not. You will have to make some changes in your code to accommodate batch calls correctly.
Step 1:
First of all, keep a flag in your state to see if already a batch request is running, say flagBatchRunning. Make it to true in your makeBatchCalls function before firing the requests.
Now once the Promise.all is resolved and all requests have completed, make it to false again.
In your action creator, check for this flag to be false.
function onGridRowsSelection(selectedRows) {
if(!state.flagBatchRunning){
makeBatchCalls(selectedRows,5)
}
}
Step 2:
Simply keeping a flag won't help you because it is quite possible that a user again clicks the bulk action button while your batch call is running and your onGridRowsSelection will ignore this update in this case. So, now you need to keep some kind of variable to store these pending batch requests.
To cater this, create an array say, pendingRequestsArray. Keep adding all your pending updates in this array and once previous batch is completed, pick all the requests from pending Array and make a batch call for them.
So your function now changes to this.
// Action creator, selectedRows is an array.
function onGridRowsSelection(selectedRows) {
if(!state.flagBatchRunning){
makeBatchCalls(selectedRows,5)
}else{
state.pendingRequestsArray.push(selectedRows); //push to pending array
}
}
async function makeBatchCalls(selectedRows, length) {
let test = arrayIds.reduce((rows, key, index) => (index % length == 0
? rows.push([key])
: rows[rows.length-1].push(key)) && rows, []);
let Batchresults = []; //convert them to two dimensionl arrays of given length [[1,2,3,4,5], [6,7,8,9,10]]
for (calls of test) {
Batchresults.push(await Promise.all(calls.map((call)=>{
fetch(`https://jsonplaceholder.typicode.com/posts/${call}`)
})
));
}
return Promise.all(Batchresults)
.then(function(results){
//call callback function here
promiseResolved();
}); //wait for all batch calls to finish
}
//assuming you have a callback function like this once all your batch calls finish
function promiseResolved(){
//set flagRunning to false
state.flagBatchRunning = false;
//if any pending requests are present, process them, else ignore
if(state.pendingRequestsArray.length > 0){
state.flagBatchRunning = true;
makeBatchCalls(pendingRequestsArray, pendingRequestsArray.length);
}
}
PS. This is just a pseudo code. Do not put logic in your action creator. It should be taken care of by reducer(to change state) and saga/thunk for async actions.
Hope this helps.