React Hooks,属性值在渲染之间随机变化

时间:2019-10-20 13:46:01

标签: javascript reactjs react-hooks

我正在尝试使用react钩子将状态组件(即Gatsby Layout组件)转换为功能组件,以管理状态。我的目标是一旦我们到达页面中的某个点并向上滚动,便会打开一个模式。

我面临的问题是,我传递给此Layout组件(TrustedInView,请参见下面的代码)的一个道具正在更改渲染之间的值,如控制台日志所示。所以我对那里发生的事情感到困惑。我希望prop的值始终是相同的,因为index.js中记录的值没有更改(并且不应更改)。

这是布局组件中的代码:

// Hook
const usePrevious = value => {
  const ref = useRef();

  // Store current value in ref
  useEffect(() => {
    ref.current = value;
  }, [value]); // Only re-run if value changes

  // Return previous value (happens before update in useEffect above)
  return ref.current;
};

let notOpenedYet = true;
// Layout component
const Layout = ({ intl, children, trustedInView }) => {
  const [modalIsOpen, setOpen] = useState(false);

  // Get the previous value (was passed into hook on last render)
  let prevPosition = usePrevious(window.scrollY);

  const handleNavigation = e => {
    const custWindow = e.currentTarget;
    if (prevPosition > custWindow.scrollY) {
      console.log('trustedInView when goes up:', trustedInView);
      if (trustedInView && notOpenedYet) {
        notOpenedYet = false;
        setTimeout(() => setOpen(true), 1500);
      }
    }
    prevPosition = custWindow.scrollY;
  };

  useEffect(() => {
    // Add event listener when component mounts:
    window.addEventListener('scroll', e => handleNavigation(e));
    // Remove event listener when component unmounts:
    return window.removeEventListener('scroll', e => handleNavigation(e));
  });

这是带有布局组件的index.js渲染方法:

 render() {
    const { intl } = this.props;
    const { activeVideo, tabs, tabNumber, trustedInView } = this.state;
    const features = [
      // some objects
    ];
    console.log('trustedInView in index :', trustedInView);
    return (
      <Layout trustedInView={trustedInView}>

这是控制台日志:

I expect the value of the prop to always be the same

任何人都能让我步入正轨吗?

1 个答案:

答案 0 :(得分:1)

每个渲染器中都会设置一个新的侦听器,并且永远不会删除它。因此,在随后的滚动中,您实际上在每个事件中看到一个以上的console.log,其中一些通过闭包捕获了一个不同的trustedInView,从而使该值似乎在变化,而不是在变化。

此代码存在几个问题

  1. useEffect没有任何依赖性,因此在每个渲染器上都被调用
  2. useEffect removeEventListener并没有在组件卸载时真正被调用,useEffect期望您返回一个函数,并且无论removeEventListener的返回结果如何。
  3. 由于要创建一个新函数并将其传递给removeEventListener而不是传递给addEventListener每次创建的另一个函数,因此不会删除任何侦听器。
  4. 每个渲染器都具有handleNavigation的新版本,因此即使我们要修复先前的错误,也必须确保删除正确的侦听器。幸运的是,这就是useCallback的目的。

考虑到这些因素,看起来会像这样:

const handleNavigation = useCallback(() => { /* ... */ }, [prevPosition, trustedInView])

useEffect(() => {
  window.addEventListener('scroll', handleNavigation);
  return () => {
    window.removeEventListener('scroll', handleNavigation);
  }
}, [handleNavigation])

但是,即使这是有问题的,因为prevPosition每次破坏效果都会发生变化,加上会因您的目的而变得陈旧。因此,最好直接使用没有您的usePrevious钩子的ref

const prevValueRef = useRef(0);

const handleNavigation = useCallback(e => {
  const scrollY = e.currentTarget.scrollY;
  const prevScrollY = prevValueRef.current;

  // Save the value for the next event
  prevValueRef.current = scrollY;

  // ...

}, [trustedInView])