useVisibleElement
2023-10-18
약 2분
useVisibleElement
스크롤 시 해당 DOM 요소가 노출됬는지 여부 감지
Hooks(Legacy)
tsx1import { useCallback, useEffect, useRef, useState } from 'react'; 2 3export default function useVisibleElement<T extends HTMLElement>(): { 4 ref: React.RefObject<T>; 5 isVisible: boolean; 6} { 7 const [isVisible, setIsVisible] = useState<boolean>(false); 8 const ref = useRef<T>(null); 9 10 const checkVisibility = useCallback(() => { 11 if (ref.current) { 12 const rect = ref.current.getBoundingClientRect(); 13 14 if (rect.top < window.innerHeight - 300 && rect.bottom > 0) { 15 setIsVisible(true); 16 } else { 17 setIsVisible(false); 18 } 19 } 20 }, []); 21 22 useEffect(() => { 23 window.addEventListener('scroll', checkVisibility); 24 window.addEventListener('touchmove', checkVisibility); 25 26 checkVisibility(); 27 28 return () => { 29 window.removeEventListener('scroll', checkVisibility); 30 window.removeEventListener('touchmove', checkVisibility); 31 }; 32 }, [checkVisibility]); 33 34 return { ref, isVisible }; 35}
Hooks(intersectionObserver API 사용)
intersectionObserver API를 사용하며 Hook의 기능을 추가/변경했다.
Props
- activeClass: 해당 요소가 노출될 때 추가할 클래스명
- key: 추적 요소의 고유 키값 부여
Return
- ref: 추적할 요소를 가진 container(div tag)를 ref로 지정하기 위함
- activeKey: 현재 노출된 요소의 키값
- activeElement: 현재 노출된 Element 요소
tsx1import { useEffect, useRef, useState } from 'react'; 2 3interface Props { 4 key?: string; 5 activeClass?: string; 6} 7 8export default function useVisibleElement(props?: Props) { 9 const { key = 'visible-element', activeClass } = props || {}; 10 11 const [activeKey, setActiveKey] = useState<string>(''); 12 const [activeElement, setActiveElement] = useState<Element | null>(null); 13 const ref = useRef<HTMLDivElement>(null); 14 15 const callback = (entries: IntersectionObserverEntry[]) => { 16 entries.forEach((entry) => { 17 const key = entry.target.getAttribute('data-key'); 18 19 if (entry.isIntersecting) { 20 if (activeClass) { 21 entry.target.classList.add(activeClass); 22 } 23 24 setActiveKey(key || ''); 25 setActiveElement(entry.target); 26 } else if (activeClass) { 27 entry.target.classList.remove(activeClass); 28 } 29 }); 30 }; 31 32 useEffect(() => { 33 const intersectionObserver = new IntersectionObserver(callback); 34 35 const scrollItems = Array.from(ref.current?.children as HTMLCollection); 36 37 if (!scrollItems?.length) return () => {}; 38 39 scrollItems.forEach((item, index) => { 40 item.setAttribute('data-key', `${key}-${index}`); 41 intersectionObserver.observe(item); 42 }); 43 44 return () => { 45 intersectionObserver.disconnect(); 46 }; 47 }, []); 48 49 return { 50 ref, 51 activeKey, 52 activeElement, 53 }; 54}
Usage
tsx1function Component() { 2 const { ref, activeKey, activeElement } = useVisibleElement({ 3 activeClass: 'active', 4 }); 5 console.log('activeKey', activeKey); // scroll-item-0 6 console.log('activeElement', activeElement); // <section><p>section 1</p></section> 7 8 return ( 9 <div ref={ref}> 10 <section> 11 <p>section 1</p> 12 </section> 13 <section> 14 <p>section 2</p> 15 </section> 16 <section> 17 <p>section 3</p> 18 </section> 19 <section> 20 <p>section 4</p> 21 </section> 22 </div> 23 ); 24}