2

A use case is:

  1. user drops a file
  2. Take a file and read with exceljs
  3. grab values from a column and keep it inside an array ids
  4. set a state variable onDropIds with the contents of ids. I got steps 1-3 working. I can't get 4 to work.

See: State always prints empty, even though the set contains values. See code lines 41-43.

Debug screenshot

import React, { useState, useCallback } from "react";
import { useDropzone } from "react-dropzone";
import Excel from "exceljs";

export default function Test(props) {
  // Local state
  const [onDropIds, setOnDropIds] = useState(new Set());

  // Callback fires as soon as the file is dropped
  const onDrop = useCallback((acceptedFiles) => {
    const file = acceptedFiles[0];
    const reader = new FileReader(); // reads the file using the `FileReader` API

    reader.onabort = () =>
      console.warn(`Reading of file ${file.path} was aborted.`);
    reader.onerror = () =>
      console.error(`Reading of file ${file.path} has failed.`);

    reader.onloadend = (e) => {
      const bufferArray = reader.result;

      // temporarily hold a set of values
      const ids = new Set();

      const workbook = new Excel.Workbook();
      workbook.xlsx
        .load(bufferArray)
        .then((sheet) => {
          workbook.worksheets[0].eachRow({ includeEmpty: true }, (row) => {
            const colValue1 = (row.values[1] || "").trim().toUpperCase();
            if (colValue1 && colValue1 !== "HEADERNAME") {
              ids.add(colValue1);
            }
          });
        })
        .then(() => {
          // set it to the state
          setOnDropIds(ids);

          // debug: print results
          console.log(onDropIds);
          console.log("---");
          console.log(ids);
        });
    };

    reader.readAsArrayBuffer(file);
  }, []);

  // Initialize the dropzone hook
  const { getRootProps, getInputProps, open, acceptedFiles } = useDropzone({
    noClick: true,
    noKeyboard: true,
    multiple: false,
    onDrop,
  });

  return (
    <div>
      <div
        {...getRootProps()}
        style={
          acceptedFiles && acceptedFiles.length
            ? {
                textAlign: "center",
                border: "1px solid #198562",
                marginTop: "0.5em",
                backgroundColor: "#d9fff3",
              }
            : {
                textAlign: "center",
                border: "1px dashed #000",
                marginTop: "0.5em",
              }
        }
      >
        <input {...getInputProps()} />
        <button
          style={{ marginTop: "0.5em" }}
          color={acceptedFiles && acceptedFiles.length ? "#00ff00" : "#ff0000"}
          onClick={open}
        >
          Browse
        </button>
        {acceptedFiles && acceptedFiles.length ? (
          <span>
            {acceptedFiles[0].path}{" "}
            <i className="fas fa-check-circle" style={{ color: "#75B436" }}></i>
          </span>
        ) : (
          "Drag 'n' drop a file here..."
        )}
      </div>
    </div>
  );
}
AmerllicA
  • 29,059
  • 15
  • 130
  • 154
Kyle
  • 5,407
  • 6
  • 32
  • 47

3 Answers3

3

The setOnDropIds is an async function, this means that React.js doesn't change the value of onDropIds right immediately during the setOnDropIds call, but it applies the change later, during an async component life-cycle computation. You need a useEffect hook to see a changed onDropIds value.

You could try something similar to:

const [onDropIds, setOnDropIds] = useState(new Set());

useEffect(() => {
  console.log("onDropIds from effect", onDropIds);
}, [onDropIds]);

and later:

 .then(() => {
   setOnDropIds(new Set([...onDropIds, ...ids]));
 });

But from your code is it not clear why you need to use a Set rather than simply an Array, probably it could be a viable option to use simply an array:

const [onDropIds, setOnDropIds] = useState([]);

useEffect(() => {
  console.log("onDropIds from effect", onDropIds);
}, [onDropIds]);

later:

const ids = [];
const workbook = new Excel.Workbook();

workbook.xlsx
  .load(bufferArray)
  .then((sheet) => {
    workbook.worksheets[0].eachRow({ includeEmpty: true }, (row) => {
      const colValue1 = (row.values[1] || "").trim().toUpperCase();

      if(colValue1 && colValue1 !== "HEADERNAME") ids.push(colValue1);
    });
  })
  .then(() => {
    setOnDropIds([...onDropIds, ...ids]);
  });

and finally convert onDropIds on a Set where you actually need it, with:

new Set(onDropIds);
Daniele Ricci
  • 15,422
  • 1
  • 27
  • 55
  • Not quite. But I'llt ry this. FYI: `onDropIds` is always empty, even on subsequent renders and cycles. i.e., I have tried dropping multiple files, on after another. Can't seem to get `onDropIds` to set. If it sets *sometimes*, then I expect the values to populate at least... – Kyle Jul 20 '20 at 14:52
  • Hi @Kyle , could you please check my updated answer based on your comments to other answers? – Daniele Ricci Jul 20 '20 at 16:42
  • Sorry @Kyle but I can't understand why in your comment to my answer you said you can't get `onDropIds` to set while in comments to other answers you stated that inside a `useEffect` (i.e. following my suggestion) you can get it set; what's changed between the two comments? Could you please update your question with the newest version of your code? Last (again from other comments) I learned that `onDropIds` get reset: it could be due your component is unmounted an later remounted or because `setOnDropIds` was called with an empty `Set`, without the concatenation it was acting as a reset. – Daniele Ricci Jul 20 '20 at 16:57
  • My use-case is taht `onDropIds` should get set every file drop into the dropzone. I'm seeing that it's printing empty on every drop. Not sure what's going on. I have tried your `setOnDropIds` using array expansion operator. I'll mark your answer and awared bounty. – Kyle Jul 20 '20 at 20:44
  • Btw, I can just return the value in the promise: `then(() => return ids;).then((promisedIds) => setOnDropIds(prev => new Set(...prev, ...promisedIds)))` – Kyle Jul 20 '20 at 20:48
  • Yes, I thought it was too concise and maybe hard to read. But moreover it seems your problem is solved @Kyle , isn't it? – Daniele Ricci Jul 23 '20 at 09:28
2

There is just on error in your code as shown below :

On Line 47:

reader.readAsArrayBuffer(file);

You have to pass file for it to be read.

The reason why you are unable to See: State always prints empty, even though set contains values at lines 41-43 is because setOnDropIds function sets the data asynchronously just like setState in case of class component.

So by the time the code reaches lines 41 it hasn't been set.

You can check that value has been set in onDropIds by adding console.log("onDropIds outside : ", onDropIds); just before return statement.

You can refer to below codesandbox and check console :

https://codesandbox.io/embed/serene-mclean-tsum3?fontsize=14&hidenavigation=1&theme=dark

A detailed explaination about how useState works asynchronously can be found at https://stackoverflow.com/a/54069332/5599770

Sawan Patodia
  • 773
  • 5
  • 13
  • Sorry, I did have the `file` object in there. Please see edit... Still not working. I think yoiu're onto something though... I may have to make the `FileReader` object return a promise. – Kyle Jul 20 '20 at 14:57
  • @Kyle https://codesandbox.io/embed/serene-mclean-tsum3?fontsize=14&hidenavigation=1&theme=dark have you checked this demo i created. It's working fine here and the onDemoIds is getting logged on console – Sawan Patodia Jul 20 '20 at 14:59
  • @Kyle what do you want to achieve once ids are set? Maybe that will clarify the issue. – Sawan Patodia Jul 20 '20 at 15:02
  • Actually, you are correct. I was able to capture it inside `useEffect`. https://i.imgur.com/cQtzty1.png My code was working all along... Strange though, `onDropIds` resets to empty every time. https://i.imgur.com/VMK111y.png – Kyle Jul 20 '20 at 15:16
  • It resets to empty. See expansion. I expect it to have two entries from (1) on a subsequent file drop (2): https://i.imgur.com/CfJUrjq.png – Kyle Jul 20 '20 at 15:22
  • @Kyle You mean you want to preserve the ids that were set from first drop and concat new ids when a new file is dropped? – Sawan Patodia Jul 20 '20 at 15:24
  • Yes. I do need to preserve. – Kyle Jul 20 '20 at 15:29
  • @Kyle I think i found the issue. You are not changing the setOnDropIds(ids); immutably. Change it to setOnDropIds(new Set(ids)); and // temporarily hold a set of values let ids = new Set(onDropIds); You can add below statement inside return's first div to check the values {JSON.stringify([...onDropIds])} – Sawan Patodia Jul 20 '20 at 16:36
  • @Kyle I have solved your issue. Please refer to the codesandbox demo : https://codesandbox.io/s/serene-mclean-tsum3?from-embed=&file=/src/Test.js – Sawan Patodia Jul 20 '20 at 16:52
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/218235/discussion-between-sawan-patodia-and-kyle). – Sawan Patodia Jul 20 '20 at 18:43
2

The setXxx function doesn't update the value immediately, it is asynchronous. You will see the new value on the next rerender.

If you need access the value in your component immediately you can use refs instead:

const onDropIds = useRef(new Set());
// onDropIds.current is now new Set() until this is changed elsewhere.

Then in the callback

.then(() => {
  // set it to the ref
  onDropIds.current = ids;

  // debug: print results
  console.log(onDropIds.current);
  console.log("---");
  console.log(ids);
});

EDIT: Reading your comment on other answers, it looks like you need the updater function:

setOnDropIds(prev => new Set([...prev, ...ids]))
Mordechai
  • 15,437
  • 2
  • 41
  • 82