Setting a state variable in react-dropzone `onDrop`, after reading with exceljs

Viewed 1087

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>
  );
}
3 Answers

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);

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

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]))
Related