『note』の記事にあるような、動くナビゲーションバーを作成
このように、スクロールに応じて動く。
スクロールダウンすると姿を隠し、かつ、少しのスクロールアップでは再表示されず、それでいて一気にスクロールアップすると再表示してくれる、絶妙な塩梅。
気になるので作ってみました。
(React、Tailwind CSS)
目標
${showNav ? "top-16" : "top-[66px]"}
propsで渡したshowNav
をスイッチさせる。(2pxのボーダーボトム含めてるので、マジックナンバー(数値ベタ打ち)登場しています。)
該当するコード部分
const [showNav, setShowNav] = useState(true);
const lastScrollY = useRef(0); // useRefを使用してlastScrollYを初期化
useEffect(() => {
lastScrollY.current = window.scrollY; // コンポーネントがマウントされた後にwindow.scrollYを設定
const handleScroll = () => {
const currentScrollY = window.scrollY;
// スクロールの方向を判定
const isScrollingUp = currentScrollY < lastScrollY.current;
const isScrollingDown = currentScrollY > lastScrollY.current;
// トップの時はナビゲーションバーを表示
if (currentScrollY === 0) {
setShowNav(true);
}
// スクロールダウンでナビゲーションバーを非表示
else if (isScrollingDown) {
setShowNav(false);
}
// スクロールアップでかつ一定の距離を超えた場合にナビゲーションバーを表示
else if (isScrollingUp && lastScrollY.current - currentScrollY > 100) {
setShowNav(true);
}
lastScrollY.current = currentScrollY;
};
const throttledHandleScroll = throttle(handleScroll, 100);
window.addEventListener("scroll", throttledHandleScroll);
return () => {
window.removeEventListener("scroll", throttledHandleScroll);
throttledHandleScroll.cancel(); // クリーンアップ時にthrottleを解除
};
}, []);
コードの流れを確認
実装したいと思い立ち、早速Chat-GPT4(CV:津田健次郎)に相談。
「note.comの記事のようなナビゲーションバーが作りたいです」
「こんな感じでどうでしょうか」
示されたコードを見てみると、何やらcurrentScrollY
やlastScrollY
の変数が宣言されている。
それらを識別して、スクロールの差分を算出しようとしている模様。
「なるほど。やはりそうなりますよね。ちょうど自分もそうしようと考えていたところです(嘘)」
追加で質問しつつ、最初に出てきたコードは不要な変数宣言などもあったため、その都度修正しました。
hooksの使用
const [showNav, setShowNav] = useState(true);
const lastScrollY = useRef(0); // useRefを使用してlastScrollYを初期化
showNav
をuseStateで管理。
スクロールの値が欲しいので、useRef使用。
useEffectの中身
useEffect(() => {
lastScrollY.current = window.scrollY; // コンポーネントがマウントされた後にwindow.scrollYを設定
const handleScroll = () => {
const currentScrollY = window.scrollY;
// スクロールの方向を判定
const isScrollingUp = currentScrollY < lastScrollY.current;
const isScrollingDown = currentScrollY > lastScrollY.current;
最初にchatGPTに提示されたコードではwindow
が外で宣言されていたため、effect内に収めました。
isScrollingUp
とisScrollingDown
でスクロール方向を検知する。
(!isScrollingUp
でもいいのか? ん?どうなるんだっけ? ===
とかどうなる…?)
集合論に自信がなかったので、愚直にisScrollingDown
の宣言も追加。
条件分岐
// トップの時はナビゲーションバーを表示
if (currentScrollY === 0) {
setShowNav(true);
}
// スクロールダウンでナビゲーションバーを非表示
else if (isScrollingDown) {
setShowNav(false);
}
// スクロールアップでかつ一定の距離を超えた場合にナビゲーションバーを表示
else if (isScrollingUp && lastScrollY.current - currentScrollY > 100) {
setShowNav(true);
}
// lastScrollY.current を更新
lastScrollY.current = currentScrollY;
ナビゲーションバーの表示・非表示の条件を追加していく。
ページがボトムのときなども、レイアウトシフト対策などをしたほうがよさそうな場合もありそうですがどうなんでしょうか。
lastScrollY.current - currentScrollY > 100
が再表示の肝のようです。noteの実際の差分は分かりませんが、100にしています(お好みで数字を増やしたり減らしたりで感度が変わります。数値のバランスによっては痙攣するような表示具合になりました)
throttleでイベントの数を間引く
参考記事
高頻度のスクロールイベントをそのままにするのはアンチパターンなようなので、適宜throttoleかdebounceを入れて、イベントの数を減らす必要がある。
debounceはイベントを遅延、throttleは一定時間内のイベントを間引く。
throttleを採用。(老舗のモジュールって感じですが、最終アップデートがずっと前なのが少し気になります……現行でメンテナンスさているものってあるのでしょうか。自作? 詳しい方がいらしたら是非おすすめを教えてください。)
lodashをまるごと取り込むとバンドルサイズが大きくなるらしいので、throttleのみをインストール。
npm install lodash.throttle
TypeScriptの場合throttleの型が必要なので
npm i @types/lodash.throttle
handleScrollにthrottleを適用させる。
const throttledHandleScroll = throttle(handleScroll, 100);
window.addEventListener("scroll", throttledHandleScroll);
return () => {
window.removeEventListener("scroll", throttledHandleScroll);
throttledHandleScroll.cancel(); // クリーンアップ時にthrottleを解除
};
}, []);
chatGPTがthrottledHandleScroll.cancel()
の部分をやけに推すので、そのまま採用。
throttleが採用されているコードを調べみても、あったりなかったりでした。あったほうがいいんですかね。詳しい方がいたら是非教えてください。
propsを渡す
ナビゲーションバーのコンポーネントにshowNav
を渡したら完成です。
振り返り
useEffectの記述も長いし、なんらかのアンチパターンを踏んでいるかもしれませんが、一応動きます。詳しい方がいらしたら是非アドバイスください。
自力でスクロールアップ、スクロールダウンに応じて表示をコントロールするところまではできましたが、chatGPTに相談することで、もう一歩踏み込んでロジックを理解することができました。
示されたコードを噛み砕きながら読んでいくのもまた面白いですね。
以上です。ありがとうございました。
Discussion