Home > Software design >  Check if element is outside window React
Check if element is outside window React

Time:01-24

I am trying to create a code that checks whether the div element is outside the window, or not. The issue with the code is that it ignores the result on the isOpen state update, so as ignores if the element is slightly outside the window screen. Is there a chance it can be further adjusted?

export const useOnScreen = (ref, rootMargin = '0px') => {
    const [isIntersecting, setIntersecting] = useState(false);
    useEffect(() => {
        const observer = new IntersectionObserver(
            ([entry]) => {
                setIntersecting(entry.isIntersecting);
            },
            {
                rootMargin
            }
        );

        if (ref.current) {
            observer.observe(ref.current);
        }

        return () => {
            observer.unobserve(ref.current);
        };
    }, []);

    return isIntersecting;
};

export default useOnScreen;

Usage:

const packRef = useRef(null);

const isVisible = useOnScreen(packRef);

{ !optionsOpen && (
  <div className="" ref={ packRef }>

enter image description here

Assigning the class with Tailwind

<div
  className={ classNames('absolute top-full right-0 w-full bg-white mt-2 rounded-lg overflow-hidden shadow-2xl', {
    '!bottom-full': !isVisible
  }) }
  ref={ observe }
>

CodePudding user response:

Since the element you're trying to observe doesn't exist when the useEffect is called, the ref is null, and nothing is observed. When the element appears it's not magically observed.

Instead of an object ref, use a callback ref. The callback ref would be called as soon as the element appears. It would initialize the observer if needed, and would observe the element.

You don't need to unobserve a removed item in this case, since the observer maintains weak references to the observed elements.

Demo - scroll up and down and toggle the element:

const { useRef, useState, useEffect, useCallback } = React;

const useOnScreen = (rootMargin = '0px') => {
  const observer = useRef();
  const [isIntersecting, setIntersecting] = useState(false);

  // disconnect observer on unmount
  useEffect(() => () => {
    if(observer) observer.disconnect(); // or observer?.disconnect() if ?. is supported
  }, []);

  const observe = useCallback(element => {
    // init observer if one doesn't exist
    if(!observer.current) observer.current = new IntersectionObserver(
      ([entry]) => { setIntersecting(entry.isIntersecting); },
      { rootMargin }
    );

    // observe an element
    if(element) observer.current.observe(element)
  }, [rootMargin]);

  return [isIntersecting, observe];
};


const Demo = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [isVisible, observe] = useOnScreen();
  
  return (
    <div className="container">
      <div className="header">
        <button onClick={() => setIsOpen(!isOpen)}>Toggle {isOpen ? 'on' : 'off'}</button>
        <div>Visible {isVisible ? 'yes' : 'no'}</div>
      </div>
      
      {isOpen && <div className="child" ref={observe} />}
    </div>
  );
};

ReactDOM
  .createRoot(root)
  .render(<Demo />);
.container {
  height: 300vh;
}

.header {
  top: 10px;
  position: sticky;
}

.child {
  height: 30vh;
  margin-top: 110vh;
  background: blue;
}
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>

<div id="root"></div>

If you want the observer to trigger only when 100% of the element is visible set the threshold to 1 (or any percent the fits your case):

const { useRef, useState, useEffect, useCallback } = React;

const useOnScreen = (rootMargin = '0px') => {
  const observer = useRef();
  const [isIntersecting, setIntersecting] = useState(false);

  // disconnect observer on unmount
  useEffect(() => () => {
    if(observer) observer.disconnect(); // or observer?.disconnect() if ?. is supported
  }, []);

  const observe = useCallback(element => {
    // init observer if one doesn't exist
    if(!observer.current) observer.current = new IntersectionObserver(
      ([entry]) => { setIntersecting(entry.isIntersecting); },
      { 
        rootMargin, 
        threshold: 1 // define the threshold to control what amount of visibility triggers the observer
      }
    );

    // observe an element
    if(element) observer.current.observe(element)
  }, [rootMargin]);

  return [isIntersecting, observe];
};


const Demo = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [isVisible, observe] = useOnScreen();
  
  return (
    <div className="container">
      <div className="header">
        <button onClick={() => setIsOpen(!isOpen)}>Toggle {isOpen ? 'on' : 'off'}</button>
        <div>Fully visible {isVisible ? 'yes' : 'no'}</div>
      </div>
      
      {isOpen && (
        <div 
          className={classNames({ child: true, visible: isVisible })} 
          ref={observe} />
      )}
    </div>
  );
};

ReactDOM
  .createRoot(root)
  .render(<Demo />);
.container {
  height: 300vh;
}

.header {
  top: 10px;
  position: sticky;
}

.child {
  height: 30vh;
  margin-top: 110vh;
  background: blue;
}

.visible {
  border: 2px solid red;
}
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.3.2/index.min.js" integrity="sha512-GqhSAi WYQlHmNWiE4TQsVa7HVKctQMdgUMA 1RogjxOPdv9Kj59/no5BEvJgpvuMTYw2JRQu/szumfVXdowag==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

<div id="root"></div>

  • Related