1

I am working with react functional component.

I have download(async) functionality in my application. There is list of download buttons in my app. I want to download them one by one in queue.

const [downloadQueue, setDownloadQueue] = useState([]);

onClickDonwload = () => { //on every download click this function calls. So user can click on download continuously but I want to make sure them to be downloaded one by one.
  downloadQueue([...downloadQueue, { id, data }])
}

useEffect(() => {
  downloadQueue.map(async(data) => {
    await myDownloadAsyncFunction()
  })
}, [downloadQueue])  // here the issue is whenever the downloadQueue updates it just start downloading without waiting for the previous one

Kindly help

  • Does this answer your question? [How do I cancel an HTTP fetch() request?](https://stackoverflow.com/questions/31061838/how-do-i-cancel-an-http-fetch-request) – evolutionxbox Sep 25 '22 at 07:36
  • @AshishChoudary What does `myDownloadAsyncFunction` do - can you edit the question to include more detail on it? For example, include a simplified version of its code? – Michal Charemza Sep 25 '22 at 15:04

2 Answers2

1

How about this? It uses Promise-chaining to make a simple queue

import React, { useState } from "react";

// Any async function or function that returns a promise
async function myDownloadAsyncFunction(data) {
  return fetch('https://mydomain.test/path');
}

function DownloadButton() {
  const [queue, setQueue] = useState(Promise.resolve());

  onClickDownload = () => {
    setQueue(queue
      .then(() => myDownloadAsyncFunction('My data'))
      .catch((err) => {console.error(err)})
    )
  }

  return (
    <button onClick={onClickDownload}>Download</button>
  );
}

The above doesn't trigger the download from useEffect. If you do want to use useEffect, I think the state would probably need to be a counter to cause useEffect to run when it changes:

import React, { useState, useEffect, useRef } from "react";

// Any async function or function that returns a promise
async function myDownloadAsyncFunction(data) {
  return fetch('https://mydomain.test/path');
}

function DownloadButton() {
  const queue = useRef(Promise.resolve());
  const [clickCount, setClickCount] = useState(0);

  useEffect(() => {
    if (clickCount == 0) return;
    queue.current = queue.current
      .then(() => myDownloadAsyncFunction('My data'))
      .catch((err) => {console.error(err)});
  }, [clickCount]);

  function onClickDownload() {
    setClickCount(clickCount + 1);
  }

  return (
    <button onClick={onClickDownload}>Download</button>
  );
}

Note in both of the above examples might need to get more complicated to better deal with some of the downloads failing. In these examples though, if a download fails, the next one should continue to attempt to be downloaded.

Michal Charemza
  • 25,940
  • 14
  • 98
  • 165
  • If I want to pass data in `myDownloadAsyncFunction` I can do like this `setDownloadQueue(downloadQueue.finally(myDownloadAsyncFunction(data))) ` right? – Ashish Choudary Sep 25 '22 at 13:38
  • @AshishChoudary No, you'll need to use `downloadQueue.finally(() => myDownloadAsyncFunction(data)))` – Bergi Sep 25 '22 at 13:42
  • @AshishChoudary Ah not quite - I've edited the examples to show how you would do that – Michal Charemza Sep 25 '22 at 13:42
  • Using `.finally()` will cause an unhandled promise rejection on every download after a single one has failed once. Better add `.catch()` on every call – Bergi Sep 25 '22 at 13:44
  • @Bergi Thanks! I didn't realise that's what happened without a ".catch()" – Michal Charemza Sep 25 '22 at 13:48
  • @Bergi Although I am just testing that and I don't think the catch is needed... perhaps with `then` it would be, but `finally` runs in the case of both failed and success. In fact, I thought that was the main purpose of `finally`? – Michal Charemza Sep 25 '22 at 14:02
  • @Bergi Ah I might have just misunderstood what you meant... but from testing, I think that with `finally` and no `catch`, then there is still only a single unhandled promise rejection in the console – Michal Charemza Sep 25 '22 at 14:08
  • 1
    Hm, there's only one unhandled promise rejection at a time, but it's a new one with every step; firing an `unhandledrejection` event for the new promsie and a `rejectionhandled` event for the previous promise. How that is displayed in the devtools seems to differ. – Bergi Sep 25 '22 at 14:12
  • Ah ok. In any case, opted to now use `then` + `catch` to avoid the whole thing, as well as showing where some more complex error handling could go. – Michal Charemza Sep 25 '22 at 14:14
  • 1
    Exactly, the `queue = queue.then(runStep).catch(console.error)` is what I recommend – Bergi Sep 25 '22 at 14:14
  • @Bergi & Michal... No this will not download in series. This will just download whichever would be done first(which I don't want) in the promise. I want them to be downloaded one after another. Suppose I click on the download button three times so I want first download to be completed first then second and then third. – Ashish Choudary Sep 25 '22 at 14:30
  • @AshishChoudary From my testing of the "fake" download function, no matter how fast I repeatedly click the download button, there is 1 second interval between output to the console, so I think it should do it in series. Note - if you're using the `fetch` API, you have to make sure `return fetch(...` from the `myDownloadAsyncFunction` function – Michal Charemza Sep 25 '22 at 14:36
  • 1
    Yes that's what I am saying... You have used `1000` in setInterval that is why it will run it in series but in my case that's api call so we do not know which `download` click will take how much time. Isn't it? – Ashish Choudary Sep 25 '22 at 14:42
  • 1
    @AshishChoudary Ah the `1000` here is just to emulate a request takes 1000ms so the example works without talking to another server. In the real case, the `myDownloadAsyncFunction` function can be `return fetch('https://domain.test/path')`, and it won't depend on how long it takes. – Michal Charemza Sep 25 '22 at 14:49
  • @AshishChoudary I've removed anything to do with the `1000`/fake download from the example now since I think it confused matters. – Michal Charemza Sep 25 '22 at 14:58
0

A simple way is :

const [downloading,setDownloading] = useState(false)

useEffect( ()=> {
  if(downloadQueue.length && !downloading){
    myDownloadAsyncFunction(downloadQueue[0].data).then(()=>{
      setDownloading(false)
      setDownLoadQueue((a) => a.slice(1))
    })
  }
},[downloadQueue])

From your code, I'm not sure how do you pass parameter to myDownloadAsyncFunction(). Here I juse assume that it use downloadQueue[0].data.

PeterT
  • 487
  • 3
  • 9