notElonMusk
notElonMusk

Reputation: 374

React Infinite Loading hook, previous trigger

Im trying to make a hook similar to Waypoint.

I simply want to load items and then when the waypoint is out of screen, allow it to load more items if the waypoint is reached.

I can't seem to figure out the logic to have this work properly.

Currently it see the observer state that its on the screen. then it fetches data rapidly.

I think this is because the hook starts at false everytime. Im not sure how to make it true so the data can load. Followed by the opposite when its reached again.

Any ideas.

Here's the hook:

import { useEffect, useState, useRef, RefObject } from 'react';

export default function useOnScreen(ref: RefObject<HTMLElement>) {
  const observerRef = useRef<IntersectionObserver | null>(null);
  const [isOnScreen, setIsOnScreen] = useState(false);

  useEffect(() => {
    observerRef.current = new IntersectionObserver(([entry]) => {
      if (isOnScreen !== entry.isIntersecting) {
        setIsOnScreen(entry.isIntersecting);
      }
    });
  }, []);

  useEffect(() => {
    observerRef.current.observe(ref.current);

    return () => {
      observerRef.current.disconnect();
    };
  }, [ref]);

  return isOnScreen;
}

Here's the use of it:

import React, { useRef } from 'react';
import { WithT } from 'i18next';

import useOnScreen from 'utils/useOnScreen';

interface IInboxListProps extends WithT {
  messages: any;
  fetchData: () => void;
  searchTerm: string;
  chatID: string | null;
}

const InboxList: React.FC<IInboxListProps> = ({ messages, fetchData, searchTerm, chatID}) => {
  const elementRef = useRef(null);
  const isOnScreen = useOnScreen(elementRef);

  if (isOnScreen) {
    fetchData();
  }

  
  const renderItem = () => {
    return (
      <div className='item unread' key={chatID}>
      Item
      </div>
    );
  };

  const renderMsgList = ({ messages }) => {
    return (
      <>
        {messages.map(() => {
          return renderItem();
        })}
      </>
    );
  };

  let messagesCopy = [...messages];

  //filter results
  if (searchTerm !== '') {
    messagesCopy = messages.filter(msg => msg.user.toLocaleLowerCase().startsWith(searchTerm.toLocaleLowerCase()));
  }

  return (
    <div className='conversations'>
      {renderMsgList({ messages: messagesCopy })}
      <div className='item' ref={elementRef} style={{ bottom: '10%', position: 'relative',backgroundColor:"blue",width:"5px",height:"5px" }} />
    </div>
  );
};

export default InboxList;

Upvotes: 0

Views: 1389

Answers (3)

Suryavarma MJ
Suryavarma MJ

Reputation: 21

As my assumption. Also you can try this way.

   const observeRef = useRef(null);
    const [isOnScreen, setIsOnScreen] = useState(false);
    const [prevY, setPrevY] = useState(0);
    
     
  useEffect(()=>{
    fetchData();

  var option = {
  root : null,
  rootmargin : "0px",
  threshold : 1.0 };

  const observer =  new IntersectionObserver(
      handleObserver(),
      option
    );
   
 const handleObserver = (entities, observer) => {
    const y = observeRef.current.boundingClientRect.y;
    if (prevY > y) { 
       fetchData();
    } 
    setPrevY(y);
  } 


},[prevY]);
   

In this case we not focus chat message. we only focus below the chat<div className="item element. when div element trigger by scroll bar the fetchData() calling again and again.. Explain :

-first off all in the hanlderObserver you can see boundingClientRect.y. the boundingClientRect method read the element postion. In this case we need only y axis because use y.

  • when the scrollbar reach div element, y value changed. and then fetchData() is trigger again.

  • root : This is the root to use for the intersection. rootMargin : Just like a margin property, which is used to provide the margin value to the root either in pixel or in percent (%) . threshold : The number which is used to trigger the callback once the intersection’s area changes to be greater than or equal to the value we have provided in this example . finally you can add loading status for loading data.

         return (
             <div className='conversations'>
               {renderMsgList({ messages: messagesCopy })}
               <div className='item' ref={observeRef} style={{ bottom: '10%', position: 'relative',backgroundColor:"blue",width:"5px",height:"5px" }} />
    
             </div>
           );
    

    };

I hope its correct, i'm not sure. may it's helpful someone. thank you..

Upvotes: 0

ckedar
ckedar

Reputation: 1909

In InboxList component, what we are saying by this code

  if (isOnScreen) {
    fetchData();
  }

is that, every time InboxList renders, if waypoint is on screen, then initiate the fetch, regardless of whether previous fetch is still in progress.

Note that InboxList could get re-rendered, possibly multiple times, while the fetch is going on, due to many reasons e.g. parent component re-rendering. Every re-rendering will initiate new fetch as long as waypoint is on screen.

To prevent this, we need to keep track of ongoing fetch, something like typical isLoading state variable. Then initiate new fetch only if isLoading === false && isOnScreen.

Alternatively, if it is guaranteed that every fetch will push the waypoint off screen, then we can initiate the fetch only when waypoint is coming on screen, i.e. isOnScreen is changing to true from false :

useEffect(() => {
  if (isOnScreen) {
    fetchData();
  }    
}, [isOnScreen]);

However, this will not function correctly if our assumption, that the waypoint goes out of screen on every fetch, does not hold good. This could happen because

  • pageSize of fetch small and display area can accommodate more elements
  • data received from a fetch is getting filtered out due to client side filtering e.g. searchTerm.

Upvotes: 1

Andrea
Andrea

Reputation: 1316

Let's inspect this piece of code

  const [isOnScreen, setIsOnScreen] = useState(false);

  useEffect(() => {
    observerRef.current = new IntersectionObserver(([entry]) => {
      if (isOnScreen !== entry.isIntersecting) {
        setIsOnScreen(entry.isIntersecting);
      }
    });
  }, []);

We have the following meanings:

  • .isIntersecting is TRUE --> The element became visible
  • .isIntersecting is FALSE --> The element disappeared

and

  • isOnScreen is TRUE --> The element was at least once visible
  • isOnScreen is FALSE--> The element was never visible

When using a xor (!==) you specify that it:

  • Was never visible and just became visible
    • this happens 1 time just after the first intersection
  • Was visible once and now disappeared
    • this happens n times each time the element is out of the screen

What you want to do is to get more items each time the element intersects

export default function useOnScreen(ref: RefObject<HTMLElement>, onIntersect: function) {
  const observerRef = useRef<IntersectionObserver | null>(null);
  const [isOnScreen, setIsOnScreen] = useState(false);

  useEffect(() => {
    observerRef.current = new IntersectionObserver(([entry]) => {
      setIsOnScreen(entry.isIntersecting);
    });
  }, []);
  
  useEffect(()=?{
    if(isOnScreen){
       onIntersect();
    }
  },[isOnScreen,onIntersect])

  ...
}

and then use it like:

  const refetch= useCallback(()=>{
    fetchData();
  },[fetchData]);

  const isOnScreen = useOnScreen(elementRef, refetch);

or simply:

  const isOnScreen = useOnScreen(elementRef, fetchData);

If fetchData changes reference for some reason, you might want to use the following instead:

  const refetch= useRef(fetchData);

  const isOnScreen = useOnScreen(elementRef, refetch);

Remember that useOnScreen has to call it like onIntersect.current()

Upvotes: 1

Related Questions