Skip to content

useScrollParent 🔍

はじめに

複雑なページレイアウトで、要素の最も近いスクロール可能な親コンテナを見つけたいですか?useScrollParentはスマートな探知機のように、要素のスクロールコンテナを精度よく特定できます!🎯

無限スクロールの実装、仮想リスト、特定のコンテナのスクロールイベントの監視など、このHookによって正しいスクロール親要素を簡単に見つけることができ、スクロール関連の機能がより正確で信頼性の高いものになります!

コードデモ

基本的な使い方 🚀

スクロール親要素を見つけて監視する方法を簡単な例から始めましょう:

html
<template>
  <div class="container">
    <div class="scroll-area" style="height: 300px; overflow-y: auto;">
      <div class="content" style="height: 1000px;">
        <div ref="targetElement"<div class="target">
        私はターゲット要素です 🎯
      </div>
      </div>
    </div>
  </div>
</template>
js
import { ref, watch } from 'vue';
import { useScrollParent, useEventListener } from '@vant/use';

export default {
  setup() {
    const targetElement = ref();
    const scrollParent = useScrollParent(targetElement);

    // スクロールイベントを監視
    useEventListener(
      'scroll',
      (event) => {
        console.log('スクロールコンテナがスクロールしました!', {
          scrollTop: event.target.scrollTop,
          scrollLeft: event.target.scrollLeft
        });
      },
      { target: scrollParent }
    );

    // スクロール親要素の変化を監視
    watch(scrollParent, (newParent, oldParent) => {
      console.log('スクロール親要素が変更されました:', {
        old: oldParent,
        new: newParent
      });
    });

    return { 
      targetElement,
      scrollParent 
    };
  },
};

無限スクロールの実装 📜

useScrollParentを使用してインテリジェントな無限スクロールリストを実装します:

html
<template>
  <div class="infinite-list" ref="listContainer">
    <div 
      v-for="item in items" 
      :key="item.id" 
      class="list-item"
    >
      {{ item.content }}
    </div>
    <div v-if="loading" class="loading">
      読み込み中... ⏳
    </div>
    <div ref="loadTrigger" class="load-trigger"></div>
  </div>
</template>
js
import { ref, onMounted } from 'vue';
import { useScrollParent, useEventListener } from '@vant/use';

export default {
  setup() {
    const listContainer = ref();
    const loadTrigger = ref();
    const scrollParent = useScrollParent(listContainer);
    
    const items = ref([]);
    const loading = ref(false);
    const hasMore = ref(true);
    
    // より多くのデータを読み込む
    const loadMore = async () => {
      if (loading.value || !hasMore.value) return;
      
      loading.value = true;
      try {
        // APIリクエストのシミュレーション
        const newItems = await fetchMoreItems();
        items.value.push(...newItems);
        
        if (newItems.length < 10) {
          hasMore.value = false;
        }
      } catch (error) {
        console.error('読み込みに失敗しました:', error);
      } finally {
        loading.value = false;
      }
    };
    
    // より多く読み込む必要があるかどうかを確認
    const checkLoadMore = () => {
      if (!scrollParent.value || !loadTrigger.value) return;
      
      const container = scrollParent.value;
      const trigger = loadTrigger.value;
      
      const containerRect = container.getBoundingClientRect();
      const triggerRect = trigger.getBoundingClientRect();
      
      // トリガーがビューポートに入ったらより多くを読み込む
      if (triggerRect.top <= containerRect.bottom + 100) {
        loadMore();
      }
    };
    
    // 监听滚动事件
    useEventListener('scroll', checkLoadMore, { 
      target: scrollParent,
      passive: true 
    });
    
    // データの初期化
    onMounted(() => {
      loadMore();
    });
    
    return {
      listContainer,
      loadTrigger,
      items,
      loading
    };
  }
};

仮想スクロールの最適化 ⚡

useScrollParentを組み合わせて高パフォーマンスの仮想スクロールを実現します:

html
<template>
  <div 
    ref="virtualContainer" 
    class="virtual-scroll-container"
    :style="{ height: containerHeight + 'px' }"
  >
    <div 
      class="virtual-scroll-content"
      :style="{ 
        height: totalHeight + 'px',
        transform: `translateY(${offsetY}px)`
      }"
    >
      <div
        v-for="item in visibleItems"
        :key="item.id"
        class="virtual-item"
        :style="{ height: itemHeight + 'px' }"
      >
        {{ item.content }}
      </div>
    </div>
  </div>
</template>
js
import { ref, computed, onMounted } from 'vue';
import { useScrollParent, useEventListener } from '@vant/use';

export default {
  setup() {
    const virtualContainer = ref();
    const scrollParent = useScrollParent(virtualContainer);
    
    const allItems = ref([]);
    const itemHeight = 50;
    const containerHeight = 400;
    const scrollTop = ref(0);
    
    // 表示領域の計算
    const visibleCount = Math.ceil(containerHeight / itemHeight) + 2;
    const startIndex = computed(() => 
      Math.max(0, Math.floor(scrollTop.value / itemHeight) - 1)
    );
    const endIndex = computed(() => 
      Math.min(allItems.value.length, startIndex.value + visibleCount)
    );
    
    // 表示中のアイテム
    const visibleItems = computed(() => 
      allItems.value.slice(startIndex.value, endIndex.value)
    );
    
    // 総高さ
    const totalHeight = computed(() => 
      allItems.value.length * itemHeight
    );
    
    // オフセット量
    const offsetY = computed(() => 
      startIndex.value * itemHeight
    );
    
    // 监听滚动
    useEventListener('scroll', (event) => {
      scrollTop.value = event.target.scrollTop;
    }, { 
      target: scrollParent,
      passive: true 
    });
    
    // データの初期化
    onMounted(() => {
      allItems.value = Array.from({ length: 10000 }, (_, i) => ({
        id: i,
        content: `仮想リストアイテム ${i + 1}`
      }));
    });
    
    return {
      virtualContainer,
      visibleItems,
      totalHeight,
      offsetY,
      containerHeight
    };
  }
};

スクロール位置の同期 🔄

複数のコンテナ間でのスクロール位置の同期を実装します:

html
<template>
  <div class="sync-container">
    <div class="left-panel">
      <div ref="leftScroll" class="scroll-area">
        <div class="content">左側のコンテンツ領域</div>
      </div>
    </div>
    <div class="right-panel">
      <div ref="rightScroll" class="scroll-area">
        <div class="content">右側のコンテンツ領域</div>
      </div>
    </div>
  </div>
</template>
js
import { ref, nextTick } from 'vue';
import { useScrollParent, useEventListener } from '@vant/use';

export default {
  setup() {
    const leftScroll = ref();
    const rightScroll = ref();
    
    const leftScrollParent = useScrollParent(leftScroll);
    const rightScrollParent = useScrollParent(rightScroll);
    
    let isLeftScrolling = false;
    let isRightScrolling = false;
    
    // 左側のスクロールを右側に同期
    useEventListener('scroll', async (event) => {
      if (isRightScrolling) return;
      
      isLeftScrolling = true;
      const { scrollTop, scrollLeft } = event.target;
      
      await nextTick();
      if (rightScrollParent.value) {
        rightScrollParent.value.scrollTop = scrollTop;
        rightScrollParent.value.scrollLeft = scrollLeft;
      }
      
      setTimeout(() => {
        isLeftScrolling = false;
      }, 50);
    }, { target: leftScrollParent });
    
    // 右側のスクロールを左側に同期
    useEventListener('scroll', async (event) => {
      if (isLeftScrolling) return;
      
      isRightScrolling = true;
      const { scrollTop, scrollLeft } = event.target;
      
      await nextTick();
      if (leftScrollParent.value) {
        leftScrollParent.value.scrollTop = scrollTop;
        leftScrollParent.value.scrollLeft = scrollLeft;
      }
      
      setTimeout(() => {
        isRightScrolling = false;
      }, 50);
    }, { target: rightScrollParent });
    
    return {
      leftScroll,
      rightScroll
    };
  }
};

スクロール位置の記憶 💾

ページをリフレッシュした後にスクロール位置を復元する実装:

js
import { ref, onMounted, onUnmounted } from 'vue';
import { useScrollParent, useEventListener } from '@vant/use';

export default {
  setup() {
    const contentElement = ref();
    const scrollParent = useScrollParent(contentElement);
    const storageKey = 'scroll-position-memory';
    
    // スクロール位置を保存
    const saveScrollPosition = () => {
      if (!scrollParent.value) return;
      
      const position = {
        scrollTop: scrollParent.value.scrollTop,
        scrollLeft: scrollParent.value.scrollLeft,
        timestamp: Date.now()
      };
      
      localStorage.setItem(storageKey, JSON.stringify(position));
    };
    
    // スクロール位置を復元
    const restoreScrollPosition = () => {
      try {
        const saved = localStorage.getItem(storageKey);
        if (!saved || !scrollParent.value) return;
        
        const position = JSON.parse(saved);
        const timeDiff = Date.now() - position.timestamp;
        
        // 5分以内のスクロール位置のみ復元
        if (timeDiff < 5 * 60 * 1000) {
          scrollParent.value.scrollTop = position.scrollTop;
          scrollParent.value.scrollLeft = position.scrollLeft;
          console.log('スクロール位置が復元されました!📍');
        }
      } catch (error) {
        console.error('スクロール位置の復元に失敗しました:', error);
      }
    };
    
    // スクロールイベントを監視し、スロットリングで保存
    let saveTimer = null;
    useEventListener('scroll', () => {
      if (saveTimer) clearTimeout(saveTimer);
      saveTimer = setTimeout(saveScrollPosition, 300);
    }, { target: scrollParent });
    
    // ページ読み込み時に位置を復元
    onMounted(() => {
      setTimeout(restoreScrollPosition, 100);
    });
    
    // ページアンマウント時に位置を保存
    onUnmounted(() => {
      saveScrollPosition();
    });
    
    return {
      contentElement
    };
  }
};

API リファレンス 📚

型定義

ts
function useScrollParent(
  element: Ref<Element | undefined>,
): Ref<Element | Window | undefined>;

パラメータ

パラメータ説明デフォルト
elementスクロール親要素を探すターゲット要素Ref<Element | undefined>-

戻り値

パラメータ説明
scrollParent最も近いスクロール可能な親要素、要素またはwindowRef<Element | Window | undefined>

実際の使用シナリオ 🎯

1. 無限スクロールリスト

  • ニュースリスト: ユーザーが下部までスクロールすると自動的により多くのニュースを読み込む
  • 商品展示: 電子商取引サイトの商品リストの無限読み込み
  • ソーシャルフィード: フレンドリスト、マイクロブログなどのソーシャルコンテンツの無限スクロール

2. 仮想スクロールの最適化

  • 大規模データテーブル: 何千行ものデータを処理するテーブル
  • チャット履歴: 長期間のチャット履歴の仮想スクロール
  • ファイルリスト: 大量のファイルの高パフォーマンス表示

3. スクロールの同期

  • コードエディタ: 左右のパネルのスクロール同期
  • 比較ツール: ドキュメント比較時の同期スクロール
  • 二言語読書: 英語と中国語の対照読書のスクロール同期

4. スクロール位置管理

  • 読み進み状況: ユーザーの読み位置を記憶
  • フォーム入力: 長いフォームのスクロール位置記憶
  • 検索結果: 検索ページに戻るときにスクロール位置を復元

ベストプラクティス 💡

1. パフォーマンスの最適化

js
// ✅ 推奨:passiveリスナーを使用
useEventListener('scroll', handleScroll, { 
  target: scrollParent,
  passive: true 
});

// ✅ 推奨:スロットリングを使用して頻繁なトリガーを避ける
import { throttle } from 'lodash-es';
const throttledHandler = throttle(handleScroll, 16); // 60fps

2. エラー処理

js
// ✅ 推奨:スクロール親要素が存在するかどうかを確認
const handleScroll = () => {
  if (!scrollParent.value) {
    console.warn('スクロール親要素が存在しません');
    return;
  }
  
  // スクロールロジックを処理
};

3. メモリ管理

js
// ✅ 推奨:コンポーネントのアンマウント時にタイマーをクリーンアップ
onUnmounted(() => {
  if (scrollTimer) {
    clearTimeout(scrollTimer);
  }
});

4. リアクティブ処理

js
// ✅ 推奨:スクロール親要素の変化を監視
watch(scrollParent, (newParent, oldParent) => {
  if (oldParent) {
    // 古いイベントリスナーをクリーンアップ
  }
  if (newParent) {
    // 新しいイベントリスナーを追加
  }
});

デバッグテクニック 🔧

1. スクロール親要素の確認

js
// コンソールでスクロール親要素情報を表示
watch(scrollParent, (parent) => {
  console.log('スクロール親要素:', {
    element: parent,
    tagName: parent?.tagName,
    className: parent?.className,
    scrollHeight: parent?.scrollHeight,
    clientHeight: parent?.clientHeight
  });
}, { immediate: true });

2. スクロールイベントの監視

js
// スクロールイベントのトリガー状態をデバッグ
useEventListener('scroll', (event) => {
  console.log('スクロールイベント:', {
    scrollTop: event.target.scrollTop,
    scrollLeft: event.target.scrollLeft,
    timestamp: Date.now()
  });
}, { target: scrollParent });

3. スクロール領域の可視化

css
/* スクロールコンテナを可視化するために一時的にボーダーを追加 */
.debug-scroll-parent {
  border: 2px solid red !important;
  background: rgba(255, 0, 0, 0.1) !important;
}

ブラウザ互換性 🌐

useScrollParent は標準的な DOM API を使用しており、すべての現代的なブラウザをサポートします:

  • Chrome 60+
  • Firefox 55+
  • Safari 12+
  • Edge 79+

関連ドキュメント 📖

コアコンセプト

関連する Hooks

実際のアプリケーション

高度なトピック

Vant に基づく企業向けモバイルソリューション