Search code examples
reactjsmaterial-uiprogress-barreact-custom-hooks

When uploading a file to react I'm able get the progress of the upload but unable to pass it back to the progress bar. Would a custom hook work here?


I have a material-ui LinearDeterminate progress bar, and I would like to pass on how far along the upload is.

const LinearDeterminate = ({ uploadPercentage, setuploadPercentage }) => {
  const classes = useStyles();
  const [uploadPercentage, setuploadPercentage] = useState("");

  console.log(uploadPercentage);

  return (
    <div className={classes.root}>
      <LinearProgress variant="determinate" value={uploadPercentage} />
    </div>
  );
};

...
    <UploadInput
      path={`customer_creatives/assets/${customer_id}/${new Date().getTime()}`}
      onChange={(value) =>
        updateFieldHandler("link_to_assets")({ target: { value } })
      }
      value={submissionData["link_to_assets"] || ""}
      label="Link to Assets"
      sublabel="*Zip files before uploading"
      isImage={false}
    />
    <LinearDeterminate />
...

UploadInput is a custom input component that links to DropZone (Where the upload happens)

import React, { useState } from "react";
import ReactDropzone from "react-dropzone";
import axios from "axios";
import { noop } from "lodash";

import HelpDialog from "components/HelpDialog";
import { API_URL } from "config";

const Dropzone = ({
  path,
  onChange = noop,
  children,
  multiple = false,
  maxSize,
  sizeHelper,
  ...props
}) => {
  const [url, setUrl] = useState("");
  const [loading, setLoading] = useState("");
  const [uploadPercentage, setuploadPercentage] = useState("");
  const [sizeHelperOpen, setSizeHelperOpen] = useState(false);
  const onDrop = ([file]) => {
    const contentType = file.type; // eg. image/jpeg or image/svg+xml
    console.log(file);
    if (maxSize && maxSize < file.size) {
      setSizeHelperOpen(true);
      return;
    }

    const generatePutUrl = `${API_URL}/generate-put-url`;
    const generateGetUrl = `${API_URL}/generate-get-url`;
    const options = {
      onUploadProgress: (progressEvent) => {
        //console.log("progressEvent.loaded " + progressEvent.loaded)
        //console.log("progressEvent.total " + progressEvent.total)
        let percent = Math.round(
          (progressEvent.loaded / progressEvent.total) * 100
        );
        setuploadPercentage({
          uploadPercentage: percent,
        });
        console.log(uploadPercentage);
      },
      params: {
        Key: path,
        ContentType: contentType,
      },
      headers: {
        "Content-Type": contentType,
      },
    };

    setUrl(URL.createObjectURL(file));
    setLoading(true);

    axios.get(generatePutUrl, options).then((res) => {
      const {
        data: { putURL },
      } = res;
      axios
        .put(putURL, file, options)
        .then(() => {
          axios.get(generateGetUrl, options).then((res) => {
            const { data: getURL } = res;
            onChange(getURL);
            setLoading(false);
          });
        })
        .catch(() => {
          setLoading(false);
        });
    });
  };
  return (
    <ReactDropzone onDrop={onDrop} multiple={multiple} {...props}>
      {({ getRootProps, getInputProps }) => (
        <>
          <div {...getRootProps()}>
            <input {...getInputProps()} />
            {children({ url, loading })}
          </div>
          <HelpDialog
            open={sizeHelperOpen}
            onClose={() => setSizeHelperOpen(false)}
          >
            {sizeHelper}
          </HelpDialog>
        </>
      )}
    </ReactDropzone>
  );
};

export default Dropzone;

I'm trying to get the results from the onUploadProgress function into my progress bar. Can I use a custom hook for that? My problem with that is Dropzone already has an export. Thanks for any advice!


Solution

  • It looks as simple as lifting the state up. You actually already have the { uploadPercentage, setuploadPercentage } props on the LinearDeterminate component. Just put that state in the common parent of the UploadInput and the LinearDeterminate components, and then keep passing down the handler to the DropZone component

    Remove the state from the LinearDeterminate component

    const LinearDeterminate = ({ uploadPercentage }) => {
      const classes = useStyles();
      return (
        <div className={classes.root}>
          <LinearProgress variant="determinate" value={uploadPercentage} />
        </div>
      );
    };
    

    Move it to the common parent

      const [uploadPercentage, setuploadPercentage] = useState("");
      ...
        <UploadInput
          path={`customer_creatives/assets/${customer_id}/${new Date().getTime()}`}
          onChange={(value) =>
            updateFieldHandler("link_to_assets")({ target: { value } })
          }
          value={submissionData["link_to_assets"] || ""}
          label="Link to Assets"
          sublabel="*Zip files before uploading"
          isImage={false}
          setuploadPercentage={setuploadPercentage}
        />
        <LinearDeterminate uploadPercentage={uploadPercentage}/>
      ...
    

    UploadInput component

    const UploadInput = ({ setuploadPercentage, ...allOtherCrazyProps }) => {
      ...
      return (
        <DropZone setuploadPercentage={setuploadPercentage} {...moreCrazyProps} />
      );
    };
    

    And finally, in the DropZone

    const Dropzone = ({
      path,
      onChange = noop,
      children,
      multiple = false,
      maxSize,
      sizeHelper,
      setuploadPercentage, // this is the new prop
      ...props
    }) => {
        ...
        const options = {
          onUploadProgress: (progressEvent) => {
            ...
            setuploadPercentage(percent); // is a string?
            console.log(uploadPercentage);
          },
          ...
        };
        ...
    };
    

    If you find it cumbersome passing the handler all the way down, you could use a Context to manage that state, but anywhere you use the UserInput you'll need to wrap it on the context provider.

    I'd also say to move all the uploading logic and build something similar to downshift-js: a hook that returns all necessary props to turn an element into a droppable uploader, but you already depend on the ReactDropzone component, so I don't think can be done unless you try this other pattern https://blog.bitsrc.io/new-react-design-pattern-return-component-from-hooks-79215c3eac00