スムーススクロール、今までは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
が変更されるたびに(つまり、smooth
やlocation
が変更されるたびに)実行されます。これにより、最新のスクロール設定が反映されるようになります。
まとめ:全体の流れ
ここまで長かったですが、最後に処理の流れまとめ。
①URLの変更:
ページのURLが変わると、useLocation
が新しいlocation
を取得して変数に格納する。
※「URLの変更」と書きましたが最初にページが読み込まれた時もこの関数はURLを取得して実行されています。
②scrollToAnchor
の更新:
新しいlocation
によりscrollToAnchor
関数が更新、useCallback
によりlocation
やsmooth
の変更が検知されscrollToAnchor
が実行されます。
③useEffect
の再実行:
scrollToAnchor
が実行(変更)されると、依存配列[scrollToAnchor]
がトリガーとなりuseEffect
が再実行される。useEffect
内でscrollToAnchor
が呼び出され、ページが適切なアンカーにスクロールされます。
…という流れですね。書いててややっこしいですね。
ページが切り替わるたびにuseLocation
が新しいURL情報を取得し、それに基づいてscrollToAnchor
が実行される。
これにより正しいアンカー位置にスクロールするようになります。
長い解説しでしたが、実際に文章にするのは中々難しいですね。