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