GoonGamja
GoonGamja

Reputation: 2286

IntersectionObserver inside useEffect works only once

I am building an infinite scroll app using react and intersection observer api.

But I am stuck at the part that intersectionObserver only once inside useEffect.

Here is the code

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

When a scroll hits the bottom, scrollCallback function inside useEffect works which merges two arrays into one and eventually shows merged data I want to see.

However this only works once. useEffect lifecycle does not work again. So I changed observer.disconnect(); to observer.unobserve(bottomRef.current); but this occurs an error saying

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

Why useEffect lifecycle could not read bottomRef??

working code example : https://codesandbox.io/s/dazzling-newton-jvivj?file=/src/App.js

It is weird that code in codesandbox works

Upvotes: 1

Views: 7410

Answers (2)

T.J. Crowder
T.J. Crowder

Reputation: 1074949

You need to use useRef, not createRef, and as Andrea said, you need to use the same element with unobserve that you use with observe. Finally, since the thing that your useEffect callback uses that varies is bottomRef.current, that's the dependency to use in your dependency array.

Here's a working example, see the *** comments:

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>

But you may not need that cleanup callback at all. Andrea Giammarchi says that IntersectionObserver uses weak references to the elements being observed, and A) he's almost always right about things like that, so I believe him; and B) it makes sense that it would be designed that way. So you can probably drop the cleanup callback entirely.

Separately, I'd expect that the div you're observing would be created just once, when the component is mounted, and never recreated afterward. So in theory you could just use [] as your dependency array.

That said, I'd learn toward retaining the dependency in case React has some reason to recratet that div, and I prefer explicit cleanup, so I'd probably leave it.

Upvotes: 1

Andrea Giammarchi
Andrea Giammarchi

Reputation: 3198

If you can observe a reference, you can surely unobserve it too, as long as you have the right, same, reference.

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

This won't ever say current is not a node.

Moreover, if what changes is passengers, you better hold on that reference, instead of holding on something that will never change, because bottomRef is the current wrapper, but it won't change "ever", while the passengers state will, once intersected.

Alternatiely

As bottomRef is not meant to change, you can also just observe that node and never cleanup its observation.

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

Either ways should solve the issue.

Upvotes: 0

Related Questions