ref – https://www.freecodecamp.org/news/reveal-on-scroll-in-react-using-the-intersection-observer-api/
HomePage
We have a HomePage that calls getAllPhotos for the initial data retrieval.
We get the photos and next_cursor, but they are not rendered. Rather, we let a child component PageOne render these data.
notice here getAllPhotos’s parameter is empty.
1 2 3 4 5 6 7 8 9 10 11 12 |
const HomePage = async () => { const { photos, next_cursor } = await getAllPhotos(); const data = photos ? JSON.parse(photos) : []; return ( <div> <PageOne data={data} next_cursor={next_cursor} /> </div> ) } export default HomePage |
But first, let us analyze getAllPhotos and how it retrieves data.
getAllPhotos
The function has a parameter searchParams. The searchParams indicates what next is. Next is the initial element that we are starting at when we grab the next ten elements from mongodb.
1 2 3 4 5 6 7 |
export async function getAllPhotos(searchParams) { const sort = '-_id'; // sory by id const limit = 10; // how many docs to grab const next = searchParams?.next || null; } |
On the 1st pass, next is null because in HomePage getAllPhotos was called with empty param.
1 2 3 4 5 6 |
const photos = await PhotoModel.find({ _id: next ? sort === '_id' ? { $gt: next } : { $lt: next } : { $exists: true } }).limit(limit).sort(sort) |
Which means if next exists do this:
1 |
sort === '_id' ? { $gt: next } : { $lt: next } |
if next is null (which it is here on the 1st pass) then we just use { $exists: true }, which means we match documents that contains the field ‘_id’.
Therefore, on the 1st pass, PhotosoModel model would return ten documents in mongodb.
Then we look at the 9th doc. We get the id for it. This is so that we know the location of where to start grabbing data for the next time around. If next_cursor is null, then we are done.
1 |
const next_cursor = photos[limit - 1]?._id.toString() || undefined; |
We return the next_cursor so that child rendering components know how to proceed like so:
1 2 3 4 5 6 7 8 |
const { photos, next_cursor } = await getAllPhotos(); const data = photos ? JSON.parse(photos) : []; return ( <div> <PageOne data={data} next_cursor={next_cursor} /> </div> ) |
So now, let’s take a look at how PageOne rendering comonent processes this:
PageOne
This is a component that renders data.
First thing’s first. Let’s save the param data to state.
1 2 3 4 5 6 |
const PageOne = ({ data, next_cursor }) => { const [photos, setPhotos] = useState(data); const [next, setNext] = useState(next_cursor); ... ... } |
Its basically renders photos like this:
In Inspect window, you will see that we are observing a reference’s current to see if its true or false.
So let me explain why.
Observing when a button appears in our ViewPort
First, we need to create a custom hook. Hooks are reusable functions.
We need to implement a useInView in order to put it in our PageOne and observe if a button appears in our viewport.
In order to do this, we create a reference and point it to the button.
So we first create a custom hook.
1 2 3 4 |
const useInView = () => { const ref = useRef(); return { ref } } |
As you can see, we use a useRef to persist value of a button between renders. It can be used to store a mutable value that does not cause a re-render when updated.
Specifically, we return the ref from useInView and store a button in it:
1 2 3 4 5 6 7 8 9 10 |
const PageOne = ({ data, next_cursor }) => { const { ref, inView } = useInView(); <button className='btn_loadmore' disabled={loading} ref={ref} onClick={handleLoadMore}> { loading ? 'Loading...' : 'Load More' } </button> } |
We reference this button because we need to observe the ref’s current property. ref’s current is now button.btn_loadmore
Then create an observer and observe ref.current.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const useInView = () => { const ref = useRef(); const [inView, setInView] = useState(false); useEffect(() => { const el = ref.current; const observer = new IntersectionObserver(entries => { setInView(entries[0].isIntersecting) // our referenced button has appeared! }) if(el) observer.observe(el) return () => { if(el) observer.unobserve(el) } }, []); // runs once return { ref, inView } } |
We use an IntersectionObserver to observe for if our button has appeared in our viewport. In other words, the Intersection Observer API allows you to configure a callback that is called when a target element intersects either the device’s viewport or a specified element. That specified element is called the root element or root for the purposes of the Intersection Observer API.
So in our case, our useInView custom hook observes to see if the referenced button has appeared on our viewport. If it has, then it update inView state in our custom hook useInView.
This variable (inView) is being monitored by useEffect in PageOne, and when it changes to a ‘true’, we would load more data. (as shown in the image above)
If it changes to a false (when the button is not in viewport), then inView is false, and the evaluation would in PageOne would see that inView is false, we don’t do anything.
So in other words, inView is returned from custom hook useInView. When we have not scrolled down to make button appear in viewport, isIntersecting is ‘false’. So we don’t load more.
When we’ve scrolled so that the button appears in viewport, IntersectionObserver’s entries[0] isIntersecting is true. So we would call this function handleLoadMore().
1 2 3 4 5 6 7 |
useEffect(() => { if(inView) { console.log('inView (isIntersecting) is true!!!') handleLoadMore() } }, [inView]) |
2nd Pass
Now we have to ask ourselves, so the BUTTON appears and we handleLoadMore. But what is handleLoadMore ?
It stops loading if next becomes null, or is loading. It locks the function from being called multiple times by setLoading to true.
That way, other calls will return.
Then it goes and getAllPost with the marker next (which should be fef)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
async function handleLoadMore() { if(!next || loading) { console.log(`NOT next or is loading`, 'returning'); return; } setLoading(true) // lock const { photos, next_cursor } = await getAllPhotos({ next }) console.log('Images loaded, next_cursor: ', next_cursor); const data = photos ? JSON.parse(photos) : []; setPhotos(prev => [...prev, ...data]) // for the first pass, next_cursor is the id of the [9] item. setNext(next_cursor) // if next exists, the button for "load more" will exist // display: next ? 'block' : 'none' setLoading(false) // unlock } |
when getAllPhotos returns, next_cursor has gone down the next 10 items ($lt: next) and put it in photos. next_cursor has been updated to the next 9th element.
But this time around since next !== null, we run:
1 |
sort === '_id' ? { $gt: next } : { $lt: next } |
We’re sorting by sort ‘-_id’ so it comes to $lt: next, which means get less than from the next initial marker.
We keep scrolling down to make the button appear, which will do more loading data, until we come to the end. The end is where when we grab the last few items in our mongdo that is less than 10. At that point next is null, and we don’t load anymore.