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.
a Intersection observer with debounced callback
/* 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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment