'State not captured by callback in React component

I have this component that uses useState, and wraps its children in a Modal component, which is an old component from our application, and the modal component happens to be a total nightmare. But it takes an acceptData object which has a callback prop for when the "accept" button is clicked.

My issue is that my component is getting its state updated via another child (see onFileReadComplete), but then later when the button is clicked, that state is null. I can even see in the React dev tools that the state is not null, it has the value I expect.

Here is the component:

const CSVEditSwitcher = ({ acceptData, hideData }) => {
  const [parsedData, setParsedData] = useState(null);
  const dispatch = useDispatch();
  const enterLineups = lineupObject => dispatch(csvEditAPI(lineupObject));

  const modalProps = {
    className: "csv-upload",
    commonHide: true,
    acceptData: {
      actionName: "Submit",
      className: acceptData.className,
      callback: () => {
        if (!parsedData) {
          console.log("no data!"); // we keep getting here!
          return;
        }
        enterLineups(parsedData.masterLineups).then(acceptData.callback);
      },
    },
    hideData,
    validator: () => true,
  };

  return (
    <Modal {...modalProps}>
      {!parsedData ? (
        <UploadEditTemplate
          onFileReadComplete={data => {
            console.log({ data }); // this is always correct!
            setParsedData(data);
          }}
        />
      ) : (
        <ImportResults {...parsedData} />
      )}
    </Modal>
  );
};

In case it's some crazy this issue, I've tried replacing the callback with a function () { } syntax, to no avail.

There's nothing interesting in the call point for the callback in Modal:

  accept(e) {
    if (!e) return;
    if (e.key && (e.key !== "Enter" || e.key !== "Return")) return;

    const { acceptCallback } = this.state;
    console.log(acceptCallback, e);
    if (acceptCallback) acceptCallback(e);
  }

Update

As a hail-mary, I tried converting this to a class component (wrapping it in a functional component that provides the hook values). It actually works, but only when accessing this.state.parsedData explicitly in the callback. i.e., I have const { parsedData } = this.state at the top of the render method for the other uses of that value, but the destructured variable does not work in the callback.

I would love if anyone could lend insight on what the hell is going on here!



Solution 1:[1]

While destructuring props in injection ({...modalProps}...), you create a new instance in memory. Therefore callback prop will always have initial parsedData value.

My suggestion is to use callback as a reference to useCallback which aware to state manipulation

 const callback = useCallback(() => {
        if (!parsedData) {
          console.log("no data!")
          return;
        }
        enterLineups(parsedData.masterLineups).then(acceptData.callback);
      }, [parsedData])

 const modalProps = {
    className: "csv-upload",
    commonHide: true,
    acceptData: {
      actionName: "Submit",
      className: acceptData.className,
      callback
    },
    hideData,
    validator: () => true,
  };

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Hagai Harari