reactjsreact-hooksmodal-dialogclosuresstale

React stale useState value in closure - how to fix?


I want to use a state variable (value) when a modal is closed. However, any changes made to the state variable while the modal is open are not observed in the handler. I don't understand why it does not work.

CodeSandbox

or

Embedded CodeSandbox

  1. Open the modal
  2. Click 'Set value'
  3. Click 'Hide modal'
  4. View console log.

Console output

My understanding is that the element is rendered when the state changes (Creating someClosure foo), but then when the closure function is called after that, the value is still "". It appears to me to be a "stale value in a closure" problem, but I can't see how to fix it.

I have looked at explanations regarding how to use useEffect, but I can't see how they apply here.

Do I have to use a useRef or some other way to get this to work?

[Edit: I have reverted the React version in CodeSandbox, so I hope it will run now. I also implemented the change in the answers below, but it did not help.]

import { useState } from "react";
import { Modal, Button } from "react-materialize";

import "./styles.css";

export default function App() {
  const [isOpen, setIsOpen] = useState(false);
  const [value, setValue] = useState("");

  console.log("Creating someClosure value =", value);

  const someClosure = (argument) => {
    console.log("In someClosure value =", value);
    console.log("In someClosure argument =", argument);
    setIsOpen(false);
  };

  return (
    <div className="App">
      <Button onClick={() => setIsOpen(true)}>Show modal</Button>
      <Modal open={isOpen} options={{ onCloseStart: () => someClosure(value) }}>
        <Button onClick={() => setValue("foo")}>Set value</Button>
        <Button onClick={() => setIsOpen(false)}>Hide modal</Button>
      </Modal>
    </div>
  );
}

Solution

  • PLEASE read this answer, there is a simple and future-proof way of getting around this problem.

    The closure traps the old value because it cannot guarantee that when it is run, it can access those variables in scope (because you could pass that closure to another component to run, where the state doesn't exist). The solution is to use an overloaded version of the setState method as your update function, that provides the old value to you itself. This is what is would look like for your code:

    import { useState } from "react";
    import { Modal, Button } from "react-materialize";
    
    import "./styles.css";
    
    export default function App() {
      const [isOpen, setIsOpen] = useState(false);
      const [value, setValue] = useState("");
    
      console.log("Creating someClosure value =", value);
    
      const someClosure = (argument) => {
        // NEW CODE -------------------------------------------------
        setIsOpen((oldVal) => {
          console.log("In someClosure value =", oldVal);
          console.log("In someClosure argument =", argument);
          return false;
        });
        // OLD CODE HERE --------------------------------------------
        // console.log("In someClosure value =", value);
        // console.log("In someClosure argument =", argument);
        // setIsOpen(false);
      };
    
      return (
        <div className="App">
          <Button onClick={() => setIsOpen(true)}>Show modal</Button>
          <Modal open={isOpen} options={{ onCloseStart: () => someClosure(value) }}>
            <Button onClick={() => setValue("foo")}>Set value</Button>
            <Button onClick={() => setIsOpen(false)}>Hide modal</Button>
          </Modal>
        </div>
      );
    }