繁体   English   中英

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

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

在我的代码的某个位置,我正在从回调( UserCallback )访问我的组件的 state 变量,我发现 state 变量尚未从初始值更新,回调指的是初始值。 正如我在文档中读到的,当变量作为数组项之一传递时,它应该在更新时更新 function。 以下是示例代码。


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


我最初构建的是自定义确认模式。 我有一个 state 变量,它将display:blockdisplay:none设置为根元素。 然后,如果在组件外部单击,我需要通过将 state 变量设置为false来关闭模式。 以下是原 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],
  )

它不会关闭,因为isVisible始终为false ,这是初始值。

我在这里做错了什么?

为了进一步澄清,以下是完整的组件。

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)

在组件 mount上将 function clickListener分配给document.addEventListener ,这个 function 对count数值有一个关闭

在下一次渲染时, count数值将是陈旧的。

解决它的一种方法是使用参考闭包来实现 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

虽然当count更改时您的clickListener确实会更改,但您只在安装时绑定初始clickListener一次,因为您的useEffect依赖项列表为空。 您也可以将clickListener添加到依赖项列表中:

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

旁注:在依赖列表中使用node.current不会做任何事情,因为 react 不会注意到对 ref 的任何更改。 依赖项只能是 state 或 props。

您可以将回调传递给 setIsvisible,因此您不需要isVisible作为useCallback的依赖项。 添加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