javascriptreactjsfilereaderreact-dropzone

Make FileReader return ALL files as parsed JSONs inside a single array


I need to return ALL files dropped into the dropzone as an array of parsed JSONs and use that array as the "source of truth" in the parent component one level above. When the files are loaded, I read them with the FileReader, parse them, and create a payload, so that all the needed data parsed from files is sent to the parent component as a JSON payload, from where it can be redistributed further as props.

I need this parsed data to be an array of JSONs, but when I try to create one I either get Object is not iterable error or an array of undefined.

Dropzone component (which I installed from here)

const Dropzone = ({
  onSetPayload,
  onResetPayload,
  onAddCommonName,
  ...props
}) => {
  const onDrop = useCallback(
    (acceptedFiles) => {
      onResetPayload([]);

      acceptedFiles.forEach((file) => {
        const reader = new FileReader();
        reader.onabort = () => {
          throw new Error("file reading was aborted");
        };
        reader.onerror = () => {
          throw new Error("file reading has failed");
        };
        reader.onload = () => {
          const cert = new x509.X509Certificate(reader.result);
          const payload = getPayload(cert); // parses my certificate into a JSON payload
          onSetPayload(payload);
        };
        reader.readAsArrayBuffer(file);
      });
    },
    [onSetPayload, onResetPayload]
  );

The solution I've got so far is to update state based on previous state - i.e. a batch of files is dropped, then they are read and parsed one-by-one and our setPayload re-sets its state as many times as there are files dropped, which I want to avoid.

Parent component (CardContent).

const CardContent = (props) => {
  const [addIsActive, setAddIsActive] = useState(true);
  const toggleAddButton = () => setAddIsActive((prevState) => !prevState);

  const [payload, setPayload] = useState([]);
  const getPayload = (p) => setPayload((prev) => [...prev, p]); 
/* This setPayload does the job of a vanilla reducer, which is suboptimal.
It updates state as many times as there are files coming from FileReader.
I would rather overwrite state once with a new array from FileReader. */

  return (
    <section className={styles.CardContent}>
      <Aside
        onAddClick={toggleAddButton}
        addIsActive={addIsActive}
        payload={payload}
      />
      {addIsActive ? (
        <Dropzone
          onSetPayload={getPayload}
          onResetPayload={setPayload}
        />
      ) : (
        <output style={{ border: "2px solid #333" }}>This is output</output>
      )}
    </section>
  );
};

I cannot create an array and mutate it. I cannot seem to spread the results of reader.onload calls into an array. When I try to map the acceptedFiles into a new array, undefined is returned or worse - an infinite loop ensues.

Thank you in advance for any help.


Solution

  • You could try wrapping your Filereader code in a promise. Collect the array of promises, run it through Promise.all, setstate in .then func.

    (acceptedFiles) => {
          onResetPayload([]);
          
          const promises = acceptedFiles.map(file => new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onabort = () => {
              reject("file reading was aborted");
            };
            reader.onerror = () => {
              reject("file reading has failed");
            };
            reader.onload = () => {
              const cert = new x509.X509Certificate(reader.result);
              const payload = getPayload(cert); // parses my certificate into a JSON payload
              resolve(payload);
            };
            reader.readAsArrayBuffer(file);
          }));
          
          Promise.all(promises).then(resultArr => {
            // resultArr is an array of all the promise results.  It should be the array you are after
            setToSomeState(resultArr);
          },
          error => {
            // do something with error
          });