【问题标题】:IntersectionObserver inside useEffect works only onceuseEffect 中的 IntersectionObserver 只工作一次
【发布时间】:2021-01-20 08:59:41
【问题描述】:

我正在使用 react 和相交观察者 api 构建一个无限滚动应用程序。

但是我被卡在了intersectionObserver只有一次在useEffect中的部分。

这里是代码

import React, { useState, useEffect, createRef } from 'react';
import axios from 'axios';

function App() {
  const [page, setPage] = useState(1);
  const [passengers, setPassengers] = useState([]);
  const bottomRef = createRef();

  const scrollCallback = (entries) => {
    if (entries[0].isIntersecting) {
      axios.get(`https://api.instantwebtools.net/v1/passenger?page=${page}&size=10`).then(res => setPassengers(prevState => [...prevState, ...res.data.data]));
    }
  }; 
  
  useEffect(() => {
    const observer = new IntersectionObserver(scrollCallback, {
      root: null,
      threshold: 1,
    });
    observer.observe(bottomRef.current);
    return () => {
      observer.disconnect();
    }
  }, [bottomRef]);

  return (
    <div className="container">
      <div className="lists">
        {
          passengers.map((passenger, idx) => {
            const { airline, name, trips } = passenger;
              return (
                 <div className="list" key={idx}>
                   <h4>User Info</h4>
                   <p>name: {name}</p>
                   <p>trips: {trips}</p>
                   <h4>Airline Info</h4>
                   <p>name: {airline.name}</p>
                   <p>country: {airline.country}</p>
                   <p>established: {airline.established}</p>
                   <p>head quarter: {airline.head_quarters}</p>
                   <p>website: {airline.website}</p>
                 </div>
              )
            })
        }
      </div>
      <div ref={bottomRef} />
    </div>
  );
}

当滚动到达底部时,useEffect 中的 scrollCallback 函数起作用,它将两个数组合并为一个,并最终显示我想要查看的合并数据。

但是,这只适用于一次。 useEffect 生命周期不再起作用。所以我将observer.disconnect(); 更改为observer.unobserve(bottomRef.current); 但这会出现错误提示

TypeError: Failed to execute 'unobserve' on 'IntersectionObserver': parameter 1 is not of type 'Element'.

为什么useEffect生命周期无法读取bottomRef??

工作代码示例:https://codesandbox.io/s/dazzling-newton-jvivj?file=/src/App.js

codesandbox 中的代码可以正常工作很奇怪

【问题讨论】:

  • 请用 runnable minimal reproducible example 更新您的问题,使用 Stack Snippets([&lt;&gt;] 工具栏按钮)演示问题。 Stack Snippets 支持 React,包括 JSX; here's how to do one.
  • const {current} = bottomRef; observer.observe(current); return () =&gt; observer.unobserve(current);
  • @AndreaGiammarchi 这解决了 TypeError 但 scrollCallback 仍然有效一次
  • @T.J.Crowder 知道了
  • 我已经编辑了我的答案stackoverflow.com/a/65806370/2800218

标签: javascript reactjs intersection-observer


【解决方案1】:

您需要使用useRef,而不是createRef,和as Andrea said,您需要使用与unobserve 一起使用的相同元素与observe。最后,由于您的 useEffect 回调使用的变量是 bottomRef.current,这就是要在您的依赖项数组中使用的依赖项。

这是一个工作示例,请参阅*** cmets:

const { useState, useEffect, useRef } = React;

function App() {
    const [page, setPage] = useState(1);
    const [passengers, setPassengers] = useState([]);
    const bottomRef = useRef(null); // *** Not `createRef`

    const scrollCallback = (entries) => {
        if (entries[0].isIntersecting) {
            getMorePassengers()
            .then(res => setPassengers(prevState => [...prevState, ...res.data.data]));
            // *** Should handle/report errors here
        }
    };

    useEffect(() => {
        // *** Grab the element related to this callback
        const { current } = bottomRef;
        const observer = new IntersectionObserver(scrollCallback, {
            root: null,
            threshold: 1,
        });
        observer.observe(current);
        return () => {
            observer.disconnect(current); // *** Use the same element
        }
    }, [bottomRef.current]); // *** Note dependency

    return (
        <div className="container">
            <div className="lists">
                {
                    passengers.map((passenger, idx) => {
                        // *** I simplified this for the example
                        const { name } = passenger;
                        return (
                            <div className="list" key={idx}>
                                <p>name: {name}</p>
                            </div>
                        )
                    })
                }
            </div>
            <div ref={bottomRef} />
        </div>
    );
}

// ** Stand-in for ajax
let passengerCounter = 0;
function getMorePassengers() {
    return new Promise(resolve => {
        resolve({
            data: {
                data: [
                    // Obviously, you don't use things like `passengerCounte`
                    {name: `Passenger ${passengerCounter++}`},
                    {name: `Passenger ${passengerCounter++}`},
                    {name: `Passenger ${passengerCounter++}`},
                    {name: `Passenger ${passengerCounter++}`},
                    {name: `Passenger ${passengerCounter++}`},
                ]
            }
        });
    });
}

ReactDOM.render(<App/>, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

但您可能根本不需要清理回调。 Andrea GiammarchiIntersectionObserver 对被观察的元素使用了弱引用,并且 A)他对这样的事情几乎总是正确的,所以我相信他; B) 以这种方式设计是有道理的。所以你可以完全放弃清理回调。

另外,我希望您观察到的div 只会在组件安装时创建一次,之后再也不会重新创建。所以理论上你可以使用[]作为你的依赖数组。

也就是说,如果 React 有理由重新创建 div,我会学习保留依赖关系,并且我更喜欢显式清理,所以我可能会离开它。

【讨论】:

    【解决方案2】:

    如果你可以observe 一个参考,你当然也可以unobserve 它,只要你有正确的,相同的,参考。

    useEffect(() => {
      const observer = new IntersectionObserver(scrollCallback, {
        root: null,
        threshold: 1,
      });
      const {current} = bottomRef;
      observer.observe(current);
      return () => {
        observer.unobserve(current);
      };
    }, [passengers]);
    

    这永远不会说current 不是节点。

    此外,如果更改的是 passengers,您最好保留该引用,而不是保留永远不会更改的内容,因为 bottomRefcurrent 包装器,但它不会改变“ever”,而 passengers 状态一旦相交就会改变。

    交替

    由于bottomRef 不打算改变,您也可以只观察该节点而不清除它的观察结果。

    useEffect(() => {
      const observer = new IntersectionObserver(scrollCallback, {
        root: null,
        threshold: 1,
      });
      observer.observe(bottomRef.current);
    }, [bottomRef]);
    

    无论哪种方式都应该可以解决问题。

    【讨论】:

    • 依赖项应该是[bottomRef.current](在卸载组件之前它可能永远不会改变),而不是[passengers]passengers 改变时,被观察的 div 不会改变。
    • 不可能,因为bottomRef.current是在render上定义的,之前是undefined(至少是第一次)
    • 是的,但是不会等到useEffect回调被调用的时候,因为useEffect的回调第一次调用是渲染和设置之后参考文献。 (我很偏执,所以无论如何我可能会有一个警卫,但这只是我的偏执...... :-))
    • 没关系...useEffect(callback, [THIS_IS_NOW]) 不能通过bottomRef.current,因为它会立即解决,而不是之后。关键是,如果bottomRef 总是一样的,div 也是一样的,就没有必要返回一个清理函数给 unobserve ...
    • 我知道它马上就解决了,但这就是变化的东西,所以这就是你用作依赖的东西。使用 bottomRef 毫无意义,它永远不会改变(根据保证)。返回回调对于卸载时的清理很重要。
    猜你喜欢
    • 2022-01-05
    • 2017-01-06
    • 2018-06-22
    • 2015-09-18
    • 2013-12-31
    • 1970-01-01
    • 2013-09-28
    • 2013-03-30
    • 2015-06-04
    相关资源
    最近更新 更多