こんにちは!sawaです!
12/17のHapInSアドカレ記事です!よろしくお願いします!
今回は、Next.jsでスクロールポジションを動的に計測する関数を作成したのでご紹介します!
この関数は、「スクロールして特定のDOM要素を何割表示したか」を動的に求められるようになっています。
一般的なケースとしては、表示しているページ全体を対象としてスクロール位置を計算することなどが多いですが、本記事の方法では特定のDOM要素を対象としているため、より柔軟に活用できると思います。
また、ウィンドウサイズの変更や、対象となるDOM要素のサイズが変わった時にもリアルタイムで対応できます。
記事の後半では、この機能を関数化しているので、より簡単に活用していただけると思います!
基準にする対象による違い
先ほど少し触れましたが、具体的に何が違うのか簡単に説明します。
わかりやすいように図にしてみました。
ブラウザで見えている部分とまだ表示されていない部分を図解にすると下記のとおりです。これをベースに説明しますね。
ウィンドウの高さを基準に計算した時
シンプルな例を見てみましょう。
例えば、ウィンドウの高さを基準にして、全体の8割までスクロールした位置をトリガーポイントとする場合を考えます。 この図のように、要素の下端までスクロールした時点で発火させたい場合、要素のサイズや全体の高さが変化すると、要素を完全にスクロールし終わってから発火してしまいます。 また、要素のサイズやウィンドウの大きさが変更された場合にも適切に対応できません。
特定の要素を基準に計算した時
前述のケースに対して、特定の要素の8割を表示した時をトリガーとして設定した場合は下記の図のようになります。
要素の大きさなどが変更されても、スクロールが完了する前のタイミングで発火させることができます。
本記事はこちらのケースですね!
このような違いがあるので、使用するシーンに応じて使い分けていきたいですね!
活用シーン
活用シーンとしては、下記のような場面が考えられます。
- 無限スクロール
- 遅延読み込み
- アニメーションなどのトリガー
- スクロール位置をプログレスバーに反映
計算式
スクロールして特定のDOM要素を何割表示したかを、下記の計算式で求めます。
💡 (ウィンドウの高さ - ウィンドウの上端から要素の上端までの距離) / 対象のDOM要素の高さ
「ブラウザの高さ」「ウィンドウの上端から要素の上端までの距離」「対象のDOM要素の高さ」をそれぞれ求めていけば大丈夫ですね。
それでは具体的に数値を取得していきましょう!
関数を作成
ブラウザの高さを求める
const getWindowHeight = () => { return window.innerHeight; }; const subscribeHeightChange = (callback: () => void) => { window.addEventListener("resize", callback); return () => window.removeEventListener("resize", callback); }; const useWindowHeight = (): number => { return useSyncExternalStore(subscribeHeightChange, getWindowHeight); }; const windowHeight = useWindowHeight();
こちらに関しては下記の記事から高さに関する部分を引用させていただきました! react18以降で追加されたuseSyncExternalStoreを使っています。
ウィンドウの上端から要素の上端までの距離を求める
const [scrollPosition, setScrollPosition] = useState(0); useEffect(() => { const updatePosition = () => { if (ref.current) { const rect = ref.current.getBoundingClientRect(); const top = rect.top; setScrollPosition(top); } }; updatePosition(); window.addEventListener("scroll", updatePosition); window.addEventListener("resize", updatePosition); return () => { window.removeEventListener("scroll", updatePosition); window.removeEventListener("resize", updatePosition); }; }, []);
簡単に説明していきます。
const updatePosition = () => { if (ref.current) { const rect = ref.current.getBoundingClientRect(); const top = rect.top; setScrollPosition(top); } };
このupdatePosition
関数で要素の位置を求めます。rect.top
で要素の上端までの距離を求めて、scrollPosition
に格納します。
初期レンダリング時に updatePosition
を実行し、要素の位置をscrollPosition
に格納します。
また、スクロールとウィンドウサイズ変更時に updatePosition
を呼び出すリスナーを追加します。これでサイズに変更があったときも対応することができます。
特定のDOM要素の高さを求める
const [refHeight, setRefHeight] = useState(0); useEffect(() => { const updatePosition = () => { if (ref.current) { const rect = ref.current.getBoundingClientRect(); const height = rect.height; setRefHeight(height); } }; updatePosition(); window.addEventListener("scroll", updatePosition); window.addEventListener("resize", updatePosition); return () => { window.removeEventListener("scroll", updatePosition); window.removeEventListener("resize", updatePosition); }; }, [increaseHeight]); //要素の高さが変わるのであれば設定
こちらは前述した「ウィンドウの上端から要素の上端までの距離を求める」とほぼ同じです。
const height = rect.height;
上記の箇所で要素の高さを取得しています。
計算する
const rate = ((windowHeight - scrollPosition) / refHeight) * 100;
%表記にしたいので、* 100
としています。
対象の要素の上端を表示していない時(=要素をまだ表示していない時)はマイナス表記、対象の要素の下端までスクロールしきった時に100%を超えます。
使いどころによってはこちらを条件に含めてください。
一旦ここまでで割合が求められるようになりました!お疲れさまです!
関数化
このままではコードが煩雑なので、整理しましょう!
import { MutableRefObject, useEffect, useState, useSyncExternalStore, } from "react"; const useScrollMetrics = (ref: MutableRefObject<HTMLElement | null>) => { // ウィンドウの高さ const getWindowHeight = () => window.innerHeight; const subscribeHeightChange = (callback: { (this: Window, ev: UIEvent): any; (this: Window, ev: UIEvent): any; }) => { window.addEventListener("resize", callback); return () => window.removeEventListener("resize", callback); }; const useWindowHeight = () => { return useSyncExternalStore(subscribeHeightChange, getWindowHeight); }; const windowHeight = useWindowHeight(); // スクロール位置と要素の高さ const [scrollPosition, setScrollPosition] = useState(0); const [refHeight, setRefHeight] = useState(0); useEffect(() => { const updateMetrics = () => { if (ref.current) { const { top, height } = ref.current.getBoundingClientRect(); setScrollPosition(top); setRefHeight(height); } }; updateMetrics(); window.addEventListener("scroll", updateMetrics); window.addEventListener("resize", updateMetrics); return () => { window.removeEventListener("scroll", updateMetrics); window.removeEventListener("resize", updateMetrics); }; }, []); //割合(%表記) const rate = ((windowHeight - scrollPosition) / refHeight) * 100; return { windowHeight, scrollPosition, refHeight, rate }; }; export default useScrollMetrics;
はい!これでスッキリしましたね!
下記のように使いたいところでuseScrollMetrics
をimportしてください!
const { windowHeight, scrollPosition, refHeight, rate } = useScrollMetrics(ref);
デモ
こちらの関数を実際に使ってみたのが下記のデモです! (埋め込みの下に操作方法を少し書いています…!)
https://codesandbox.io/p/devbox/sukurorupozisiyonnoce-ding-gz22xh?embed=1&file=%2Fapp%2FuseScrollMetrics.ts
↑見にくい方はリンク先をクリックしてください。別タブが開きます。
「Preview:3000」をクリックして、
右上のアイコンを押せばプレビューが開きます。
- pages.ts
- useScrollMetrics
- style.css
が今回手を加えたファイルです
プレビュー画面右上にそれぞれの数値が反映されるようになっています!スクロールしたりウィンドウサイズを変えて確かめてみてください!
また、画面下部のボタンで要素の大きさを変更できるようにしています。こちらも試してみてね。
おわりに
アドカレにしては真面目な記事になってしまいました……が、アウトプットできて嬉しいです!
読んでくださった方、この場を用意してくださった方に感謝です!
この記事がお役に立てれば嬉しいです! みなさま楽しいクリスマスをお過ごしください!それでは!
ここまで読んでいただいたお礼にうちの犬を見せますね!
可愛いいいいいいいいいいい♡♡♡♡♡♡♡♡♡ メリークリスマス!!!!!!!!!