1

I am trying to use MUI Loading Button and change the loading state of the button when it is clicked. The problem is that I am always getting false for downloadLoading even that I set its state to true in onClick event.

I have async function (downloadFile) in onClick, but I am setting the state of the loading before the function. When I log the state of downloadLoading inside the onClick and async function, it is false. When I remove the downloadFile function from onClick, state is set to true and I am able to see the loading indicator. How can I solve this problem? Why is it happening?

export const Row: React.FC<IProps> = ({
  record,
}: IProps): JSX.Element => {
 
  const [downloadLoading, setDownloadLoading] = useState<boolean>(false);

  useEffect(() => {
    return () => {
      setDownloadLoading(false);
    };
  }, []);

  const download = (content: any, fileName: string) => {
    const link = document.createElement("a");
    link.setAttribute("href", URL.createObjectURL(content));
    link.setAttribute("download", `${fileName}.pdf`);
    link.style.visibility = "hidden";
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  };

  const downloadFile = async (
    e: React.MouseEvent<HTMLElement>,
    id: string | undefined,
    fileName: string
  ) => {
    e.preventDefault();
   
    try {
      const response = await fetch(
        `http://localhost:8080/download/id/` +
          id
      );

      if (!response.ok) {
        response.json().then((value) => {
          console.log("val",value)
        });
      } else {
        const content = await response.blob();
        download(content, fileName);
      }
      setDownloadLoading(false);
    } catch (e) {
      console.log("error",e)
    }
  };

  return (
    <>
      <TableRow>
        <TableCell align="left" size="medium">
          {record.name}
        </TableCell>
        <TableCell size="small">
          <OutlinedCustomButton
            loading={downloadLoading}
            loadingIndicator={<CircularProgress color="inherit" size={20} />}
            loadingPosition="end"
            endIcon={<FileDownloadIcon />}
            fullWidth
            onClick={(e) => {
              setDownloadLoading(true);
              console.log(downloadLoading);
              downloadFile(e, record.id, record.name);
            }}
          >
            Download
          </OutlinedCustomButton>
        </TableCell>
      </TableRow>
    </>
  );
};
Enchantres
  • 853
  • 2
  • 9
  • 22
  • Does this answer your question? [Why does calling react setState method not mutate the state immediately?](https://stackoverflow.com/questions/30782948/why-does-calling-react-setstate-method-not-mutate-the-state-immediately) – Konrad Aug 30 '22 at 13:59
  • @KonradLinkowski I tried the solutions from the suggested link, but did not succeed to get things done. – Enchantres Aug 30 '22 at 14:06
  • So maybe try [this](https://stackoverflow.com/questions/54069253/the-usestate-set-method-is-not-reflecting-a-change-immediately), but the conclusion is that you can't just wait for the state to update. Use variables that are passed to `setState` – Konrad Aug 30 '22 at 14:08
  • About `setDownloadLoading(true); console.log(downloadLoading);` You'll be console logging the old `downloadLoading`. The new `downloadLoading` will be returned by `useState` the next time the component is rendered. – zoran404 Aug 30 '22 at 14:37
  • Your `useEffect` makes no sense. You are essentially resetting the loading indicator after your component has been removed. – zoran404 Aug 30 '22 at 14:40

3 Answers3

1

You are getting false for downloadLoading probably because of this part. At each re-render, it's setting the var to false:

  useEffect(() => {
    return () => {
      setDownloadLoading(false);
    };
  }, []);

If you are already initializing the var as false, no need for that useEffect:

  const [downloadLoading, setDownloadLoading] = useState<boolean>(false);

Also, it's worth mentioning a good practice here: if your var holds the value for a state like "process is loading" or "process is not loading", a more intuitive name would be "isLoading", instead of "downloadLoading". That way, you can see right away this var holds a boolean value.

  • You're wrong. his `useEffect` has `[]` as the second parameter, meaning that it is only executed once. – zoran404 Aug 30 '22 at 14:30
  • Actually `setDownloading(false)` is called from a clean up function (returned by `useEffect` callback). It is equivalent to `componentWillUnmount` & will run before the component is unmounted. – Nice Books Aug 30 '22 at 16:16
  • However, it's indeed best to rename `downloadLoading` to `isLoading`. – Nice Books Aug 30 '22 at 16:17
  • I removed the useEffect and loading is still not being set to true. – Enchantres Aug 31 '22 at 07:18
1

Your 1st useEffect is unnecessary as it only contains a cleanup function (equivalent to componentWillUnmount) that resets the state at the end.

Try converting the component to a class based one:

In the onClick callback:

onClick={(e)=> {
    this.setState({downloadLoading: true}, ()=> {
        // download happens after downloadLoading is set to true
        downloadFile(e, record.id, record.name);
    })
}}

To test, download a really large file or host the server, as localhost would be quite fast, to notice the loading.

Nice Books
  • 1,675
  • 2
  • 17
  • 21
0

As @NiceBooks said localhost would be quite fast to notice the loading, I used setTimeout, so the useState can be reflected in onClick():

        onClick={() => {
           setIsDownloadLoading(true);
           setTimeout(() => {
              downloadFile(record.supplierInstrument.id, record.name);
          }, 3000);
        }}

Now I am able to see the loading indicator.

Enchantres
  • 853
  • 2
  • 9
  • 22