スムーススクロールの実装

2024.07.12

スムーススクロール、今まではjQueryを使って実装してきました。

Reactでもスムーススクロールは簡単に実装できるんじゃないかと思っていましたが、結構ハマってしまいました。。。

これがベストなやり方かわからないですが、メモとして残します。
スムーススクロールのパッケージは色々あるようでしたが今回は調べつつ自分で作ってみました。

実装した機能

実装した機能は三つです。

アンカーの箇所までスクロールさせる。

スムーススクロールなので当たり前ですがハッシュ値のあるリンクをクリックした際、idのある要素までスクロールします。

アンカーの無いリンクの時はページの一番最初を表示させる。

SPAはページが切り替わってもスクロール位置が変わらないため、ページの一番最初を表示するように実装。

API取得がある際は要素表示が完了してから、指定の位置にスクロールさせる

これが今回一番大変でした。。。

JavaScriptのメソッド「scrollIntoView」を使用してターゲットの要素にスクロールするようににしたのですが、APIが読み込まれる前に動いてしまいターゲットの位置よりも前でスクロールが止まってしまっていました。

API読み込みの無いページでは正常に動くので、最初は原因がわからず頭を抱えました…

この原因は「scrollIntoView」は要素までの距離を自動的に計算してくれるのですが、APIで読み込み表示する前にスクロール先までの距離を計算してしまい指定位置が合わなかった…ということです。

今回はトップページだけ記事一覧を表示する際にAPIから取得表示しており、スムーススクロールの位置が合っていませんでした。

ここら辺がSPA以外の、jQueryとWordpressで作っていただけでは出会わないエラーですね…。
wordpressはサーバー側でhtmlを作って、その後でブラウザ上でjQueryが動くので

急ぎの対応として今回はスクロールするタイミングを「setTimeOut」で遅らせて、全てのページが表示されてからスクロールするようにしました。
電波状況の悪いところだと、上手くスクロールしてくれない可能性もありますが取り急ぎで…

実装したコード

何はともあれまずは実装したコードから。

スクロール機能はカスタムフックとして作りました。ファイル名は「useScrollToAnchor.tsx」としています。

import { useCallback } from 'react';
import { useLocation } from 'react-router-dom';

type ScrollToAnchorProps = {
  smooth?: boolean;
};


export const useScrollToAnchor = ({ smooth,}: ScrollToAnchorProps): { scrollToAnchor: () => void } => {
  const location = useLocation();


  const scrollToAnchor = useCallback<() => void>(() => {
    const hash = location.hash.slice(1);
    const target = document.getElementById(hash);

    if (target) {
      //ページ内にハッシュ値がある場合の処理
      const timerId = setTimeout(() => {
        target.scrollIntoView({ behavior: smooth ? 'smooth' : 'auto', block: 'start' });
      }, 500);

      // クリーンアップ関数でタイマーをクリア
      return () => clearTimeout(timerId);
    } else {
      //ページ内にハッシュ値がない場合。ページのトップに移動
      window.scrollTo({ top: 0, behavior: 'auto' });
    }
  }, [location, smooth]);

  return { scrollToAnchor };//関数を返して、呼び出し先で実行できるようにしている。

};

このカスタムフックを別コンポーネントで呼び出します。
今回は全てのページでスムーススクロールを動くようにするので、グローバルナビゲーションのコンポーネントで呼び出しにしました。

import { useEffect, useState } from 'react';
import { NavLink, Outlet } from 'react-router-dom';

import { useScrollToAnchor } from "../../hooks/useScrollToAnchor";



export default function RouterNav() {
  const { scrollToAnchor } = useScrollToAnchor({ smooth: true });

  useEffect(() => {
    scrollToAnchor();
  }, [scrollToAnchor]);

  return (
    <>
    //ナビゲーションを表示する要素が入ります
    </>
  );
}

コードの説明

カスタムフックの説明からです。

type ScrollToAnchorProps = {
  smooth?: boolean;
};

この部分では、フックに渡されるプロパティの型を定義しています。
smoothはオプションのブール値で、スクロールがスムーズに行われるかどうかを指定するのに使います。

export const useScrollToAnchor = ({ smooth, }: ScrollToAnchorProps): { scrollToAnchor: () => void } => {

ここでは「scrollToAnchor」という関数を返り値としています。呼び出し先のコンポーネントで関数を呼び出すためです。

続いて実際の処理部分

  const location = useLocation();


  const scrollToAnchor = useCallback<() => void>(() => {
    const hash = location.hash.slice(1);
    const target = document.getElementById(hash);

    if (target) {
      //ページ内にハッシュ値がある場合の処理
      const timerId = setTimeout(() => {
        target.scrollIntoView({ behavior: smooth ? 'smooth' : 'auto', block: 'start' });
      }, 500);

      // クリーンアップ関数でタイマーをクリア
      return () => clearTimeout(timerId);
    } else {
      //ページ内にハッシュ値がない場合。ページのトップに移動
      window.scrollTo({ top: 0, behavior: 'auto' });
    }
  }, [location, smooth]);

  return { scrollToAnchor };//関数を返して、呼び出し先で実行できるようにしている。

useLocationフックは現在のURLハッシュ値を取得するのに使い、取得したハッシュ値をスクロールするターゲット要素にしています。

ハッシュ値がない場合はページのトップを表示し、ハッシュ値がある場合はターゲットの要素までスクロールします。
APIの読み込み対策としてsetTimeoutを使って、API取得データが表示されてからスムーススクロールさせたいため、0.5秒スクロールを遅らせています。

また、ページが変わるたびに新しいタイマーが設定されないように、clearTimeoutでタイマーをクリアしています。

useCallbackと呼び出し元のuseEffect

useCallbackで指定している依存配列「location, smooth」。
locationはuselocationによりURLが変わるたびに変数の中身も変わります。これによりページが切り替わるたびに関数scrollToAnchorが実行されます。

また関数の呼び出し元でsmooth:trueを引数にしています。

export default function RouterNav() {
  const { scrollToAnchor } = useScrollToAnchor({ smooth: true });

  useEffect(() => {
    scrollToAnchor();
  }, [scrollToAnchor]);

smooth: trueを渡しています。これはuseScrollToAnchorフックにスムーズスクロールを使用を指示するために使っています。

呼び出し元のuseEffectは、scrollToAnchorが変更されるたびに(つまり、smoothlocationが変更されるたびに)実行されます。これにより、最新のスクロール設定が反映されるようになります。

まとめ:全体の流れ

ここまで長かったですが、最後に処理の流れまとめ。

URLの変更:

ページのURLが変わると、useLocationが新しいlocationを取得して変数に格納する。

※「URLの変更」と書きましたが最初にページが読み込まれた時もこの関数はURLを取得して実行されています。

scrollToAnchorの更新:

新しいlocationによりscrollToAnchor関数が更新、useCallbackによりlocationsmoothの変更が検知されscrollToAnchorが実行されます。

useEffectの再実行:

scrollToAnchorが実行(変更)されると、依存配列[scrollToAnchor]がトリガーとなりuseEffectが再実行される。useEffect内でscrollToAnchorが呼び出され、ページが適切なアンカーにスクロールされます。

…という流れですね。書いててややっこしいですね。

ページが切り替わるたびにuseLocationが新しいURL情報を取得し、それに基づいてscrollToAnchorが実行される。
これにより正しいアンカー位置にスクロールするようになります。

長い解説しでしたが、実際に文章にするのは中々難しいですね。

totop Page Top