Search code examples
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

  • I agree with Drew's solution, but it felt for me a bit overcomplicated and also not very future-proof.

    I think if we put callback in the ref instead of value it makes thing a bit straightforward and you won't need to worry about other possible stale values.

    Example how it might look:

    export default function App() {
      const [isOpen, setIsOpen] = useState(false);
      const [value, setValue] = useState("");
    
      const someClosure = (argument) => {
        console.log("In someClosure value =", value);
        console.log("In someClosure argument =", argument);
        setIsOpen(false);
      };
      const someClosureRef = useRef(someClosure); // <-- new
      someClosureRef.current = someClosure; // <-- new
    
      return (
        <div className="App">
          <Button onClick={() => setIsOpen(true)}>Show modal</Button>
          <Modal
            open={isOpen}
            options={{ onCloseStart: () => someClosureRef.current() /** <-- updated **/ }}
          >
            <Button onClick={() => setValue("foo")}>Set value</Button>
            <Button onClick={() => setIsOpen(false)}>Hide modal</Button>
          </Modal>
        </div>
      );
    }
    

    https://codesandbox.io/s/react-stale-usestate-value-in-closure-how-to-fix-forked-tnj6x2?file=/src/App.js:235-996

    enter image description here