以前シンプルに作り直したカスタムフック をほんのり複雑に作り直しました。
具体的には以前のものはウィンドウのスクロール量を測定することに特化していたのですが、引数で測定対象を渡せるように修正した次第です。
useScroll.tsx
import { useEffect, useState } from 'react'; export default function useScroll(parentElement: Window | HTMLElement | null) { const [ target, setTarget] = useState<Window | HTMLElement | null>(null); const [ scrollTop, setScrollTop ] = useState(0); const [ scrollLeft, setScrollLeft] = useState(0); useEffect(() => { if (parentElement) { setTarget(parentElement); } }, [parentElement]); useEffect(() => { if (target) { target.removeEventListener('scroll', handleScroll); target.addEventListener('scroll', handleScroll, { passive: true }); handleScroll(); } return () => { if (target) { target.removeEventListener('scroll', handleScroll); } } }, [target]); function handleScroll() { setScrollTop((target as HTMLElement).scrollTop || (target as Window).scrollY || 0); setScrollLeft((target as HTMLElement).scrollLeft || (target as Window).scrollX || 0); } return { scrollLeft, scrollTop }; }
引数でスクロール量を測定するDOMを受け取るようになったため、以前のものより若干複雑になりましたが、その分便利になりました。
HTMLElementかWindowが渡ってくるので、
(target as HTMLElement).scrollTop || (target as Window).scrollY || 0
と書いているのですが、もっと良い書き方がある気がしてなりません。
useResize.tsx
import { useEffect, useState } from 'react'; export default function useResize() { const [ windowWidth, setWindowWidth ] = useState(0); const [ windowHeight, setWindowHeight ] = useState(0); useEffect(() => { window.removeEventListener('resize', handleResize); window.addEventListener('resize', handleResize, { passive: true }); handleResize(); return () => { window.removeEventListener('resize', handleResize); } }, []); function handleResize() { setWindowWidth(window.innerWidth); setWindowHeight(window.innerHeight); } return { windowWidth, windowHeight }; }
以前のものと、全くもって一緒です。
useScrollProgress
import { useEffect, useState } from 'react'; import useResize from './useResize'; import useScroll from './useScroll'; export default function useScrollProgress(parentElement: Window | HTMLElement | null, scrollElement: HTMLElement | null) { const [ scrollProgress, setClosureScrollProgress ] = useState(0); const [ clientHeight, setClientHeight ] = useState(0); const { scrollTop } = useScroll(parentElement); const { windowHeight } = useResize(); useEffect(() => { if (scrollElement && scrollElement.clientHeight) { setClientHeight(scrollElement.clientHeight); } }, [scrollElement]) useEffect(() => { if (scrollElement) { setClientHeight(scrollElement.clientHeight); setClosureScrollProgress(scrollTop / (clientHeight - windowHeight)); } }, [scrollTop, windowHeight]); function setScrollProgress(scrollProgress: number) { if (parentElement && parentElement.scrollTo) { parentElement.scrollTo(0, scrollProgress * (clientHeight - windowHeight)); } } return { setScrollProgress, scrollProgress }; }
新規に作ったカスタムフックです。
親と子を渡すと、スクロールの割合を計算します。
引数は親だけにできる気もしているのですが、一旦、親と子を渡す設計で作りました。
scrollProgressを使うと、割合を渡した際に、子をその部分までスクロールさせることもできます。
使い方
import useResize from '../hooks/useScrollProgress'; import { useEffect } from 'react'; export default function IndexPage() { const [ parentElement, setParentElement ] = useState<Window>(null); const [ scrollElement, setScrollElement ] = useState<HTMLElement>(null); const { setScrollProgress, scrollProgress } = useScrollProgress(parentElement, scrollElement); useEffect(() => { setParentElement(window); setScrollElement(document.getElementById('__next')); }, []); return ( <div> <p>progress: { scrollProgress * 100 }%</p> <button onClick={ () => setScrollProgress(.5) }>50%</button> </div> ); }
こんな感じで使います。
現状は縦方向のスクロール量しか計算していません。