2

I have a Dropzone that allows for multiple concurrent uploads and I want to show the progress for all uploads.

In my Dropzone component I have part of the state which is an array of uploads:

const [uploads, setUploads] = useState([])

Each element of the uploads array will be an upload object that has a few values, like so:

const uploading = {
  id: 1,
  files: <array of files>,
  progress: 0
}

Once files/folders are dropped into the dropzone, the "uploading" object will be added to the "uploads state array" and the files will be sent to the backend API, which asynchronously uploads the files to the server.

The backend will periodically send a progress callback to the UI, to update the progress value of the correct element in the uploads state array (see progressCallback below)

What I am currently unable to achieve is to make sure the UI re-renders every time an object in the uploads array is being updated to show progress, so that I can show the progress of all uploads as they happen.

The UI Component looks like this:

export function UploaderDropzone(props) {
  const [uploads, setUploads] = useState([])

  const progressCallback = useCallback((progressObject, sessionContext, step) => {
    const {uploadSessionParameters} = sessionContext
    let uploading = {}
    // let tmpArray = []
    const tmpArray = [...uploads]

    if (step === 'progress') {
      const filtered = findUploadById(tmpArray, uploadSessionParameters.uploadSessionId)
      uploading = filtered[0]

      if (uploading) {
        const itemIndex = tmpArray.indexOf(uploading)
        tmpArray.splice(itemIndex, 1)

        uploading.progress = progressObject.percentUpload

        tmpArray.push(uploading)
        setUploads(tmpArray)
        // setUploads(prevUploads => [...prevUploads, uploading])
      }

      console.log('progress tmpArray = ' + JSON.stringify(tmpArray));
      console.log('progress uploads = ' + JSON.stringify(uploads))
    }

    if (step === 'initialize') {
      const uploadNumber = uploads.length + 1

      uploading = {
        uploadSessionId: uploadSessionParameters.uploadSessionId,
        files: sessionContext.files,
        uploadNumber: uploadNumber,
        uploadName: `Upload #${uploadNumber}`,
        sent: false,
        progress: 0,
      }

      tmpArray.push(uploading)
      setUploads(tmpArray)

      console.log('initialize tmpArray = ' + JSON.stringify(tmpArray))
      console.log('initialize uploads = ' + JSON.stringify(uploads))
    }
  }, [uploads])

  const progressBars = uploads.map((upload) => {
    return (
      <Fragment>
        <ProgessBar progress={upload.progress} />
      </Fragment>
    )
  })

  // ... more code here ... not required for understanding

  return {
    <Fragment>
      <Dropzone 
        onDrop={
          acceptedFiles => {
            const filteredFiles = acceptedFiles.filter((file) => 
              validateFile(file))

              console.log("Filtered files" + filteredFiles)

              if (filteredFiles.length > 0) {
                setAlertMsg('')
              }
              else {
                setAlertMsg('No files uploaded.')
              }

              // call to Node.js backend, passing it the progressCallback
              startAsyncUploadSession(filteredFiles, progressCallback);
          }
        }
      />
      {progressBars}
    </Fragment>
    
  }

}

The ProgressBar component is very simple:

export function ProgressBar(props) {
  const {progress} = props

  return (
    <Fragment>
      <p>`${progress}% uploaded ...`</p>
    </Fragment>
  )
}

Right now, this code doesn't even show the progress bar even though the uploads state array is constantly being updated in the progressCallback. Since I don't know the number of concurrent uploads that will be done, I cannot set the state in the higher order component and pass it as props, I need the child component (ProgressBar) to receive it as props from the multiple objects in the state array ... but I am clearly missing something ...

Any pointers ? Any hooks I could use to register the progress value of the objects in the uploads state array so that every time the backend updates us on the progress it is reflected in the UI ?

Edit: To include the partial fix suggested by @Robin Zigmond

Edit2: After some debugging, it seems to be a synchronization issue. So I need to add some code and details here.

When files are dropped into the Dropzone, its sends the files to the Node.js backend through a function call to a mock server, the call to startAsyncUploadSession(filteredFiles, progressCallback); in the onDrop event of the Dropzone (which uses the react-dropzone lib).

It would seem that when I call progressCallback later, the state is as it was on the first render, aka uploads state array is an empty array as it was when the files were dropped, not the updated one which contains the object added to uploads array in the 'initializing' step.

So amended question would be "How to make sure that the UI state is up to date when the progressCallback is called later on by the backend ?"

1 Answer 1

1

The problem is in your state updating code inside progressCallback. Here is the offending code, for reference:

const tmpArray = uploads
const itemIndex = tmpArray.indexOf(uploading)
tmpArray.splice(itemIndex, 1)

// HERE UPDATING ONE OF ITEM'S VALUES IN UPLOADS STATE ARRAY
uploading.progress = progressObject.percentUpload

tmpArray.push(uploading)
setUploads(tmpArray)

What this does is:

  1. sets tmpArray to be a reference to the same object (uploads) as the current state
  2. then mutates that array, first by splicing an element out, then pushing a new element on to it

At no point in step 2) does the reference change. So when you then call setUploads(tmpArray) - which might as well be setUploads(uploads) as those two variables are still references to the exact same array - React thinks you're setting the state to be exactly what it was, and therefore doesn't know to rerender.

That's the long way of explaining why you should never mutate state, as you are doing here. You need to update it immutably - that is, leave the old state alone, construct a new object/array, and pass that to the function that sets the new state.

There are a number of ways to do that here, but in your case it should be as simple as just making tmpArray a (shallow) *copy) of the current state. That is, change:

const tmpArray = uploads

to

const tmpArray = [...uploads]

Now tmpArray is a new reference, to an array holding the same values as before. Note that the copy is only "shallow", so the objects inside the array are still references to just one underlying object for each array element. But that doesn't seem to matter here, because you don't mutate those objects. If you try your code with this change, I believe it should start to work (or at least get you past this particular problem).

Sign up to request clarification or add additional context in comments.

3 Comments

That is indeed a step in the right direction ... though now that I debug I notice I have another issue. Now what I have to resolve is the fact that after assigning "const tmpArray = [...upload]" I have tmpArray show as "undefined" ... even tho when I debug, I see that uploads contains elements. I wonder how it is possible that tmpArray is undefined after that line, maybe because the progressCallback is called by the backend which doesn't know UI state ?
Don't see how that can happen - anything of the form [ ...someVar ] will be an array (if it doesn't throw an error), there's no way it can be undefined. Unfortunately I can't debug for you, I can't see the rest of your code or what's going on when you step through the code - but perhaps you have some variable names confused somewhere?
I don't see how that can happen either, honestly I have tried making sure that tmpArray has at least an empty value, like so: let tmpArray = []; ... and then assigning it the value of uploads like so: tmpArray = [...uploads] and it still shows as undefined. I am running this in storybook, I might have to debug further.

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.