Saulet Yskak
Saulet Yskak

Reputation: 41

Upload file stuck on 'uploading' status with customRequest Ant Design (antd)

I really need your help with uploading files using Ant Design (antd). I don't need request action with upload component, therefore I use the onSuccess() function in customRequest to skip fetch, but onChange method the status stucks only on 'uploading'. It doesn't go to 'done' status. I need your help, please

Sandbox link: https://codesandbox.io/s/runtime-platform-znyow?file=/src/App.js

    import React, { useState } from "react";
    import "./styles.css";
    import { Button, Upload } from "antd";

    export default function App() {
      const [fileList, setFileList] = useState([]);

      const fileProps = {
        action: "",
        name: "file",
        multiple: true,
        customRequest(e) {
          e.onSuccess();
        },
        onChange(info) {
          const { status } = info.file;
          console.log(status); // always 'uploading'
          if (status === "done") {
            // this part is unreachable
            let fileList = [...info.fileList];
            setFileList(fileList);
          }
        },
        onRemove: (file) => {
          setFileList((prevState) => prevState.filter((d) => d.uid !== file.uid));
        }
      };

      return (
        <div className="App">
          <Upload {...fileProps} fileList={fileList}>
            <Button type="primary">Attach file</Button>
          </Upload>
        </div>
      );
    }

Upvotes: 4

Views: 7948

Answers (4)

Charlie Drews
Charlie Drews

Reputation: 11

Here's what fixed it for me. When my onChange prop looked like this, I had the same result, alway stuck in Uploading and never shows as Done:

onChange: (info) => {
  setFileList(info.fileList);
},

Then I read this section of the react documentation about why you might not get a re-render when expected:

React will ignore your update if the next state is equal to the previous state, as determined by an Object.is comparison. This usually happens when you change an object or an array in state directly:

obj.x = 10;  // 🚩 Wrong: mutating existing object
setObj(obj); // 🚩 Doesn't do anything

You mutated an existing obj object and passed it back to setObj, so React ignored the update. To fix this, you need to ensure that you’re always replacing objects and arrays in state instead of mutating them:

// ✅ Correct: creating a new object
setObj({
  ...obj,
  x: 10
});

That made me think I should try creating a copy of the fileList before updating my state, and sure enough, that worked:

onChange: (info) => {
  const newFileList = [...info.fileList];
  setFileList(newFileList);
},

Now the progress bar animates as expected and the list entry switches to the Done appearance when it should.

Upvotes: 1

tannerbaum
tannerbaum

Reputation: 422

I have been banging my head against the wall for hours, and you won't believe what the fix to this is. Set fileList to undefined on the Upload component if the array is empty, which was mine and your default state value.

return (
    <div className="App">
      <Upload {...fileProps} fileList={fileList.length > 0 ? fileList : undefined}>
        etc
      </Upload>
    </div>
  );

As soon as I did that I was getting status "done" in my onChange handler.

I was tipped off by this comment in an AntD issue.


Edit: It appears that if you want to upload again after an initial batch, the fileList prop has to stay undefined, which means even more work on our end...

In case it helps anyone, here is the working hook I made in our project to manage this. I wish it had half the React state tracking everything, but seems necessary in my case with this limitation of antd.

const useUpload = ({
  fileBucket,
  setFileList,
  fileList,
}) => {
  const [updatedFiles, setUpdatedFiles] = useState([]);
  const [completed, setCompleted] = useState(0);
  const [fileTotal, setFileTotal] = useState(0);

  const storage = getStorage();

  // Only update the file list when all files have their downloadUrl fetched
  useEffect(() => {
    if (fileTotal === 0) {
      return;
    }

    if (fileTotal === completed) {
      setFileList([...fileList, ...updatedFiles]);
      setCompleted(0);
      setFileTotal(0);
    }
  }, [completed, fileTotal, setFileList, updatedFiles, fileList]);

  // antd cancels upload on a return of false
  const beforeUpload = async (file) => {
    // ... not relevant  
  };

  const customRequest = ({ onError, onSuccess, file }) => {
    const storageRef = ref(
      storage,
      `${fileBucket}/${new Date()} -- ${generateRandomId()}`
    );

    uploadBytes(storageRef, file)
      .then((snapshot) => getDownloadURL(snapshot.ref))
      .then((downloadUrl) => {
        onSuccess({ downloadUrl });
      })
      .catch(() => {
        onError();
      });
  };

  const onChange = async ({ file, fileList }) => {
    setFileTotal(fileList.length);

    if (file.status === "done") {
      const { downloadUrl } = file.response;

      // Using the callback function on setState functions helps deal with race conditions.
      // We still need to delay the update until all the files have loaded, hence the regretable two state values.
      setUpdatedFiles((prevState) => {
        const fileIndex = fileList.findIndex((fileInList) => {
          return fileInList.uid === file.uid;
        });

        return [
          ...prevState,
          {
            ...file,
            url: downloadUrl,
            pageNumber: fileIndex + 1,
          },
        ];
      });
      setCompleted((prevState) => prevState + 1);

      message.success(`${file.name} file uploaded successfully`);
    }

    if (file.status === "error") {
      message.error(`${file.name} file upload failed.`);
    }
  };
  return { beforeUpload, onChange, customRequest };
};

Upvotes: 1

Quyet Hoang
Quyet Hoang

Reputation: 1

Please setFileList(fileList) in function onChange(info)

Upvotes: 0

Bastek
Bastek

Reputation: 83

i've had similiar problem after updating version of antd. Here's the code how i fix this without sending request.

    const handleChange = useCallback((info) => {
    if (info.file.status === 'uploading') {
        setImage({ loading: true, image: null });
        info.file.status = 'done';
    }
    if (info.file.status === 'done') {
        getBase64(info.file.originFileObj, (imageUrl) => {
            const img = new Image();
            img.src = imageUrl;
            img.addEventListener('load', function () {
                setImage({ loading: false, image: imageUrl });
                setFileList([{ ...info.fileList[0] }]);
            });
        });
    }
}, []);

inside condition (info.file.status === 'uploading') i've changed info.file.status === 'done' and surprsingly it work. Probably there's better solution to solve this, but maybe this will help

Upvotes: 3

Related Questions