みかづきブログ・カスタム

基本的にはちょちょいのほいです。

ページのスクロールとリサイズを管理するシンプルなカスタムフックをつくってページのスクロール量を割合で管理する 🖱

f:id:kimizuka:20211229220518g:plain

以前シンプルに作り直したカスタムフック をほんのり複雑に作り直しました。
具体的には以前のものはウィンドウのスクロール量を測定することに特化していたのですが、引数で測定対象を渡せるように修正した次第です。

blog.kimizuka.org

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>
  );
}

こんな感じで使います。
現状は縦方向のスクロール量しか計算していません。