React Class 组件的问题与带有 onScroll Init 的 React Hook 行为不同

Problem with React Class Component Not Behaving Same as React Hook with onScroll Init

我正在尝试制作一个 React Class 组件,其行为与我开发的 React Hook 相同(并且正确)。我在 stackblitz 和此处显示的示例在页面首次加载时未显示正确的图像。一旦发生滚动事件,它就会变得正确。

我得到了以下显示不正确行为的示例。您可以通过将导入语句更改为当前被注释掉的语句来查看正确的行为。当我从 useEffect 的依赖数组中删除 isLoading 时,钩子以同样的方式中断。

https://stackblitz.com/edit/react-scroll-problem

注意:在构造函数中将 isLoading 更改为 false 可以解决问题,但会添加图像的双重显示(首先是黑白,然后是彩色),这是挂钩所没有的。

import * as React from "react";

class ImageToggleOnScrollCC extends React.Component {
  constructor(props) {
    super(props);
    this.imgRef = React.createRef();
    this.state = {
      inView: false,
      isLoading: true
    };
  }

  isInView = imageRefx => {
    if (this.imgRef.current) {
      const rect = this.imgRef.current.getBoundingClientRect();
      return rect.top >= 0 && rect.bottom <= window.innerHeight;
    }
    return false;
  };

  scrollHandler = () => {
    this.setState({
      inView: this.isInView()
    });
  };

  // componentDidUpdate(prevProps) {
  //   console.log("componentDidUpdate")
  //   if (this.props.isLoading !== prevProps.isLoading) {
  //     console.log("componentDidUpdate isLoading changed")
  //   }
  // }

  componentWillUnmount() {
    window.removeEventListener("scroll", scrollHandler);
  }

  componentDidMount() {
    window.addEventListener("scroll", this.scrollHandler);
    this.setState({
      inView: this.isInView(),
      isLoading: false
    });
  }

  render() {
    if (this.state.isLoading === true) {
      return null;
    } else {
      return (
        <div>
          <i>ImageToggleOnScrollCC - Class Component</i>
          <br />
          <img
            src={
              this.state.inView
                ? 'https://via.placeholder.com/200x200.png/0000FF/808080?text=ON-SCREEN'
                : 'https://via.placeholder.com/200x200.png?text=off-screen'
            }
            alt=""
            ref={this.imgRef}
            width="200"
            height="200"
          />
        </div>
      );
    }
  }
}

export default ImageToggleOnScrollCC;

下面是工作的 React Hook 组件,我希望上面的 Class 组件能够像上面的组件一样工作。

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

const ImageTogglerOnScroll = ({ primaryImg, secondaryImg }) => {
  const imageRef = useRef(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    window.addEventListener("scroll", scrollHandler);
    setInView(isInView());
    setIsLoading(false);
    return () => {
      window.removeEventListener("scroll", scrollHandler);
    };
  }, [isLoading]);

  const [inView, setInView] = useState(false);

  const isInView = () => {
    if (imageRef.current) {
      const rect = imageRef.current.getBoundingClientRect();
      return rect.top >= 0 && rect.bottom <= window.innerHeight;
    }
    return false;
  };

  const scrollHandler = () => {
    setInView(() => {
      return isInView();
    });
  };

  return isLoading ? null : (
    <div>
      <i>ImageToggleOnScroll - Functional Component React Hooks</i>
      <br />
      <img
        src={inView ? secondaryImg : primaryImg}
        alt=""
        ref={imageRef}
        width="200"
        height="200"
      />
    </div>
  );
};

export default ImageTogglerOnScroll;

这里的问题是两个组件都需要至少渲染 3 次才能显示图像,以及 inView 时加载的正确图像。

  1. 初始渲染,它将 isLoading 设置为 true。
  2. 只有在 isLoading 设置为 true 后,才会在渲染图像时分配元素的引用。
  3. 之后,您的组件可以追溯到 img 元素,确定它的位置并将其标记为 inView 或不标记。

在挂钩组件中,您有 useEffect((), obj) 检查 obj 的任何更改(本例 [isLoading]),然后在挂钩版本的组件上触发重新渲染。一旦 isLoading 更改并且 运行 的第 2 步和第 3 步,这将导致重新渲染。

同时,在 class 组件中,第 3 步实际上从来没有 运行s(没有用户手动触发滚动事件):它呈现第一次,然后在 componentDidMount()isLoading 设置为 false 就是这样。因为在那个时间点还没有设置图像引用,isInView() returns false 并且没有更多的代码 运行。此 class 组件缺少对后续 isLoading 更改的检查。

这是在 class 组件中实现该检查的一种方法:

class ImageToggleOnScrollCC extends React.Component {
  // ...

  componentDidUpdate(prevProps, prevState) {
    if (this.state.isLoading !== prevState.isLoading) {
      console.log("componentDidUpdate isLoading changed")
      console.log("this.imgRef.current is also assigned at this time", this.imgRef.current)

      this.setState({
        inView: this.isInView(), // will return true and rerender
      });
    }
  }

  // ...
}