Search code examples
javascriptreactjsreact-hooksaxios

React function component re-rendering infinitely when using Axios in useCallback hook?


Here is a file uploading component, everything works at expected, however, when attempting to POST the file using Axios in a useCallback, the ProgressBar component re-renders infinitely if there is an error from Axios. If I comment out the Axios post, the component does not re-render infinitely. How do I avoid the infinite re-rendering of the ProgressBar component?

import { useState, useCallback, useEffect } from 'react'
import { useDropzone } from 'react-dropzone'
import uuid from 'react-uuid'
import axios from 'axios'

import ProgressBar from './ProgressBar'

const FileUploader = ({ setNotifications }) => {
  const [fileCount, setFileCount] = useState(0)
  const [filesUploaded, setFilesUploaded] = useState([])
  const [progress, setProgress] = useState(0)
  const [uploaded, setUploaded] = useState(false)
  const [exists, setExists] = useState(false)
  const [error, setError] = useState(false)
  
  const onDrop = useCallback(acceptedFiles => {
    acceptedFiles.forEach(file => {
      const reader = new FileReader()
      // console.log(file)
      reader.onloadstart = () => {
        const exists = filesUploaded.find(uploadedFile => uploadedFile.name === file.name)
        
        if (exists) {
          return setNotifications(notifications => {
            return [...notifications, `'${file.name}' has already been uploaded.`]
          })
        }
        // setStart(true)
        return setFilesUploaded(filesUploaded => {
          return [...filesUploaded, file]
        })
      }
      reader.onabort = () => {
        setError(true)
        console.log('file reading was aborted')
      }
      reader.onerror = () => {
        setError(true)
        console.log('file reading has failed')
      }
      reader.onprogress = e => {
        // console.log('loaded', e.loaded)
        // console.log('total', e.total)
        if (e.lengthComputable) {
          setProgress((e.loaded / e.total) * 100)
        }
      }
      reader.onload = async () => {
        // complete
        await axios.post(
          '/api/images',
          {
            file: reader.result
          }
        )
          .then(res => {
            if (res) {
              setUploaded(true)
              if (res === 200) {
                // success
                setExists(false)
              } else if (res === 409) {
                // already exists
                setExists(true)
              }
            }
          })
          .catch(err => {
            setError(true)
            console.error(err)
          })
      }
      reader.readAsArrayBuffer(file)
    })
    
  }, [filesUploaded, setNotifications])

  const { getRootProps, getInputProps } = useDropzone({ onDrop, multiple: true })

  useEffect(() => {
    setFileCount(filesUploaded.length)
   }, [setFileCount, filesUploaded, setNotifications])

  return (
    <div>
      <div className='file-uploader'>
        <div
          className='file-uploader-input'
          {...getRootProps()}
        >
          <input {...getInputProps()} />
          <p>Upload Files</p>
        </div>
      </div>
      <div className='progress-bar-container'>
        {filesUploaded.map(file => {
          return (
            <ProgressBar
              key={uuid()}
              file={file}
              progress={progress}
              uploaded={uploaded}
              exists={exists}
              error={error}
            />
          )
        })}
      </div>
    </div>
  )
}

export default FileUploader

Solution

  • The component re-renders because filesUploaded is modified in the callback each time and is listed as a dependency to the same callback. It looks like you wish to terminate the upload if the file already has been updated, but currently you only terminate the loadstart event handler. I suggest you move some of the functionality out from then loadstart event.

    acceptedFiles.forEach(file => {
      const exists = filesUploaded.find(uploadedFile => uploadedFile.name === file.name)
      if (exists) {
        setNotifications(notifications => {
          return [...notifications, `'${file.name}' has already been uploaded.`]
        })
      } else {
        const reader = new FileReader()
        reader.onloadstart = () => {
          return setFilesUploaded(filesUploaded => {
            return [...filesUploaded, file]
          })
        }
        [...]