Reputation: 2286
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
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
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