Skip to content

Instantly share code, notes, and snippets.

@minhoyooDEV
Last active June 10, 2024 04:31
Show Gist options
  • Select an option

  • Save minhoyooDEV/35fa34a16b5ddfc1ba55728b645c478c to your computer and use it in GitHub Desktop.

Select an option

Save minhoyooDEV/35fa34a16b5ddfc1ba55728b645c478c to your computer and use it in GitHub Desktop.

Revisions

  1. minhoyooDEV revised this gist Jun 10, 2024. 1 changed file with 51 additions and 43 deletions.
    94 changes: 51 additions & 43 deletions useIntersectionObserver.tsx
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,4 @@
    /* eslint-disable @typescript-eslint/no-explicit-any */
    import { useCallback, useEffect, useRef } from 'react';
    import { useCallback, useEffect, useRef, useState } from 'react';

    // debounce 함수는 주어진 함수가 특정 시간 동안 호출되지 않도록 합니다.
    // The debounce function ensures that the provided function is not called repeatedly within the specified wait time.
    @@ -20,8 +19,15 @@ function debounce<T extends (...args: any[]) => void>(
    };
    }

    // IntersectionObserver의 상태를 나타내는 열거형(enum)입니다.
    // Enum representing the status of the Intersection Observer.
    enum IntersectionObserverStatus {
    IDLE,
    ACTIVE,
    }

    // useIntersectionObserver 훅은 Intersection Observer를 사용하여 요소의 가시성을 감지합니다.
    // The useIntersectionObserverV2 hook uses Intersection Observer to detect the visibility of elements.
    // The useIntersectionObserver hook uses Intersection Observer to detect the visibility of elements.
    export function useIntersectionObserver<T extends HTMLElement>(props: {
    callback: (entry: IntersectionObserverEntry) => void; // 요소가 보일 때 실행되는 콜백 함수 / Callback function executed when the element is visible
    options?: IntersectionObserverInit & {
    @@ -36,12 +42,13 @@ export function useIntersectionObserver<T extends HTMLElement>(props: {
    options: {
    $htmlSelector,
    $once,
    $initDelay = 100, // 기본 초기 지연 시간을 100ms로 설정 / Default initial delay time set to 100ms
    $callbackDebounce = 100, // 기본 콜백 디바운스 시간을 100ms로 설정 / Default callback debounce time set to 100ms
    $initDelay = 1600, // 기본 초기 지연 시간을 1600ms로 설정 / Default initial delay time set to 1600ms
    $callbackDebounce = 1200, // 기본 콜백 디바운스 시간을 1200ms로 설정 / Default callback debounce time set to 1200ms
    ...options
    } = {},
    } = props;
    const onceStore = useRef(new Map<HTMLElement, boolean>()); // 한 번만 감지된 요소를 저장 / Store elements detected only once
    const [status, setStatus] = useState(IntersectionObserverStatus.IDLE); // Intersection Observer의 현재 상태를 저장 / Stores the current status of the Intersection Observer
    const onceStore = useRef(new Map<HTMLElement, boolean>()); // 한 번만 감지된 요소를 저장 / Stores elements detected only once
    const target = useRef<T | null>(null); // 관찰할 타겟 요소 / Target element to be observed
    const observer = useRef<IntersectionObserver | null>(null); // Intersection Observer 인스턴스 / Intersection Observer instance
    const visibleElements = useRef(new Set()); // 현재 보이는 요소들 / Currently visible elements
    @@ -69,7 +76,7 @@ export function useIntersectionObserver<T extends HTMLElement>(props: {
    observer.current = null;
    target.current = null;
    }
    }, []);
    }, [observer.current]);

    // 관찰 토글 함수: 현재 타겟 요소의 관찰을 토글합니다.
    // Toggle observe function: toggles observation of the current target element.
    @@ -81,55 +88,56 @@ export function useIntersectionObserver<T extends HTMLElement>(props: {
    }
    };

    // 초기 지연 후 상태를 ACTIVE로 설정
    // Set the status to ACTIVE after the initial delay
    useEffect(() => {
    setTimeout(() => {
    setStatus(IntersectionObserverStatus.ACTIVE);
    }, $initDelay);
    }, []);

    // Intersection Observer 설정
    // Setting up the Intersection Observer
    useEffect(() => {
    if (status === IntersectionObserverStatus.IDLE) {
    return;
    }

    observer.current = new IntersectionObserver(
    (entries) => {
    entries.forEach((entry) => {
    if (onceStore.current.get(entry.target as HTMLElement)) {
    return;
    }
    const target = entry.target as HTMLElement;

    if (entry.isIntersecting) {
    visibleElements.current.add(entry.target);
    if ($once) {
    onceStore.current.set(entry.target as HTMLElement, true);
    entry.isIntersecting
    ? visibleElements.current.add(target)
    : visibleElements.current.delete(target);

    if (visibleElements.current.has(target)) {
    if (onceStore.current.get(target)) {
    return;
    }
    } else {
    visibleElements.current.delete(entry.target);

    debouncedEntryFuncs.current.get(target)?.(entry);
    }
    debouncedEntryFuncs.current.get(entry.target)?.(entry);
    });
    },
    { threshold: 0.7, ...options },
    );

    setTimeout(() => {
    if ($htmlSelector) {
    const elements = document.querySelectorAll($htmlSelector);
    elements.forEach((element) => {
    subscribe(element as T);
    const entryCalled = debounce((entry: IntersectionObserverEntry) => {
    if (visibleElements.current.has(entry.target)) {
    callbackProps(entry);
    if (process.env.NODE_ENV === 'development') {
    console.log('[DEV] intersection ', entry);
    }
    if ($htmlSelector) {
    const elements = document.querySelectorAll($htmlSelector);
    elements.forEach((element) => {
    subscribe(element as T);
    const entryCalled = debounce((entry: IntersectionObserverEntry) => {
    if (visibleElements.current.has(entry.target)) {
    if ($once) {
    onceStore.current.set(entry.target as HTMLElement, true);
    }
    }, $callbackDebounce);
    debouncedEntryFuncs.current.set(element, entryCalled);
    });
    } else {
    subscribe(target.current);
    }
    }, $initDelay);

    return () => {
    observer.current?.disconnect();
    observer.current = null;
    };
    }, [callbackProps, options]);
    callbackProps(entry);
    }
    }, $callbackDebounce);

    return [subscribe, unsubscribe, toggleObserve] as const;
    }
    debouncedEntryFuncs.current.set(element, entryCalled);
    });
    } else {
    subscribe(target.current);
  2. minhoyooDEV created this gist Jun 9, 2024.
    135 changes: 135 additions & 0 deletions useIntersectionObserver.tsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,135 @@
    /* eslint-disable @typescript-eslint/no-explicit-any */
    import { useCallback, useEffect, useRef } from 'react';

    // debounce 함수는 주어진 함수가 특정 시간 동안 호출되지 않도록 합니다.
    // The debounce function ensures that the provided function is not called repeatedly within the specified wait time.
    function debounce<T extends (...args: any[]) => void>(
    func: T,
    wait: number,
    ): (...args: Parameters<T>) => void {
    let timeout: ReturnType<typeof setTimeout> | null = null;

    return (...args: Parameters<T>): void => {
    if (timeout !== null) {
    clearTimeout(timeout);
    }

    timeout = setTimeout(() => {
    func(...args);
    }, wait);
    };
    }

    // useIntersectionObserver 훅은 Intersection Observer를 사용하여 요소의 가시성을 감지합니다.
    // The useIntersectionObserverV2 hook uses Intersection Observer to detect the visibility of elements.
    export function useIntersectionObserver<T extends HTMLElement>(props: {
    callback: (entry: IntersectionObserverEntry) => void; // 요소가 보일 때 실행되는 콜백 함수 / Callback function executed when the element is visible
    options?: IntersectionObserverInit & {
    $once?: boolean; // 요소가 한번만 감지되도록 설정 / Option to detect the element only once
    $htmlSelector?: string; // 감지할 요소의 선택자 / Selector for the elements to be observed
    $initDelay?: number; // 초기 지연 시간 / Initial delay time
    $callbackDebounce?: number; // 콜백 디바운스 시간 / Callback debounce time
    };
    }) {
    const {
    callback: callbackProps,
    options: {
    $htmlSelector,
    $once,
    $initDelay = 100, // 기본 초기 지연 시간을 100ms로 설정 / Default initial delay time set to 100ms
    $callbackDebounce = 100, // 기본 콜백 디바운스 시간을 100ms로 설정 / Default callback debounce time set to 100ms
    ...options
    } = {},
    } = props;
    const onceStore = useRef(new Map<HTMLElement, boolean>()); // 한 번만 감지된 요소를 저장 / Store elements detected only once
    const target = useRef<T | null>(null); // 관찰할 타겟 요소 / Target element to be observed
    const observer = useRef<IntersectionObserver | null>(null); // Intersection Observer 인스턴스 / Intersection Observer instance
    const visibleElements = useRef(new Set()); // 현재 보이는 요소들 / Currently visible elements
    const debouncedEntryFuncs = useRef(
    new Map<Element, (entry: IntersectionObserverEntry) => void>(), // 디바운스된 콜백 함수들 / Debounced callback functions
    );

    // 구독 함수: 노드를 관찰합니다.
    // Subscribe function: observes the node.
    const subscribe = useCallback(
    (node: T | null) => {
    if (node) {
    observer.current?.observe(node);
    target.current = node;
    }
    },
    [observer.current],
    );

    // 구독 해제 함수: 모든 관찰을 중지합니다.
    // Unsubscribe function: stops all observations.
    const unsubscribe = useCallback(() => {
    if (observer.current) {
    observer.current.disconnect();
    observer.current = null;
    target.current = null;
    }
    }, []);

    // 관찰 토글 함수: 현재 타겟 요소의 관찰을 토글합니다.
    // Toggle observe function: toggles observation of the current target element.
    const toggleObserve = () => {
    if (target.current) {
    observer.current?.unobserve(target.current);
    } else {
    observer.current?.observe(target.current as unknown as HTMLElement);
    }
    };

    // Intersection Observer 설정
    // Setting up the Intersection Observer
    useEffect(() => {
    observer.current = new IntersectionObserver(
    (entries) => {
    entries.forEach((entry) => {
    if (onceStore.current.get(entry.target as HTMLElement)) {
    return;
    }

    if (entry.isIntersecting) {
    visibleElements.current.add(entry.target);
    if ($once) {
    onceStore.current.set(entry.target as HTMLElement, true);
    }
    } else {
    visibleElements.current.delete(entry.target);
    }
    debouncedEntryFuncs.current.get(entry.target)?.(entry);
    });
    },
    { threshold: 0.7, ...options },
    );

    setTimeout(() => {
    if ($htmlSelector) {
    const elements = document.querySelectorAll($htmlSelector);
    elements.forEach((element) => {
    subscribe(element as T);
    const entryCalled = debounce((entry: IntersectionObserverEntry) => {
    if (visibleElements.current.has(entry.target)) {
    callbackProps(entry);
    if (process.env.NODE_ENV === 'development') {
    console.log('[DEV] intersection ', entry);
    }
    }
    }, $callbackDebounce);
    debouncedEntryFuncs.current.set(element, entryCalled);
    });
    } else {
    subscribe(target.current);
    }
    }, $initDelay);

    return () => {
    observer.current?.disconnect();
    observer.current = null;
    };
    }, [callbackProps, options]);

    return [subscribe, unsubscribe, toggleObserve] as const;
    }