简体   繁体   English

从 useCallback 访问 state 变量时,值不会更新

[英]When accessing a state variable from a useCallback, value is not updated

At a certain place of my code I'm accessing a state variable of my component from a call back ( UserCallback ) and I find the state variable has not updated from the initial value and call back is referring to the initial value.在我的代码的某个位置,我正在从回调( UserCallback )访问我的组件的 state 变量,我发现 state 变量尚未从初始值更新,回调指的是初始值。 As I read in the documentation when variable is passed as one of array items then it should update the function when it is updated.正如我在文档中读到的,当变量作为数组项之一传递时,它应该在更新时更新 function。 Following is a sample code.以下是示例代码。


const Child = forwardRef((props, ref) => {
  const [count, setCount] = useState(0);
  const node = useRef(null);

  useImperativeHandle(ref, () => ({
    increment() {
      setCount(count + 1);
    }
  }));

  const clickListener = useCallback(
    e => {
      if (!node.current.contains(e.target)) {
        alert(count);
      }
    },
    [count]
  );

  useEffect(() => {
    // Attach the listeners on component mount.
    document.addEventListener("click", clickListener);
    // Detach the listeners on component unmount.
    return () => {
      document.removeEventListener("click", clickListener);
    };
  }, []);

  return (
    <div
      ref={node}
      style={{ width: "500px", height: "100px", backgroundColor: "yellow" }}
    >
      <h1>Hi {count}</h1>
    </div>
  );
});

const Parent = () => {
  const childRef = useRef();

  return (
    <div>
      <Child ref={childRef} />
      <button onClick={() => childRef.current.increment()}>Click</button>
    </div>
  );
};

export default function App() {
  return (
    <div className="App">
      <Parent />
    </div>
  );
}


What I'm originally building is a custom confirmation modal.我最初构建的是自定义确认模式。 I have a state variable which set either display:block or display:none to the root element.我有一个 state 变量,它将display:blockdisplay:none设置为根元素。 Then if there is a click outside the component I need to close the modal by setting state variable to false .然后,如果在组件外部单击,我需要通过将 state 变量设置为false来关闭模式。 Following is the original function.以下是原 function。

  const clickListener = useCallback(
    (e: MouseEvent) => {
      console.log('isVisible - ', isVisible, ' count - ', count, ' !node.current.contains(e.target) - ', !node.current.contains(e.target))
      if (isVisible && !node.current.contains(e.target)) {
        setIsVisible(false)
      }
    },
    [node.current, isVisible],
  )

It doesn't get closed because isVisible is always false which is the initial value.它不会关闭,因为isVisible始终为false ,这是初始值。

What am I doing wrong here?我在这里做错了什么?

For further clarifications following is the full component.为了进一步澄清,以下是完整的组件。

const ConfirmActionModal = (props, ref) => {

  const [isVisible, setIsVisible] = useState(false)
  const [count, setCount] = useState(0)

  const showModal = () => {
    setIsVisible(true)
    setCount(1)
  }

  useImperativeHandle(ref, () => {
    return {
      showModal: showModal
    }
  });

  const node = useRef(null)
  const stateRef = useRef(isVisible);

  const escapeListener = useCallback((e: KeyboardEvent) => {
    if (e.key === 'Escape') {
      setIsVisible(false)
    }
  }, [])

  useEffect(() => {
    stateRef.current = isVisible;
  }, [isVisible]);

  useEffect(() => {
    const clickListener = e => {
      if (stateRef.current && !node.current.contains(e.target)) {
        setIsVisible(false)
      }
    };

    // Attach the listeners on component mount.
    document.addEventListener('click', clickListener)
    document.addEventListener('keyup', escapeListener)
    // Detach the listeners on component unmount.
    return () => {
      document.removeEventListener('click', clickListener)
      document.removeEventListener('keyup', escapeListener)
    }
  }, [])

  return (
    <div ref={node}>
      <ConfirmPanel style={{ display : isVisible ? 'block': 'none'}}>
        <ConfirmMessage>
          Complete - {isVisible.toString()} - {count}
        </ConfirmMessage>
        <PrimaryButton
          type="submit"
          style={{
            backgroundColor: "#00aa10",
            color: "white",
            marginRight: "10px",
            margin: "auto"
          }}
          onClick={() => {console.log(isVisible); setCount(2)}}
        >Confirm</PrimaryButton>
      </ConfirmPanel>
    </div>

  )

}

export default forwardRef(ConfirmActionModal)

You assign a function clickListener to document.addEventListener on component mount , this function has a closure on count value.在组件 mount上将 function clickListener分配给document.addEventListener ,这个 function 对count数值有一个关闭

On the next render, the count value will be stale.在下一次渲染时, count数值将是陈旧的。

One way to solve it is implementing a function with refernce closure instead:解决它的一种方法是使用参考闭包来实现 function:

const Child = forwardRef((props, ref) => {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  useEffect(() => {
    // countRef.current always holds the most updated state
    const clickListener = e => {
      if (!node.current.contains(e.target)) {
        alert(countRef.current);
      }
    };

    document.addEventListener("click", clickListener);
    return () => {
      document.removeEventListener("click", clickListener);
    };
  }, []);
...
}

编辑 fast-wood-stsrn

While your clickListener does change when count changes you only bind the initial clickListener once on mount because your useEffect dependency list is empty.虽然当count更改时您的clickListener确实会更改,但您只在安装时绑定初始clickListener一次,因为您的useEffect依赖项列表为空。 You could ad clickListener to the dependency list as well:您也可以将clickListener添加到依赖项列表中:

useEffect(() => {
    // Attach the listeners on component mount.
    document.addEventListener("click", clickListener);
    // Detach the listeners on component unmount.
    return () => {
      document.removeEventListener("click", clickListener);
    };
}, [clickListener]);

Side note: using node.current in a dependency list doesn't do anything as react does not notice any changes to a ref.旁注:在依赖列表中使用node.current不会做任何事情,因为 react 不会注意到对 ref 的任何更改。 Dependencies can only be state or props.依赖项只能是 state 或 props。

You can pass a callback to setIsvisible so you don't need isVisible as a dependency of the useCallback .您可以将回调传递给 setIsvisible,因此您不需要isVisible作为useCallback的依赖项。 Adding node.current is pointless since node is a ref and gets mutated:添加node.current是没有意义的,因为 node 是一个 ref 并且会发生变异:

const clickListener = useCallback((e) => {
  setIsVisible((isVisible) => {//pass callback to state setter
    if (isVisible && !node.current.contains(e.target)) {
      return false;
    }
    return isVisible;
  });
}, []);//no dependencies needed

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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