Skip to content

Instantly share code, notes, and snippets.

@manabuyasuda
Last active January 30, 2025 09:37
Show Gist options
  • Save manabuyasuda/e9960e978e0048a38dab6bc0e2cdfc62 to your computer and use it in GitHub Desktop.
Save manabuyasuda/e9960e978e0048a38dab6bc0e2cdfc62 to your computer and use it in GitHub Desktop.
/**
* 特定のスクロール位置(要素)を通過したかどうかを監視するカスタムフックです。
* スクロール位置と要素の位置を比較して、要素を通過したかどうかを判定します。
* @param {string} selector 監視対象の要素を特定するCSSセレクタ
* @param {Object} options 監視オプション
* @param {string} options.scrollBasePosition スクロール量の基準位置を画面の上端('top')または下端('bottom')とするか
* @param {string} options.elementBasePosition 要素の基準位置を要素の上端('top')または下端('bottom')とするか
* @param {boolean} options.once 一度要素を通過したら監視を終了するかどうか
* @returns {boolean} true: スクロール量が要素の位置を超えた, false: まだ要素の位置まで到達していない
*/
export function useScrollPastElement(
selector: string,
options: {
scrollBasePosition?: 'top' | 'bottom';
elementBasePosition?: 'top' | 'bottom';
once?: boolean;
} = {
scrollBasePosition: 'top',
elementBasePosition: 'bottom',
once: false,
},
): boolean {
// 要素を通過したかどうか
const [hasPassed, setHasPassed] = useState<boolean>(false);
// 一度だけ監視する場合は通過後にtrueにして監視を終了する
const hasPassedOnce = useRef<boolean>(false);
// スクロールイベントの連続発火を制御するフラグ
const ticking = useRef<boolean>(false);
// 監視対象の要素を取得してメモ化する
const getTargetElement = useCallback((): HTMLElement | null => {
return document.querySelector(selector) as HTMLElement;
}, [selector]);
useEffect(() => {
const handleScroll = () => {
// 監視対象の要素が存在しない場合は処理をスキップする
const targetElement = getTargetElement();
if (!targetElement) {
return;
}
// 一度だけの判定オプションが有効で、すでに通過している場合は処理をスキップする
if (options.once && hasPassedOnce.current) {
return;
}
// 処理が実行されていない場合だけ実行する
if (!ticking.current) {
requestAnimationFrame(() => {
// スクロール基準位置を計算する(画面上端または下端)
const currentScrollY =
options.scrollBasePosition === 'bottom'
? window.scrollY + window.innerHeight
: window.scrollY;
// 要素の絶対位置を取得する
const rect = targetElement.getBoundingClientRect();
// 要素の絶対位置を計算する
// `getBoundingClientRect()`はビューポートを基準にした相対値のため、`window.scrollY`を足して絶対値に変換する
const elementPosition =
window.scrollY +
(options.elementBasePosition === 'bottom' ? rect.bottom : rect.top);
// スクロール位置が要素の位置を超えているかを判定する
const isPassed = currentScrollY >= elementPosition;
// 要素を通過した状態を更新する
setHasPassed(isPassed);
// 一度だけの判定オプションが有効で、要素を通過した場合は監視を終了
if (options.once && isPassed) {
hasPassedOnce.current = true;
window.removeEventListener('scroll', handleScroll);
}
// 処理を完了し、次のイベントを受付可能にする
ticking.current = false;
});
// 状態を処理中に更新して、これ以降のイベントをブロックする
ticking.current = true;
}
};
// Scroll Junkを防止してパフォーマンスを最適化する
window.addEventListener('scroll', handleScroll, { passive: true });
// 初期状態をチェックする
handleScroll();
return () => window.removeEventListener('scroll', handleScroll);
}, [options, getTargetElement]);
return hasPassed;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment