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

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

ページのスクロールを管理するカスタムフックをつくる 🖱

最近、Next.jsをつかってページスクロールを管理する実験を行ってます。

blog.kimizuka.org

で、色々試していく中で、この仕組みをカスタムフックにしておくと、使いまわしやすいのではないかと思いまして、挑戦してみました。

ja.reactjs.org

useScroll.tsx

import { useEffect, useState } from 'react';
import Animation from './Animation';

const useScroll = (id: string = '__next'): [
  number,
  (targetProgress: number) => void
] => {
  const [ direction, setDirection ] = useState('');
  const [ progress, setProgress ] = useState(0);
  const [ lastProgress, setLastProgress ] = useState(0);
  const [ scrollProgress, setScrollProgress ] = useState(null);
  const [ contentsHeight, setContentsHeight ] = useState(0);
  const [ windowHeight, setWindowHeight ] = useState(0);
  const [ scrollY, setScrollY ] = useState(0);
  const [ animation, setAnimation ] = useState(new Animation({
    startValue: 0,
    targetValue: 0,
    duration: 1
  }));

  useEffect(() => {
    if (!direction) {
      init();

      return;
    }
  }, [direction]);

  useEffect(() => {
    const diff = lastProgress - progress;

    if (diff < 0) {
      setDirection('down');
    } else {
      setDirection('up');
    }

    setLastProgress(progress);
  }, [progress]);

  useEffect(() => {
    if (typeof scrollProgress !== 'number') {
      return;
    }

    window.scrollTo(window.scrollX, (contentsHeight - windowHeight) * scrollProgress);
    setScrollProgress(null);
  }, [scrollProgress]);

  useEffect(() => {
    if (contentsHeight - windowHeight) {
      setProgress(scrollY / (contentsHeight - windowHeight));
    }
  }, [scrollY]);

  useEffect(() => {
    window.addEventListener('mousedown', cancelScroll, {
      passive: false
    });
    window.addEventListener('wheel', cancelScroll, {
      passive: false
    });
    window.addEventListener('touchstart', cancelScroll, {
      passive: false
    });

    return () => {
      animation.stop();
      window.removeEventListener('mousedown', cancelScroll);
      window.removeEventListener('wheel', cancelScroll);
      window.removeEventListener('touchstart', cancelScroll);
    }

    function cancelScroll() {
      animation.stop();
    }
  }, [animation]);

  function init() {
    window.addEventListener('resize', handleResize, {
      passive: true
    });

    document.addEventListener('scroll', handleScroll, {
      passive: true
    });

    handleResize();
    setScrollProgress(normalize(0));
  }

  function handleResize() {
    setContentsHeight(document.getElementById(id).clientHeight);
    setWindowHeight(window.innerHeight);
  }

  function handleScroll() {
    setScrollY(window.scrollY);
  }

  function normalize(val: number): number {
    return Math.max(0, Math.min(val, 1));
  }

  function setProgressWithAnimation(targetProgress: number) {
    const startProgress = progress;
    const diff = targetProgress - startProgress;

    setAnimation(
      new Animation({
        startValue: 0,
        targetValue: targetProgress,
        duration: 800,
        easing: 'easeInOut',
        step: (val: number) => {
          val = startProgress + diff * val;

          setScrollProgress(normalize(val));
        }
      })
    );
  }

  return [ progress, setProgressWithAnimation ];
};

export default useScroll;

ざっくり書くと、こんな感じです。
別途読み込んでいる Animation.tsこちらです。

blog.kimizuka.org

DEMO

こんな感じでつかいます。

https://kimizuka.github.io/next-scroll/scroll-hook/

const [ progress, setProgressWithAnimation ] = useScroll();

探り探り実装してみたものの、引数に初期値を渡さない設計(ドキュメントルートのIDを渡す)になっているのが気がかりです。良いのかな。
でも、少なくとも自分で使う分には便利に使えているのでオーケーとしましょう。


追記

シンプルに改良しました。
blog.kimizuka.org


更に追記

useHookesたるライブラリを教えていただきました。自作せずともuseWindowScrollで充分ですね。

usehooks.com
usehooks.com