Skip to content

Instantly share code, notes, and snippets.

@minhoyooDEV
Last active June 10, 2024 04:31
Show Gist options
  • Save minhoyooDEV/35fa34a16b5ddfc1ba55728b645c478c to your computer and use it in GitHub Desktop.
Save minhoyooDEV/35fa34a16b5ddfc1ba55728b645c478c to your computer and use it in GitHub Desktop.
a Intersection observer with debounced callback
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.
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);
};
}
// IntersectionObserver의 상태를 나타내는 열거형(enum)입니다.
// Enum representing the status of the Intersection Observer.
enum IntersectionObserverStatus {
IDLE,
ACTIVE,
}
// useIntersectionObserver 훅은 Intersection Observer를 사용하여 요소의 가시성을 감지합니다.
// 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 & {
$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 = 1600, // 기본 초기 지연 시간을 1600ms로 설정 / Default initial delay time set to 1600ms
$callbackDebounce = 1200, // 기본 콜백 디바운스 시간을 1200ms로 설정 / Default callback debounce time set to 1200ms
...options
} = {},
} = props;
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
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;
}
}, [observer.current]);
// 관찰 토글 함수: 현재 타겟 요소의 관찰을 토글합니다.
// 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);
}
};
// 초기 지연 후 상태를 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) => {
const target = entry.target as HTMLElement;
entry.isIntersecting
? visibleElements.current.add(target)
: visibleElements.current.delete(target);
if (visibleElements.current.has(target)) {
if (onceStore.current.get(target)) {
return;
}
debouncedEntryFuncs.current.get(target)?.(entry);
}
});
},
{ threshold: 0.7, ...options },
);
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);
}
callbackProps(entry);
}
}, $callbackDebounce);
debouncedEntryFuncs.current.set(element, entryCalled);
});
} else {
subscribe(target.current);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment