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

时间:2020-07-16 10:22:56

标签: reactjs callback state react-functional-component

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


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


我最初构建的是自定义确认模式。我有一个状态变量,将 display:block display:none 设置为根元素。然后,如果在组件外部单击,则需要通过将状态变量设置为 false 来关闭模式。以下是原始功能。

  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)

3 个答案:

答案 0 :(得分:2)

您将clickListener的功能document.addEventListener分配给了{strong>组件安装,而此功能has a closure的值为count

在下一个渲染中,count的值将是陈旧的。

解决此问题的一种方法是改为使用引用闭包来实现该功能:

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);
    };
  }, []);
...
}

Edit fast-wood-stsrn

答案 1 :(得分:1)

您可以将回调传递给setIsvisible,因此不需要isVisible作为useCallback的依赖项。添加node.current是毫无意义的,因为node是一个引用并且会被突变:

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

答案 2 :(得分:0)

clickListener更改时count确实发生更改时,由于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不会注意到对引用的任何更改。依赖只能是状态或道具。