简体   繁体   中英

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.

控制台输出

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

This is a hard concept. You are using into your member function a state which evaluates "" at render so regardless state change the function signature still the same before render this is the reason why useEffect and useCallback should be used to trait side effects about state change. But there are a way to ensure get correct state without hooks. Just passing state as a parameter to function, by this approach you will receive the current state at render so just with few changes you achieve this.

At someClosure just create an argument:

const someClosure = (value) => {...}

So into modal component,

options={{ onCloseStart: someClosure(value) }}

Should be what you are looking for

Issue

The issue here is that you've declared a function during some render cycle and the current values of any variable references are closed over in scope:

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

This "instance" of the callback is passed as a callback to a component and is invoked at a later point in time:

<Modal open={isOpen} options={{ onCloseStart: someClosure }}>
  <Button onClick={() => setValue("foo")}>Set value</Button>
  <Button onClick={() => setIsOpen(false)}>Hide modal</Button>
</Modal>

When the modal is triggered to close the callback with the now stale closure over the value state value is called.

Solution

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

Basically yes, use a React ref and a useEffect hook to cache the state value that can be mutated/accessed at any time outside the normal React component lifecycle.

Example:

import { useEffect, useRef, useState } from "react";

...

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

  const valueRef = useRef(value);

  useEffect(() => {
    console.log("Creating someClosure value =", value);
    valueRef.current = value; // <-- cache current value
  }, [value]);


  const someClosure = (argument) => {
    console.log("In someClosure value =", valueRef.current); // <-- access current ref 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(valueRef.current) // <-- access current ref value
        }}
      >
        <Button onClick={() => setValue("foo")}>Set value</Button>
        <Button onClick={() => setIsOpen(false)}>Hide modal</Button>
      </Modal>
    </div>
  );
}

编辑 react-stale-usestate-value-in-closure-how-to-fix

在此处输入图像描述

Although Drew's solution has solved the problem, but this proplem is actually caused by <Model> element which use options to pass callback function which has been resolved at first render. element don't update their options in the later rendering. This should be a bug.

In Drew's solution.

options={{
  onCloseStart: () => someClosure(valueRef.current) // <-- access current ref value
}}

this callback's argument is a ref object which has similar to a pointer. when the ref's current changed, it looks like the value is not stalled.

You can verify by add:

onClick={()=>someClosure(value)} 

in the <Model> element and you will see the value is updated.

This is a interesting problem, so I check the <Model> element source code in Github:

  useEffect(() => {
    const modalRoot = _modalRoot.current;
    if (!_modalInstance.current) {
      _modalInstance.current = M.Modal.init(_modalRef.current, options);
    }

    return () => {
      if (root.contains(modalRoot)) {
        root.removeChild(modalRoot);
      }
      _modalInstance.current.destroy();
    };
    // deep comparing options object
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [safeJSONStringify(options), root]);

You can find that the author use SafeJSONStringify(options) to do a deep comparing which don't care any state's value change.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM