useVisibleElement

2023-10-18

2

useVisibleElement

스크롤 시 해당 DOM 요소가 노출됬는지 여부 감지

Hooks(Legacy)

tsx
1import { 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

  1. activeClass: 해당 요소가 노출될 때 추가할 클래스명
  2. key: 추적 요소의 고유 키값 부여

Return

  1. ref: 추적할 요소를 가진 container(div tag)를 ref로 지정하기 위함
  2. activeKey: 현재 노출된 요소의 키값
  3. activeElement: 현재 노출된 Element 요소
tsx
1import { 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

tsx
1function 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}